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 (!shouldAnimateExpandPanel()) {
145                 return true;
146             }
147             if (!mCarDeviceProvisionedController.isCurrentUserFullySetup()) {
148                 return true;
149             }
150             if (!isInflated()) {
151                 getOverlayViewGlobalStateController().inflateView(this);
152             }
153 
154             boolean consumed = openGestureDetector.onTouchEvent(event);
155             if (consumed) {
156                 return true;
157             }
158             int action = event.getActionMasked();
159             if (action == MotionEvent.ACTION_UP) {
160                 maybeCompleteAnimation(event);
161             }
162 
163             return true;
164         };
165 
166         mDragCloseTouchListener = (v, event) -> {
167             if (!isInflated()) {
168                 return true;
169             }
170             boolean consumed = closeGestureDetector.onTouchEvent(event);
171             if (consumed) {
172                 return true;
173             }
174             int action = event.getActionMasked();
175             if (action == MotionEvent.ACTION_UP) {
176                 maybeCompleteAnimation(event);
177             }
178             return true;
179         };
180     }
181 
182     @Override
onFinishInflate()183     protected void onFinishInflate() {
184         setUpHandleBar();
185     }
186 
187     /** Sets the overlay panel animation direction along the x or y axis. */
setOverlayDirection(@verlayDirection int direction)188     public void setOverlayDirection(@OverlayDirection int direction) {
189         if (direction == OVERLAY_FROM_TOP_BAR) {
190             mAnimateDirection = POSITIVE_DIRECTION;
191         } else if (direction == OVERLAY_FROM_BOTTOM_BAR) {
192             mAnimateDirection = NEGATIVE_DIRECTION;
193         } else {
194             throw new IllegalArgumentException("Direction not supported");
195         }
196     }
197 
198     /** Toggles the visibility of the panel. */
toggle()199     public void toggle() {
200         if (!isInflated()) {
201             getOverlayViewGlobalStateController().inflateView(this);
202         }
203         if (isPanelExpanded()) {
204             animateCollapsePanel();
205         } else {
206             animateExpandPanel();
207         }
208     }
209 
210     /** Checks if a {@link MotionEvent} is an action to open the panel.
211      * @param e {@link MotionEvent} to check.
212      * @return true only if opening action.
213      */
isOpeningAction(MotionEvent e)214     protected boolean isOpeningAction(MotionEvent e) {
215         if (isOverlayFromTopBar()) {
216             return e.getActionMasked() == MotionEvent.ACTION_DOWN;
217         }
218 
219         if (isOverlayFromBottomBar()) {
220             return e.getActionMasked() == MotionEvent.ACTION_UP;
221         }
222 
223         return false;
224     }
225 
226     /** Checks if a {@link MotionEvent} is an action to close the panel.
227      * @param e {@link MotionEvent} to check.
228      * @return true only if closing action.
229      */
isClosingAction(MotionEvent e)230     protected boolean isClosingAction(MotionEvent e) {
231         if (isOverlayFromTopBar()) {
232             return e.getActionMasked() == MotionEvent.ACTION_UP;
233         }
234 
235         if (isOverlayFromBottomBar()) {
236             return e.getActionMasked() == MotionEvent.ACTION_DOWN;
237         }
238 
239         return false;
240     }
241 
242     /* ***************************************************************************************** *
243      * Panel Animation
244      * ***************************************************************************************** */
245 
246     /** Animates the closing of the panel. */
animateCollapsePanel()247     protected void animateCollapsePanel() {
248         if (!shouldAnimateCollapsePanel()) {
249             return;
250         }
251 
252         if (!isPanelExpanded() && !isPanelVisible()) {
253             return;
254         }
255 
256         onAnimateCollapsePanel();
257         animatePanel(mClosingVelocity, /* isClosing= */ true);
258     }
259 
260     /** Determines whether {@link #animateCollapsePanel()} should collapse the panel. */
shouldAnimateCollapsePanel()261     protected abstract boolean shouldAnimateCollapsePanel();
262 
263     /** Called when the panel is beginning to collapse. */
onAnimateCollapsePanel()264     protected abstract void onAnimateCollapsePanel();
265 
266     /** Animates the expansion of the panel. */
animateExpandPanel()267     protected void animateExpandPanel() {
268         if (!shouldAnimateExpandPanel()) {
269             return;
270         }
271 
272         if (!mCarDeviceProvisionedController.isCurrentUserFullySetup()) {
273             return;
274         }
275 
276         onAnimateExpandPanel();
277         setPanelVisible(true);
278         animatePanel(mOpeningVelocity, /* isClosing= */ false);
279 
280         setPanelExpanded(true);
281     }
282 
283     /** Determines whether {@link #animateExpandPanel()}} should expand the panel. */
shouldAnimateExpandPanel()284     protected abstract boolean shouldAnimateExpandPanel();
285 
286     /** Called when the panel is beginning to expand. */
onAnimateExpandPanel()287     protected abstract void onAnimateExpandPanel();
288 
289     /** Returns the percentage at which we've determined whether to open or close the panel. */
getSettleClosePercentage()290     protected abstract int getSettleClosePercentage();
291 
292     /**
293      * Depending on certain conditions, determines whether to fully expand or collapse the panel.
294      */
maybeCompleteAnimation(MotionEvent event)295     protected void maybeCompleteAnimation(MotionEvent event) {
296         if (isPanelVisible()) {
297             if (mSettleClosePercentage == 0) {
298                 mSettleClosePercentage = getSettleClosePercentage();
299             }
300 
301             boolean closePanel = isOverlayFromTopBar()
302                     ? mSettleClosePercentage > mPercentageCursorPositionOnScreen
303                     : mSettleClosePercentage < mPercentageCursorPositionOnScreen;
304             animatePanel(DEFAULT_FLING_VELOCITY, closePanel);
305         }
306     }
307 
308     /**
309      * Animates the panel from one position to other. This is used to either open or
310      * close the panel completely with a velocity. If the animation is to close the
311      * panel this method also makes the view invisible after animation ends.
312      */
313     protected void animatePanel(float velocity, boolean isClosing) {
314         float to = getEndPosition(isClosing);
315 
316         Rect rect = getLayout().getClipBounds();
317         if (rect != null) {
318             float from = getCurrentStartPosition(rect);
319             if (from != to) {
320                 animate(from, to, velocity, isClosing);
321             } else if (isClosing) {
322                 resetPanelVisibility();
323             } else if (!mIsAnimating && !mPanelExpanded) {
324                 // This case can happen when the touch ends in the navigation bar.
325                 // It is important to check for mIsAnimation, because sometime a closing animation
326                 // starts and the following calls will grey out the navigation bar for a sec, this
327                 // looks awful ;)
328                 onExpandAnimationEnd();
329                 setPanelExpanded(true);
330             }
331 
332             // If we swipe down the notification panel all the way to the bottom of the screen
333             // (i.e. from == to), then we have finished animating the panel.
334             return;
335         }
336 
337         // We will only be here if the shade is being opened programmatically or via button when
338         // height of the layout was not calculated.
339         ViewTreeObserver panelTreeObserver = getLayout().getViewTreeObserver();
340         panelTreeObserver.addOnGlobalLayoutListener(
341                 new ViewTreeObserver.OnGlobalLayoutListener() {
342                     @Override
343                     public void onGlobalLayout() {
344                         ViewTreeObserver obs = getLayout().getViewTreeObserver();
345                         obs.removeOnGlobalLayoutListener(this);
346                         animate(
347                                 getDefaultStartPosition(),
348                                 getEndPosition(/* isClosing= */ false),
349                                 velocity,
350                                 isClosing
351                         );
352                     }
353                 });
354     }
355 
356     /* Returns the start position if the user has not started swiping. */
357     private int getDefaultStartPosition() {
358         return isOverlayFromTopBar() ? 0 : getLayout().getHeight();
359     }
360 
361     /** Returns the start position if we are in the middle of swiping. */
362     protected int getCurrentStartPosition(Rect clipBounds) {
363         return isOverlayFromTopBar() ? clipBounds.bottom : clipBounds.top;
364     }
365 
366     private int getEndPosition(boolean isClosing) {
367         return (isOverlayFromTopBar() && !isClosing) || (isOverlayFromBottomBar() && isClosing)
368                 ? getLayout().getHeight()
369                 : 0;
370     }
371 
372     protected void animate(float from, float to, float velocity, boolean isClosing) {
373         if (mIsAnimating) {
374             return;
375         }
376         mIsAnimating = true;
377         mIsTracking = true;
378         ValueAnimator animator = ValueAnimator.ofFloat(from, to);
379         animator.addUpdateListener(
380                 animation -> {
381                     float animatedValue = (Float) animation.getAnimatedValue();
382                     setViewClipBounds((int) animatedValue);
383                 });
384         animator.addListener(new AnimatorListenerAdapter() {
385             @Override
386             public void onAnimationEnd(Animator animation) {
387                 super.onAnimationEnd(animation);
388                 mIsAnimating = false;
389                 mIsTracking = false;
390                 mOpeningVelocity = DEFAULT_FLING_VELOCITY;
391                 mClosingVelocity = DEFAULT_FLING_VELOCITY;
392                 if (isClosing) {
393                     resetPanelVisibility();
394                 } else {
395                     onExpandAnimationEnd();
396                     setPanelExpanded(true);
397                 }
398             }
399         });
400         getFlingAnimationUtils().apply(animator, from, to, Math.abs(velocity));
401         animator.start();
402     }
403 
resetPanelVisibility()404     protected void resetPanelVisibility() {
405         setPanelVisible(false);
406         getLayout().setClipBounds(null);
407         onCollapseAnimationEnd();
408         setPanelExpanded(false);
409     }
410 
411     /**
412      * Called in {@link Animator.AnimatorListener#onAnimationEnd(Animator)} when the panel is
413      * closing.
414      */
415     protected abstract void onCollapseAnimationEnd();
416 
417     /**
418      * Called in {@link Animator.AnimatorListener#onAnimationEnd(Animator)} when the panel is
419      * opening.
420      */
421     protected abstract void onExpandAnimationEnd();
422 
423     /* ***************************************************************************************** *
424      * Panel Visibility
425      * ***************************************************************************************** */
426 
427     /** Set the panel view to be visible. */
setPanelVisible(boolean visible)428     protected final void setPanelVisible(boolean visible) {
429         mPanelVisible = visible;
430         onPanelVisible(visible);
431     }
432 
433     /** Returns {@code true} if panel is visible. */
isPanelVisible()434     public final boolean isPanelVisible() {
435         return mPanelVisible;
436     }
437 
438     /** Business logic run when panel visibility is set. */
439     @CallSuper
onPanelVisible(boolean visible)440     protected void onPanelVisible(boolean visible) {
441         if (DEBUG) {
442             Log.e(TAG, "onPanelVisible: " + visible);
443         }
444 
445         if (visible) {
446             getOverlayViewGlobalStateController().showView(/* panelViewController= */ this);
447         }
448         else if (getOverlayViewGlobalStateController().isWindowVisible()) {
449             getOverlayViewGlobalStateController().hideView(/* panelViewController= */ this);
450         }
451         getLayout().setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
452 
453         // TODO(b/202890142): Unify OverlayPanelViewController with super class show and hide
454         for (OverlayViewStateListener l : mViewStateListeners) {
455             l.onVisibilityChanged(visible);
456         }
457     }
458 
459     /* ***************************************************************************************** *
460      * Panel Expansion
461      * ***************************************************************************************** */
462 
463     /**
464      * Set the panel state to expanded. This will expand or collapse the overlay window if
465      * necessary.
466      */
setPanelExpanded(boolean expand)467     protected final void setPanelExpanded(boolean expand) {
468         mPanelExpanded = expand;
469         onPanelExpanded(expand);
470     }
471 
472     /** Returns {@code true} if panel is expanded. */
isPanelExpanded()473     public final boolean isPanelExpanded() {
474         return mPanelExpanded;
475     }
476 
477     @CallSuper
onPanelExpanded(boolean expand)478     protected void onPanelExpanded(boolean expand) {
479         if (DEBUG) {
480             Log.e(TAG, "onPanelExpanded: " + expand);
481         }
482     }
483 
484     /* ***************************************************************************************** *
485      * Misc
486      * ***************************************************************************************** */
487 
488     /**
489      * Given the position of the pointer dragging the panel, return the percentage of its closeness
490      * to the ending edge.
491      */
calculatePercentageFromEndingEdge(float y)492     protected void calculatePercentageFromEndingEdge(float y) {
493         if (getLayout().getHeight() > 0) {
494             float height = getVisiblePanelHeight(y);
495             mPercentageFromEndingEdge = Math.round(
496                     Math.abs(height / getLayout().getHeight() * 100));
497         }
498     }
499 
500     /**
501      * Given the position of the pointer dragging the panel, update its vertical position in terms
502      * of the percentage of the total height of the screen.
503      */
calculatePercentageCursorPositionOnScreen(float y)504     protected void calculatePercentageCursorPositionOnScreen(float y) {
505         mPercentageCursorPositionOnScreen = Math.round(Math.abs(y / mScreenHeightPx * 100));
506     }
507 
getVisiblePanelHeight(float y)508     private float getVisiblePanelHeight(float y) {
509         return isOverlayFromTopBar() ? y : getLayout().getHeight() - y;
510     }
511 
512     /** Sets the boundaries of the overlay panel that can be seen based on pointer position. */
setViewClipBounds(int y)513     protected void setViewClipBounds(int y) {
514         // Bound the pointer position to be within the overlay panel.
515         y = Math.max(0, Math.min(y, getLayout().getHeight()));
516         Rect clipBounds = new Rect();
517         int top, bottom;
518         if (isOverlayFromTopBar()) {
519             top = 0;
520             bottom = y;
521         } else {
522             top = y;
523             bottom = getLayout().getHeight();
524         }
525         clipBounds.set(0, top, getLayout().getWidth(), bottom);
526         getLayout().setClipBounds(clipBounds);
527         onScroll(y);
528     }
529 
530     /**
531      * Called while scrolling, this passes the position of the clip boundary that is currently
532      * changing.
533      */
onScroll(int y)534     protected void onScroll(int y) {
535         if (getHandleBarViewId() == null) return;
536         View handleBar = getLayout().findViewById(getHandleBarViewId());
537         if (handleBar == null) return;
538 
539         int handleBarPos = y;
540         if (isOverlayFromTopBar()) {
541             // For top-down panels, shift the handle bar up by its height to make space such that
542             // it is aligned to the bottom of the visible overlay area.
543             handleBarPos = Math.max(0, y - handleBar.getHeight());
544         }
545         handleBar.setTranslationY(handleBarPos);
546     }
547 
548     /* ***************************************************************************************** *
549      * Getters
550      * ***************************************************************************************** */
551 
552     /** Returns the open touch listener. */
getDragOpenTouchListener()553     public final View.OnTouchListener getDragOpenTouchListener() {
554         return mDragOpenTouchListener;
555     }
556 
557     /** Returns the close touch listener. */
getDragCloseTouchListener()558     public final View.OnTouchListener getDragCloseTouchListener() {
559         return mDragCloseTouchListener;
560     }
561 
562     /** Gets the fling animation utils used for animating this panel. */
getFlingAnimationUtils()563     protected final FlingAnimationUtils getFlingAnimationUtils() {
564         return mFlingAnimationUtils;
565     }
566 
567     /** Returns {@code true} if the panel is currently tracking. */
isTracking()568     protected final boolean isTracking() {
569         return mIsTracking;
570     }
571 
572     /** Sets whether the panel is currently tracking or not. */
setIsTracking(boolean isTracking)573     protected final void setIsTracking(boolean isTracking) {
574         mIsTracking = isTracking;
575     }
576 
577     /** Returns {@code true} if the panel is currently animating. */
isAnimating()578     protected final boolean isAnimating() {
579         return mIsAnimating;
580     }
581 
582     /** Returns the percentage of the panel that is open from the bottom. */
getPercentageFromEndingEdge()583     protected final int getPercentageFromEndingEdge() {
584         return mPercentageFromEndingEdge;
585     }
586 
isOverlayFromTopBar()587     private boolean isOverlayFromTopBar() {
588         return mAnimateDirection == POSITIVE_DIRECTION;
589     }
590 
isOverlayFromBottomBar()591     private boolean isOverlayFromBottomBar() {
592         return mAnimateDirection == NEGATIVE_DIRECTION;
593     }
594 
595     /* ***************************************************************************************** *
596      * Gesture Listeners
597      * ***************************************************************************************** */
598 
599     /** Called when the user is beginning to scroll down the panel. */
600     protected abstract void onOpenScrollStart();
601 
602     /**
603      * Only responsible for open hooks. Since once the panel opens it covers all elements
604      * there is no need to merge with close.
605      */
606     protected abstract class OpenGestureListener extends
607             GestureDetector.SimpleOnGestureListener {
608 
609         @Override
onScroll(MotionEvent event1, MotionEvent event2, float distanceX, float distanceY)610         public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
611                 float distanceY) {
612 
613             if (!isPanelVisible()) {
614                 onOpenScrollStart();
615             }
616             setPanelVisible(true);
617 
618             // clips the view for the panel when the user scrolls to open.
619             setViewClipBounds((int) event2.getRawY());
620 
621             // Initially the scroll starts with height being zero. This checks protects from divide
622             // by zero error.
623             calculatePercentageFromEndingEdge(event2.getRawY());
624             calculatePercentageCursorPositionOnScreen(event2.getRawY());
625 
626             mIsTracking = true;
627             return true;
628         }
629 
630 
631         @Override
onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY)632         public boolean onFling(MotionEvent event1, MotionEvent event2,
633                 float velocityX, float velocityY) {
634             if (mAnimateDirection * velocityY > SWIPE_THRESHOLD_VELOCITY) {
635                 mOpeningVelocity = velocityY;
636                 open();
637                 return true;
638             }
639             animatePanel(DEFAULT_FLING_VELOCITY, true);
640 
641             return false;
642         }
643 
644         protected abstract void open();
645     }
646 
647     /** Determines whether the scroll event should allow closing of the panel. */
648     protected abstract boolean shouldAllowClosingScroll();
649 
650     protected abstract class CloseGestureListener extends
651             GestureDetector.SimpleOnGestureListener {
652 
653         @Override
onSingleTapUp(MotionEvent motionEvent)654         public boolean onSingleTapUp(MotionEvent motionEvent) {
655             if (isPanelExpanded()) {
656                 animatePanel(DEFAULT_FLING_VELOCITY, true);
657             }
658             return true;
659         }
660 
661         @Override
onScroll(MotionEvent event1, MotionEvent event2, float distanceX, float distanceY)662         public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
663                 float distanceY) {
664             if (!shouldAllowClosingScroll()) {
665                 return false;
666             }
667             float y = getYPositionOfPanelEndingEdge(event1, event2);
668             if (getLayout().getHeight() > 0) {
669                 mPercentageFromEndingEdge = (int) Math.abs(
670                         y / getLayout().getHeight() * 100);
671                 mPercentageCursorPositionOnScreen = (int) Math.abs(y / mScreenHeightPx * 100);
672                 boolean isInClosingDirection = mAnimateDirection * distanceY > 0;
673 
674                 // This check is to figure out if onScroll was called while swiping the card at
675                 // bottom of the panel. At that time we should not allow panel to
676                 // close. We are also checking for the upwards swipe gesture here because it is
677                 // possible if a user is closing the panel and while swiping starts
678                 // to open again but does not fling. At that time we should allow the
679                 // panel to close fully or else it would stuck in between.
680                 if (Math.abs(getLayout().getHeight() - y)
681                         > SWIPE_DOWN_MIN_DISTANCE && isInClosingDirection) {
682                     setViewClipBounds((int) y);
683                     mIsTracking = true;
684                 } else if (!isInClosingDirection) {
685                     setViewClipBounds((int) y);
686                 }
687             }
688             // if we return true the items in RV won't be scrollable.
689             return false;
690         }
691 
692         /**
693          * To prevent the jump in the clip bounds while closing the panel we should calculate the y
694          * position using the diff of event1 and event2. This will help the panel clip smoothly as
695          * the event2 value changes while event1 value will be fixed.
696          * @param event1 MotionEvent that contains the position of where the event2 started.
697          * @param event2 MotionEvent that contains the position of where the user has scrolled to
698          *               on the screen.
699          */
getYPositionOfPanelEndingEdge(MotionEvent event1, MotionEvent event2)700         private float getYPositionOfPanelEndingEdge(MotionEvent event1, MotionEvent event2) {
701             float diff = mAnimateDirection * (event1.getRawY() - event2.getRawY());
702             float y = isOverlayFromTopBar() ? getLayout().getHeight() - diff : diff;
703             y = Math.max(0, Math.min(y, getLayout().getHeight()));
704             return y;
705         }
706 
707         @Override
onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY)708         public boolean onFling(MotionEvent event1, MotionEvent event2,
709                 float velocityX, float velocityY) {
710             // should not fling if the touch does not start when view is at the end of the list.
711             if (!shouldAllowClosingScroll()) {
712                 return false;
713             }
714             if (Math.abs(event1.getX() - event2.getX()) > SWIPE_MAX_OFF_PATH
715                     || Math.abs(velocityY) < SWIPE_THRESHOLD_VELOCITY) {
716                 // swipe was not vertical or was not fast enough
717                 return false;
718             }
719             boolean isInClosingDirection = mAnimateDirection * velocityY < 0;
720             if (isInClosingDirection) {
721                 close();
722                 return true;
723             } else {
724                 // we should close the shade
725                 animatePanel(velocityY, false);
726             }
727             return false;
728         }
729 
730         protected abstract void close();
731     }
732 
733     protected abstract class SystemBarCloseGestureListener extends CloseGestureListener {
734         @Override
735         public boolean onSingleTapUp(MotionEvent e) {
736             mClosingVelocity = DEFAULT_FLING_VELOCITY;
737             if (isPanelExpanded()) {
738                 close();
739             }
740             return super.onSingleTapUp(e);
741         }
742 
743         @Override
744         public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
745                 float distanceY) {
746             calculatePercentageFromEndingEdge(event2.getRawY());
747             calculatePercentageCursorPositionOnScreen(event2.getRawY());
748             setViewClipBounds((int) event2.getRawY());
749             return true;
750         }
751     }
752 
753     /**
754      * Optionally returns the ID of the handle bar view which enables dragging the panel to close
755      * it. Return null if no handle bar is to be set up.
756      */
757     protected Integer getHandleBarViewId() {
758         return null;
759     };
760 
761     protected void setUpHandleBar() {
762         Integer handleBarViewId = getHandleBarViewId();
763         if (handleBarViewId == null) return;
764         View handleBar = getLayout().findViewById(handleBarViewId);
765         if (handleBar == null) return;
766         GestureDetector handleBarCloseGestureDetector =
767                 new GestureDetector(mContext, new HandleBarCloseGestureListener());
768         handleBar.setOnTouchListener((v, event) -> {
769             int action = event.getActionMasked();
770             switch (action) {
771                 case MotionEvent.ACTION_UP:
772                     maybeCompleteAnimation(event);
773                     // Intentionally not breaking here, since handleBarClosureGestureDetector's
774                     // onTouchEvent should still be called with MotionEvent.ACTION_UP.
775                 default:
776                     handleBarCloseGestureDetector.onTouchEvent(event);
777                     return true;
778             }
779         });
780     }
781 
782     /**
783      * A GestureListener to be installed on the handle bar.
784      */
785     private class HandleBarCloseGestureListener extends GestureDetector.SimpleOnGestureListener {
786 
787         @Override
onScroll(MotionEvent event1, MotionEvent event2, float distanceX, float distanceY)788         public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
789                 float distanceY) {
790             calculatePercentageFromEndingEdge(event2.getRawY());
791             calculatePercentageCursorPositionOnScreen(event2.getRawY());
792             // To prevent the jump in the clip bounds while closing the notification panel using
793             // the handle bar, we should calculate the height using the diff of event1 and event2.
794             // This will help the notification shade to clip smoothly as the event2 value changes
795             // as event1 value will be fixed.
796             float diff = mAnimateDirection * (event1.getRawY() - event2.getRawY());
797             float y = isOverlayFromTopBar()
798                     ? getLayout().getHeight() - diff
799                     : diff;
800             // Ensure the position is within the overlay panel.
801             y = Math.max(0, Math.min(y, getLayout().getHeight()));
802             setViewClipBounds((int) y);
803             return true;
804         }
805     }
806 }
807