• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.car.window;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ValueAnimator;
22 import android.annotation.IntDef;
23 import android.content.Context;
24 import android.content.res.Resources;
25 import android.graphics.Rect;
26 import android.util.Log;
27 import android.view.GestureDetector;
28 import android.view.MotionEvent;
29 import android.view.View;
30 import android.view.ViewTreeObserver;
31 
32 import androidx.annotation.CallSuper;
33 
34 import com.android.systemui.car.CarDeviceProvisionedController;
35 import com.android.systemui.dagger.qualifiers.Main;
36 import com.android.wm.shell.animation.FlingAnimationUtils;
37 
38 import java.lang.annotation.Retention;
39 import java.lang.annotation.RetentionPolicy;
40 
41 /**
42  * The {@link OverlayPanelViewController} provides additional dragging animation capabilities to
43  * {@link OverlayViewController}.
44  */
45 public abstract class OverlayPanelViewController extends OverlayViewController {
46 
47     /** @hide */
48     @IntDef(flag = true, prefix = { "OVERLAY_" }, value = {
49             OVERLAY_FROM_TOP_BAR,
50             OVERLAY_FROM_BOTTOM_BAR
51     })
52     @Retention(RetentionPolicy.SOURCE)
53     public @interface OverlayDirection {}
54 
55     /**
56      * Indicates that the overlay panel should be opened from the top bar and expanded by dragging
57      * towards the bottom bar.
58      */
59     public static final int OVERLAY_FROM_TOP_BAR = 0;
60 
61     /**
62      * Indicates that the overlay panel should be opened from the bottom bar and expanded by
63      * dragging towards the top bar.
64      */
65     public static final int OVERLAY_FROM_BOTTOM_BAR = 1;
66 
67     private static final boolean DEBUG = false;
68     private static final String TAG = "OverlayPanelViewController";
69 
70     // used to calculate how fast to open or close the window
71     protected static final float DEFAULT_FLING_VELOCITY = 0;
72     // max time a fling animation takes
73     protected static final float FLING_ANIMATION_MAX_TIME = 0.5f;
74     // acceleration rate for the fling animation
75     protected static final float FLING_SPEED_UP_FACTOR = 0.6f;
76 
77     protected static final int SWIPE_DOWN_MIN_DISTANCE = 25;
78     protected static final int SWIPE_MAX_OFF_PATH = 75;
79     protected static final int SWIPE_THRESHOLD_VELOCITY = 200;
80     private static final int POSITIVE_DIRECTION = 1;
81     private static final int NEGATIVE_DIRECTION = -1;
82 
83     private final Context mContext;
84     private final int mScreenHeightPx;
85     private final FlingAnimationUtils mFlingAnimationUtils;
86     private final CarDeviceProvisionedController mCarDeviceProvisionedController;
87     private final View.OnTouchListener mDragOpenTouchListener;
88     private final View.OnTouchListener mDragCloseTouchListener;
89 
90     protected int mAnimateDirection = POSITIVE_DIRECTION;
91 
92     private int mSettleClosePercentage;
93     private int mPercentageFromEndingEdge;
94     private int mPercentageCursorPositionOnScreen;
95 
96     private boolean mPanelVisible;
97     private boolean mPanelExpanded;
98 
99     protected float mOpeningVelocity = DEFAULT_FLING_VELOCITY;
100     protected float mClosingVelocity = DEFAULT_FLING_VELOCITY;
101 
102     protected boolean mIsAnimating;
103     private boolean mIsTracking;
104 
OverlayPanelViewController( Context context, @Main Resources resources, int stubId, OverlayViewGlobalStateController overlayViewGlobalStateController, FlingAnimationUtils.Builder flingAnimationUtilsBuilder, CarDeviceProvisionedController carDeviceProvisionedController )105     public OverlayPanelViewController(
106             Context context,
107             @Main Resources resources,
108             int stubId,
109             OverlayViewGlobalStateController overlayViewGlobalStateController,
110             FlingAnimationUtils.Builder flingAnimationUtilsBuilder,
111             CarDeviceProvisionedController carDeviceProvisionedController
112     ) {
113         super(stubId, overlayViewGlobalStateController);
114 
115         mContext = context;
116         mScreenHeightPx = Resources.getSystem().getDisplayMetrics().heightPixels;
117         mFlingAnimationUtils = flingAnimationUtilsBuilder
118                 .setMaxLengthSeconds(FLING_ANIMATION_MAX_TIME)
119                 .setSpeedUpFactor(FLING_SPEED_UP_FACTOR)
120                 .build();
121         mCarDeviceProvisionedController = carDeviceProvisionedController;
122 
123         // Attached to a navigation bar to open the overlay panel
124         GestureDetector openGestureDetector = new GestureDetector(context,
125                 new OpenGestureListener() {
126                     @Override
127                     protected void open() {
128                         animateExpandPanel();
129                     }
130                 });
131 
132         // Attached to the other navigation bars to close the overlay panel
133         GestureDetector closeGestureDetector = new GestureDetector(context,
134                 new SystemBarCloseGestureListener() {
135                     @Override
136                     protected void close() {
137                         if (isPanelExpanded()) {
138                             animateCollapsePanel();
139                         }
140                     }
141                 });
142 
143         mDragOpenTouchListener = (v, event) -> {
144             if (!mCarDeviceProvisionedController.isCurrentUserFullySetup()) {
145                 return true;
146             }
147             if (!isInflated()) {
148                 getOverlayViewGlobalStateController().inflateView(this);
149             }
150 
151             boolean consumed = openGestureDetector.onTouchEvent(event);
152             if (consumed) {
153                 return true;
154             }
155             int action = event.getActionMasked();
156             if (action == MotionEvent.ACTION_UP) {
157                 maybeCompleteAnimation(event);
158             }
159 
160             return true;
161         };
162 
163         mDragCloseTouchListener = (v, event) -> {
164             if (!isInflated()) {
165                 return true;
166             }
167             boolean consumed = closeGestureDetector.onTouchEvent(event);
168             if (consumed) {
169                 return true;
170             }
171             int action = event.getActionMasked();
172             if (action == MotionEvent.ACTION_UP) {
173                 maybeCompleteAnimation(event);
174             }
175             return true;
176         };
177     }
178 
179     @Override
onFinishInflate()180     protected void onFinishInflate() {
181         setUpHandleBar();
182     }
183 
184     /** Sets the overlay panel animation direction along the x or y axis. */
setOverlayDirection(@verlayDirection int direction)185     public void setOverlayDirection(@OverlayDirection int direction) {
186         if (direction == OVERLAY_FROM_TOP_BAR) {
187             mAnimateDirection = POSITIVE_DIRECTION;
188         } else if (direction == OVERLAY_FROM_BOTTOM_BAR) {
189             mAnimateDirection = NEGATIVE_DIRECTION;
190         } else {
191             throw new IllegalArgumentException("Direction not supported");
192         }
193     }
194 
195     /** Toggles the visibility of the panel. */
toggle()196     public void toggle() {
197         if (!isInflated()) {
198             getOverlayViewGlobalStateController().inflateView(this);
199         }
200         if (isPanelExpanded()) {
201             animateCollapsePanel();
202         } else {
203             animateExpandPanel();
204         }
205     }
206 
207     /** Checks if a {@link MotionEvent} is an action to open the panel.
208      * @param e {@link MotionEvent} to check.
209      * @return true only if opening action.
210      */
isOpeningAction(MotionEvent e)211     protected boolean isOpeningAction(MotionEvent e) {
212         if (mAnimateDirection == POSITIVE_DIRECTION) {
213             return e.getActionMasked() == MotionEvent.ACTION_DOWN;
214         }
215 
216         if (mAnimateDirection == NEGATIVE_DIRECTION) {
217             return e.getActionMasked() == MotionEvent.ACTION_UP;
218         }
219 
220         return false;
221     }
222 
223     /** Checks if a {@link MotionEvent} is an action to close the panel.
224      * @param e {@link MotionEvent} to check.
225      * @return true only if closing action.
226      */
isClosingAction(MotionEvent e)227     protected boolean isClosingAction(MotionEvent e) {
228         if (mAnimateDirection == POSITIVE_DIRECTION) {
229             return e.getActionMasked() == MotionEvent.ACTION_UP;
230         }
231 
232         if (mAnimateDirection == NEGATIVE_DIRECTION) {
233             return e.getActionMasked() == MotionEvent.ACTION_DOWN;
234         }
235 
236         return false;
237     }
238 
239     /* ***************************************************************************************** *
240      * Panel Animation
241      * ***************************************************************************************** */
242 
243     /** Animates the closing of the panel. */
animateCollapsePanel()244     protected void animateCollapsePanel() {
245         if (!shouldAnimateCollapsePanel()) {
246             return;
247         }
248 
249         if (!isPanelExpanded() || !isPanelVisible()) {
250             return;
251         }
252 
253         onAnimateCollapsePanel();
254         animatePanel(mClosingVelocity, /* isClosing= */ true);
255     }
256 
257     /** Determines whether {@link #animateCollapsePanel()} should collapse the panel. */
shouldAnimateCollapsePanel()258     protected abstract boolean shouldAnimateCollapsePanel();
259 
260     /** Called when the panel is beginning to collapse. */
onAnimateCollapsePanel()261     protected abstract void onAnimateCollapsePanel();
262 
263     /** Animates the expansion of the panel. */
animateExpandPanel()264     protected void animateExpandPanel() {
265         if (!shouldAnimateExpandPanel()) {
266             return;
267         }
268 
269         if (!mCarDeviceProvisionedController.isCurrentUserFullySetup()) {
270             return;
271         }
272 
273         onAnimateExpandPanel();
274         setPanelVisible(true);
275         animatePanel(mOpeningVelocity, /* isClosing= */ false);
276 
277         setPanelExpanded(true);
278     }
279 
280     /** Determines whether {@link #animateExpandPanel()}} should expand the panel. */
shouldAnimateExpandPanel()281     protected abstract boolean shouldAnimateExpandPanel();
282 
283     /** Called when the panel is beginning to expand. */
onAnimateExpandPanel()284     protected abstract void onAnimateExpandPanel();
285 
286     /** Returns the percentage at which we've determined whether to open or close the panel. */
getSettleClosePercentage()287     protected abstract int getSettleClosePercentage();
288 
289     /**
290      * Depending on certain conditions, determines whether to fully expand or collapse the panel.
291      */
maybeCompleteAnimation(MotionEvent event)292     protected void maybeCompleteAnimation(MotionEvent event) {
293         if (isPanelVisible()) {
294             if (mSettleClosePercentage == 0) {
295                 mSettleClosePercentage = getSettleClosePercentage();
296             }
297 
298             boolean closePanel = mAnimateDirection == POSITIVE_DIRECTION
299                     ? mSettleClosePercentage > mPercentageCursorPositionOnScreen
300                     : mSettleClosePercentage < mPercentageCursorPositionOnScreen;
301             animatePanel(DEFAULT_FLING_VELOCITY, closePanel);
302         }
303     }
304 
305     /**
306      * Animates the panel from one position to other. This is used to either open or
307      * close the panel completely with a velocity. If the animation is to close the
308      * panel this method also makes the view invisible after animation ends.
309      */
310     protected void animatePanel(float velocity, boolean isClosing) {
311         float to = getEndPosition(isClosing);
312 
313         Rect rect = getLayout().getClipBounds();
314         if (rect != null) {
315             float from = getCurrentStartPosition(rect);
316             if (from != to) {
317                 animate(from, to, velocity, isClosing);
318             } else if (isClosing) {
319                 resetPanelVisibility();
320             } else if (!mIsAnimating && !mPanelExpanded) {
321                 // This case can happen when the touch ends in the navigation bar.
322                 // It is important to check for mIsAnimation, because sometime a closing animation
323                 // starts and the following calls will grey out the navigation bar for a sec, this
324                 // looks awful ;)
325                 onExpandAnimationEnd();
326                 setPanelExpanded(true);
327             }
328 
329             // If we swipe down the notification panel all the way to the bottom of the screen
330             // (i.e. from == to), then we have finished animating the panel.
331             return;
332         }
333 
334         // We will only be here if the shade is being opened programmatically or via button when
335         // height of the layout was not calculated.
336         ViewTreeObserver panelTreeObserver = getLayout().getViewTreeObserver();
337         panelTreeObserver.addOnGlobalLayoutListener(
338                 new ViewTreeObserver.OnGlobalLayoutListener() {
339                     @Override
340                     public void onGlobalLayout() {
341                         ViewTreeObserver obs = getLayout().getViewTreeObserver();
342                         obs.removeOnGlobalLayoutListener(this);
343                         animate(
344                                 getDefaultStartPosition(),
345                                 getEndPosition(/* isClosing= */ false),
346                                 velocity,
347                                 isClosing
348                         );
349                     }
350                 });
351     }
352 
353     /* Returns the start position if the user has not started swiping. */
354     private int getDefaultStartPosition() {
355         return mAnimateDirection > 0 ? 0 : getLayout().getHeight();
356     }
357 
358     /** Returns the start position if we are in the middle of swiping. */
getCurrentStartPosition(Rect clipBounds)359     protected int getCurrentStartPosition(Rect clipBounds) {
360         return mAnimateDirection > 0 ? clipBounds.bottom : clipBounds.top;
361     }
362 
getEndPosition(boolean isClosing)363     private int getEndPosition(boolean isClosing) {
364         return (mAnimateDirection > 0 && !isClosing) || (mAnimateDirection == -1 && isClosing)
365                 ? getLayout().getHeight()
366                 : 0;
367     }
368 
animate(float from, float to, float velocity, boolean isClosing)369     protected void animate(float from, float to, float velocity, boolean isClosing) {
370         if (mIsAnimating) {
371             return;
372         }
373         mIsAnimating = true;
374         mIsTracking = true;
375         ValueAnimator animator = ValueAnimator.ofFloat(from, to);
376         animator.addUpdateListener(
377                 animation -> {
378                     float animatedValue = (Float) animation.getAnimatedValue();
379                     setViewClipBounds((int) animatedValue);
380                 });
381         animator.addListener(new AnimatorListenerAdapter() {
382             @Override
383             public void onAnimationEnd(Animator animation) {
384                 super.onAnimationEnd(animation);
385                 mIsAnimating = false;
386                 mIsTracking = false;
387                 mOpeningVelocity = DEFAULT_FLING_VELOCITY;
388                 mClosingVelocity = DEFAULT_FLING_VELOCITY;
389                 if (isClosing) {
390                     resetPanelVisibility();
391                 } else {
392                     onExpandAnimationEnd();
393                     setPanelExpanded(true);
394                 }
395             }
396         });
397         getFlingAnimationUtils().apply(animator, from, to, Math.abs(velocity));
398         animator.start();
399     }
400 
resetPanelVisibility()401     protected void resetPanelVisibility() {
402         setPanelVisible(false);
403         getLayout().setClipBounds(null);
404         onCollapseAnimationEnd();
405         setPanelExpanded(false);
406     }
407 
408     /**
409      * Called in {@link Animator.AnimatorListener#onAnimationEnd(Animator)} when the panel is
410      * closing.
411      */
412     protected abstract void onCollapseAnimationEnd();
413 
414     /**
415      * Called in {@link Animator.AnimatorListener#onAnimationEnd(Animator)} when the panel is
416      * opening.
417      */
418     protected abstract void onExpandAnimationEnd();
419 
420     /* ***************************************************************************************** *
421      * Panel Visibility
422      * ***************************************************************************************** */
423 
424     /** Set the panel view to be visible. */
setPanelVisible(boolean visible)425     protected final void setPanelVisible(boolean visible) {
426         mPanelVisible = visible;
427         onPanelVisible(visible);
428     }
429 
430     /** Returns {@code true} if panel is visible. */
isPanelVisible()431     public final boolean isPanelVisible() {
432         return mPanelVisible;
433     }
434 
435     /** Business logic run when panel visibility is set. */
436     @CallSuper
onPanelVisible(boolean visible)437     protected void onPanelVisible(boolean visible) {
438         if (DEBUG) {
439             Log.e(TAG, "onPanelVisible: " + visible);
440         }
441 
442         if (visible) {
443             getOverlayViewGlobalStateController().showView(/* panelViewController= */ this);
444         }
445         else if (getOverlayViewGlobalStateController().isWindowVisible()) {
446             getOverlayViewGlobalStateController().hideView(/* panelViewController= */ this);
447         }
448         getLayout().setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
449 
450         // TODO(b/202890142): Unify OverlayPanelViewController with super class show and hide
451         for (OverlayViewStateListener l : mViewStateListeners) {
452             l.onVisibilityChanged(visible);
453         }
454     }
455 
456     /* ***************************************************************************************** *
457      * Panel Expansion
458      * ***************************************************************************************** */
459 
460     /**
461      * Set the panel state to expanded. This will expand or collapse the overlay window if
462      * necessary.
463      */
setPanelExpanded(boolean expand)464     protected final void setPanelExpanded(boolean expand) {
465         mPanelExpanded = expand;
466         onPanelExpanded(expand);
467     }
468 
469     /** Returns {@code true} if panel is expanded. */
isPanelExpanded()470     public final boolean isPanelExpanded() {
471         return mPanelExpanded;
472     }
473 
474     @CallSuper
onPanelExpanded(boolean expand)475     protected void onPanelExpanded(boolean expand) {
476         if (DEBUG) {
477             Log.e(TAG, "onPanelExpanded: " + expand);
478         }
479     }
480 
481     /* ***************************************************************************************** *
482      * Misc
483      * ***************************************************************************************** */
484 
485     /**
486      * Given the position of the pointer dragging the panel, return the percentage of its closeness
487      * to the ending edge.
488      */
calculatePercentageFromEndingEdge(float y)489     protected void calculatePercentageFromEndingEdge(float y) {
490         if (getLayout().getHeight() > 0) {
491             float height = getVisiblePanelHeight(y);
492             mPercentageFromEndingEdge = Math.round(
493                     Math.abs(height / getLayout().getHeight() * 100));
494         }
495     }
496 
497     /**
498      * Given the position of the pointer dragging the panel, update its vertical position in terms
499      * of the percentage of the total height of the screen.
500      */
calculatePercentageCursorPositionOnScreen(float y)501     protected void calculatePercentageCursorPositionOnScreen(float y) {
502         mPercentageCursorPositionOnScreen = Math.round(Math.abs(y / mScreenHeightPx * 100));
503     }
504 
getVisiblePanelHeight(float y)505     private float getVisiblePanelHeight(float y) {
506         return mAnimateDirection > 0 ? y : getLayout().getHeight() - y;
507     }
508 
509     /** Sets the boundaries of the overlay panel that can be seen based on pointer position. */
setViewClipBounds(int y)510     protected void setViewClipBounds(int y) {
511         // Bound the pointer position to be within the overlay panel.
512         y = Math.max(0, Math.min(y, getLayout().getHeight()));
513         Rect clipBounds = new Rect();
514         int top, bottom;
515         if (mAnimateDirection > 0) {
516             top = 0;
517             bottom = y;
518         } else {
519             top = y;
520             bottom = getLayout().getHeight();
521         }
522         clipBounds.set(0, top, getLayout().getWidth(), bottom);
523         getLayout().setClipBounds(clipBounds);
524         onScroll(y);
525     }
526 
527     /**
528      * Called while scrolling, this passes the position of the clip boundary that is currently
529      * changing.
530      */
onScroll(int y)531     protected void onScroll(int y) {
532         if (getHandleBarViewId() == null) return;
533         View handleBar = getLayout().findViewById(getHandleBarViewId());
534         if (handleBar == null) return;
535 
536         handleBar.setTranslationY(y);
537     }
538 
539     /* ***************************************************************************************** *
540      * Getters
541      * ***************************************************************************************** */
542 
543     /** Returns the open touch listener. */
getDragOpenTouchListener()544     public final View.OnTouchListener getDragOpenTouchListener() {
545         return mDragOpenTouchListener;
546     }
547 
548     /** Returns the close touch listener. */
getDragCloseTouchListener()549     public final View.OnTouchListener getDragCloseTouchListener() {
550         return mDragCloseTouchListener;
551     }
552 
553     /** Gets the fling animation utils used for animating this panel. */
getFlingAnimationUtils()554     protected final FlingAnimationUtils getFlingAnimationUtils() {
555         return mFlingAnimationUtils;
556     }
557 
558     /** Returns {@code true} if the panel is currently tracking. */
isTracking()559     protected final boolean isTracking() {
560         return mIsTracking;
561     }
562 
563     /** Sets whether the panel is currently tracking or not. */
setIsTracking(boolean isTracking)564     protected final void setIsTracking(boolean isTracking) {
565         mIsTracking = isTracking;
566     }
567 
568     /** Returns {@code true} if the panel is currently animating. */
isAnimating()569     protected final boolean isAnimating() {
570         return mIsAnimating;
571     }
572 
573     /** Returns the percentage of the panel that is open from the bottom. */
getPercentageFromEndingEdge()574     protected final int getPercentageFromEndingEdge() {
575         return mPercentageFromEndingEdge;
576     }
577 
578     /* ***************************************************************************************** *
579      * Gesture Listeners
580      * ***************************************************************************************** */
581 
582     /** Called when the user is beginning to scroll down the panel. */
583     protected abstract void onOpenScrollStart();
584 
585     /**
586      * Only responsible for open hooks. Since once the panel opens it covers all elements
587      * there is no need to merge with close.
588      */
589     protected abstract class OpenGestureListener extends
590             GestureDetector.SimpleOnGestureListener {
591 
592         @Override
onScroll(MotionEvent event1, MotionEvent event2, float distanceX, float distanceY)593         public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
594                 float distanceY) {
595 
596             if (!isPanelVisible()) {
597                 onOpenScrollStart();
598             }
599             setPanelVisible(true);
600 
601             // clips the view for the panel when the user scrolls to open.
602             setViewClipBounds((int) event2.getRawY());
603 
604             // Initially the scroll starts with height being zero. This checks protects from divide
605             // by zero error.
606             calculatePercentageFromEndingEdge(event2.getRawY());
607             calculatePercentageCursorPositionOnScreen(event2.getRawY());
608 
609             mIsTracking = true;
610             return true;
611         }
612 
613 
614         @Override
onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY)615         public boolean onFling(MotionEvent event1, MotionEvent event2,
616                 float velocityX, float velocityY) {
617             if (mAnimateDirection * velocityY > SWIPE_THRESHOLD_VELOCITY) {
618                 mOpeningVelocity = velocityY;
619                 open();
620                 return true;
621             }
622             animatePanel(DEFAULT_FLING_VELOCITY, true);
623 
624             return false;
625         }
626 
627         protected abstract void open();
628     }
629 
630     /** Determines whether the scroll event should allow closing of the panel. */
631     protected abstract boolean shouldAllowClosingScroll();
632 
633     protected abstract class CloseGestureListener extends
634             GestureDetector.SimpleOnGestureListener {
635 
636         @Override
onSingleTapUp(MotionEvent motionEvent)637         public boolean onSingleTapUp(MotionEvent motionEvent) {
638             if (isPanelExpanded()) {
639                 animatePanel(DEFAULT_FLING_VELOCITY, true);
640             }
641             return true;
642         }
643 
644         @Override
onScroll(MotionEvent event1, MotionEvent event2, float distanceX, float distanceY)645         public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
646                 float distanceY) {
647             if (!shouldAllowClosingScroll()) {
648                 return false;
649             }
650             float y = getYPositionOfPanelEndingEdge(event1, event2);
651             if (getLayout().getHeight() > 0) {
652                 mPercentageFromEndingEdge = (int) Math.abs(
653                         y / getLayout().getHeight() * 100);
654                 mPercentageCursorPositionOnScreen = (int) Math.abs(y / mScreenHeightPx * 100);
655                 boolean isInClosingDirection = mAnimateDirection * distanceY > 0;
656 
657                 // This check is to figure out if onScroll was called while swiping the card at
658                 // bottom of the panel. At that time we should not allow panel to
659                 // close. We are also checking for the upwards swipe gesture here because it is
660                 // possible if a user is closing the panel and while swiping starts
661                 // to open again but does not fling. At that time we should allow the
662                 // panel to close fully or else it would stuck in between.
663                 if (Math.abs(getLayout().getHeight() - y)
664                         > SWIPE_DOWN_MIN_DISTANCE && isInClosingDirection) {
665                     setViewClipBounds((int) y);
666                     mIsTracking = true;
667                 } else if (!isInClosingDirection) {
668                     setViewClipBounds((int) y);
669                 }
670             }
671             // if we return true the items in RV won't be scrollable.
672             return false;
673         }
674 
675         /**
676          * To prevent the jump in the clip bounds while closing the panel we should calculate the y
677          * position using the diff of event1 and event2. This will help the panel clip smoothly as
678          * the event2 value changes while event1 value will be fixed.
679          * @param event1 MotionEvent that contains the position of where the event2 started.
680          * @param event2 MotionEvent that contains the position of where the user has scrolled to
681          *               on the screen.
682          */
getYPositionOfPanelEndingEdge(MotionEvent event1, MotionEvent event2)683         private float getYPositionOfPanelEndingEdge(MotionEvent event1, MotionEvent event2) {
684             float diff = mAnimateDirection * (event1.getRawY() - event2.getRawY());
685             float y = mAnimateDirection > 0 ? getLayout().getHeight() - diff : diff;
686             y = Math.max(0, Math.min(y, getLayout().getHeight()));
687             return y;
688         }
689 
690         @Override
onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY)691         public boolean onFling(MotionEvent event1, MotionEvent event2,
692                 float velocityX, float velocityY) {
693             // should not fling if the touch does not start when view is at the end of the list.
694             if (!shouldAllowClosingScroll()) {
695                 return false;
696             }
697             if (Math.abs(event1.getX() - event2.getX()) > SWIPE_MAX_OFF_PATH
698                     || Math.abs(velocityY) < SWIPE_THRESHOLD_VELOCITY) {
699                 // swipe was not vertical or was not fast enough
700                 return false;
701             }
702             boolean isInClosingDirection = mAnimateDirection * velocityY < 0;
703             if (isInClosingDirection) {
704                 close();
705                 return true;
706             } else {
707                 // we should close the shade
708                 animatePanel(velocityY, false);
709             }
710             return false;
711         }
712 
713         protected abstract void close();
714     }
715 
716     protected abstract class SystemBarCloseGestureListener extends CloseGestureListener {
717         @Override
718         public boolean onSingleTapUp(MotionEvent e) {
719             mClosingVelocity = DEFAULT_FLING_VELOCITY;
720             if (isPanelExpanded()) {
721                 close();
722             }
723             return super.onSingleTapUp(e);
724         }
725 
726         @Override
727         public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
728                 float distanceY) {
729             calculatePercentageFromEndingEdge(event2.getRawY());
730             calculatePercentageCursorPositionOnScreen(event2.getRawY());
731             setViewClipBounds((int) event2.getRawY());
732             return true;
733         }
734     }
735 
736     /**
737      * Optionally returns the ID of the handle bar view which enables dragging the panel to close
738      * it. Return null if no handle bar is to be set up.
739      */
740     protected Integer getHandleBarViewId() {
741         return null;
742     };
743 
744     protected void setUpHandleBar() {
745         Integer handleBarViewId = getHandleBarViewId();
746         if (handleBarViewId == null) return;
747         View handleBar = getLayout().findViewById(handleBarViewId);
748         if (handleBar == null) return;
749         GestureDetector handleBarCloseGestureDetector =
750                 new GestureDetector(mContext, new HandleBarCloseGestureListener());
751         handleBar.setOnTouchListener((v, event) -> {
752             int action = event.getActionMasked();
753             switch (action) {
754                 case MotionEvent.ACTION_UP:
755                     maybeCompleteAnimation(event);
756                     // Intentionally not breaking here, since handleBarClosureGestureDetector's
757                     // onTouchEvent should still be called with MotionEvent.ACTION_UP.
758                 default:
759                     handleBarCloseGestureDetector.onTouchEvent(event);
760                     return true;
761             }
762         });
763     }
764 
765     /**
766      * A GestureListener to be installed on the handle bar.
767      */
768     private class HandleBarCloseGestureListener extends GestureDetector.SimpleOnGestureListener {
769 
770         @Override
onScroll(MotionEvent event1, MotionEvent event2, float distanceX, float distanceY)771         public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
772                 float distanceY) {
773             calculatePercentageFromEndingEdge(event2.getRawY());
774             calculatePercentageCursorPositionOnScreen(event2.getRawY());
775             // To prevent the jump in the clip bounds while closing the notification panel using
776             // the handle bar, we should calculate the height using the diff of event1 and event2.
777             // This will help the notification shade to clip smoothly as the event2 value changes
778             // as event1 value will be fixed.
779             float diff = mAnimateDirection * (event1.getRawY() - event2.getRawY());
780             float y = mAnimateDirection > 0
781                     ? getLayout().getHeight() - diff
782                     : diff;
783             // Ensure the position is within the overlay panel.
784             y = Math.max(0, Math.min(y, getLayout().getHeight()));
785             setViewClipBounds((int) y);
786             return true;
787         }
788     }
789 }
790