• 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 (isOverlayFromTopBar()) {
213             return e.getActionMasked() == MotionEvent.ACTION_DOWN;
214         }
215 
216         if (isOverlayFromBottomBar()) {
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 (isOverlayFromTopBar()) {
229             return e.getActionMasked() == MotionEvent.ACTION_UP;
230         }
231 
232         if (isOverlayFromBottomBar()) {
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 = isOverlayFromTopBar()
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 isOverlayFromTopBar() ? 0 : getLayout().getHeight();
356     }
357 
358     /** Returns the start position if we are in the middle of swiping. */
359     protected int getCurrentStartPosition(Rect clipBounds) {
360         return isOverlayFromTopBar() ? clipBounds.bottom : clipBounds.top;
361     }
362 
363     private int getEndPosition(boolean isClosing) {
364         return (isOverlayFromTopBar() && !isClosing) || (isOverlayFromBottomBar() && isClosing)
365                 ? getLayout().getHeight()
366                 : 0;
367     }
368 
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 isOverlayFromTopBar() ? 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 (isOverlayFromTopBar()) {
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         int handleBarPos = y;
537         if (isOverlayFromTopBar()) {
538             // For top-down panels, shift the handle bar up by its height to make space such that
539             // it is aligned to the bottom of the visible overlay area.
540             handleBarPos = Math.max(0, y - handleBar.getHeight());
541         }
542         handleBar.setTranslationY(handleBarPos);
543     }
544 
545     /* ***************************************************************************************** *
546      * Getters
547      * ***************************************************************************************** */
548 
549     /** Returns the open touch listener. */
getDragOpenTouchListener()550     public final View.OnTouchListener getDragOpenTouchListener() {
551         return mDragOpenTouchListener;
552     }
553 
554     /** Returns the close touch listener. */
getDragCloseTouchListener()555     public final View.OnTouchListener getDragCloseTouchListener() {
556         return mDragCloseTouchListener;
557     }
558 
559     /** Gets the fling animation utils used for animating this panel. */
getFlingAnimationUtils()560     protected final FlingAnimationUtils getFlingAnimationUtils() {
561         return mFlingAnimationUtils;
562     }
563 
564     /** Returns {@code true} if the panel is currently tracking. */
isTracking()565     protected final boolean isTracking() {
566         return mIsTracking;
567     }
568 
569     /** Sets whether the panel is currently tracking or not. */
setIsTracking(boolean isTracking)570     protected final void setIsTracking(boolean isTracking) {
571         mIsTracking = isTracking;
572     }
573 
574     /** Returns {@code true} if the panel is currently animating. */
isAnimating()575     protected final boolean isAnimating() {
576         return mIsAnimating;
577     }
578 
579     /** Returns the percentage of the panel that is open from the bottom. */
getPercentageFromEndingEdge()580     protected final int getPercentageFromEndingEdge() {
581         return mPercentageFromEndingEdge;
582     }
583 
isOverlayFromTopBar()584     private boolean isOverlayFromTopBar() {
585         return mAnimateDirection == POSITIVE_DIRECTION;
586     }
587 
isOverlayFromBottomBar()588     private boolean isOverlayFromBottomBar() {
589         return mAnimateDirection == NEGATIVE_DIRECTION;
590     }
591 
592     /* ***************************************************************************************** *
593      * Gesture Listeners
594      * ***************************************************************************************** */
595 
596     /** Called when the user is beginning to scroll down the panel. */
597     protected abstract void onOpenScrollStart();
598 
599     /**
600      * Only responsible for open hooks. Since once the panel opens it covers all elements
601      * there is no need to merge with close.
602      */
603     protected abstract class OpenGestureListener extends
604             GestureDetector.SimpleOnGestureListener {
605 
606         @Override
onScroll(MotionEvent event1, MotionEvent event2, float distanceX, float distanceY)607         public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
608                 float distanceY) {
609 
610             if (!isPanelVisible()) {
611                 onOpenScrollStart();
612             }
613             setPanelVisible(true);
614 
615             // clips the view for the panel when the user scrolls to open.
616             setViewClipBounds((int) event2.getRawY());
617 
618             // Initially the scroll starts with height being zero. This checks protects from divide
619             // by zero error.
620             calculatePercentageFromEndingEdge(event2.getRawY());
621             calculatePercentageCursorPositionOnScreen(event2.getRawY());
622 
623             mIsTracking = true;
624             return true;
625         }
626 
627 
628         @Override
onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY)629         public boolean onFling(MotionEvent event1, MotionEvent event2,
630                 float velocityX, float velocityY) {
631             if (mAnimateDirection * velocityY > SWIPE_THRESHOLD_VELOCITY) {
632                 mOpeningVelocity = velocityY;
633                 open();
634                 return true;
635             }
636             animatePanel(DEFAULT_FLING_VELOCITY, true);
637 
638             return false;
639         }
640 
641         protected abstract void open();
642     }
643 
644     /** Determines whether the scroll event should allow closing of the panel. */
645     protected abstract boolean shouldAllowClosingScroll();
646 
647     protected abstract class CloseGestureListener extends
648             GestureDetector.SimpleOnGestureListener {
649 
650         @Override
onSingleTapUp(MotionEvent motionEvent)651         public boolean onSingleTapUp(MotionEvent motionEvent) {
652             if (isPanelExpanded()) {
653                 animatePanel(DEFAULT_FLING_VELOCITY, true);
654             }
655             return true;
656         }
657 
658         @Override
onScroll(MotionEvent event1, MotionEvent event2, float distanceX, float distanceY)659         public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
660                 float distanceY) {
661             if (!shouldAllowClosingScroll()) {
662                 return false;
663             }
664             float y = getYPositionOfPanelEndingEdge(event1, event2);
665             if (getLayout().getHeight() > 0) {
666                 mPercentageFromEndingEdge = (int) Math.abs(
667                         y / getLayout().getHeight() * 100);
668                 mPercentageCursorPositionOnScreen = (int) Math.abs(y / mScreenHeightPx * 100);
669                 boolean isInClosingDirection = mAnimateDirection * distanceY > 0;
670 
671                 // This check is to figure out if onScroll was called while swiping the card at
672                 // bottom of the panel. At that time we should not allow panel to
673                 // close. We are also checking for the upwards swipe gesture here because it is
674                 // possible if a user is closing the panel and while swiping starts
675                 // to open again but does not fling. At that time we should allow the
676                 // panel to close fully or else it would stuck in between.
677                 if (Math.abs(getLayout().getHeight() - y)
678                         > SWIPE_DOWN_MIN_DISTANCE && isInClosingDirection) {
679                     setViewClipBounds((int) y);
680                     mIsTracking = true;
681                 } else if (!isInClosingDirection) {
682                     setViewClipBounds((int) y);
683                 }
684             }
685             // if we return true the items in RV won't be scrollable.
686             return false;
687         }
688 
689         /**
690          * To prevent the jump in the clip bounds while closing the panel we should calculate the y
691          * position using the diff of event1 and event2. This will help the panel clip smoothly as
692          * the event2 value changes while event1 value will be fixed.
693          * @param event1 MotionEvent that contains the position of where the event2 started.
694          * @param event2 MotionEvent that contains the position of where the user has scrolled to
695          *               on the screen.
696          */
getYPositionOfPanelEndingEdge(MotionEvent event1, MotionEvent event2)697         private float getYPositionOfPanelEndingEdge(MotionEvent event1, MotionEvent event2) {
698             float diff = mAnimateDirection * (event1.getRawY() - event2.getRawY());
699             float y = isOverlayFromTopBar() ? getLayout().getHeight() - diff : diff;
700             y = Math.max(0, Math.min(y, getLayout().getHeight()));
701             return y;
702         }
703 
704         @Override
onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY)705         public boolean onFling(MotionEvent event1, MotionEvent event2,
706                 float velocityX, float velocityY) {
707             // should not fling if the touch does not start when view is at the end of the list.
708             if (!shouldAllowClosingScroll()) {
709                 return false;
710             }
711             if (Math.abs(event1.getX() - event2.getX()) > SWIPE_MAX_OFF_PATH
712                     || Math.abs(velocityY) < SWIPE_THRESHOLD_VELOCITY) {
713                 // swipe was not vertical or was not fast enough
714                 return false;
715             }
716             boolean isInClosingDirection = mAnimateDirection * velocityY < 0;
717             if (isInClosingDirection) {
718                 close();
719                 return true;
720             } else {
721                 // we should close the shade
722                 animatePanel(velocityY, false);
723             }
724             return false;
725         }
726 
727         protected abstract void close();
728     }
729 
730     protected abstract class SystemBarCloseGestureListener extends CloseGestureListener {
731         @Override
732         public boolean onSingleTapUp(MotionEvent e) {
733             mClosingVelocity = DEFAULT_FLING_VELOCITY;
734             if (isPanelExpanded()) {
735                 close();
736             }
737             return super.onSingleTapUp(e);
738         }
739 
740         @Override
741         public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
742                 float distanceY) {
743             calculatePercentageFromEndingEdge(event2.getRawY());
744             calculatePercentageCursorPositionOnScreen(event2.getRawY());
745             setViewClipBounds((int) event2.getRawY());
746             return true;
747         }
748     }
749 
750     /**
751      * Optionally returns the ID of the handle bar view which enables dragging the panel to close
752      * it. Return null if no handle bar is to be set up.
753      */
754     protected Integer getHandleBarViewId() {
755         return null;
756     };
757 
758     protected void setUpHandleBar() {
759         Integer handleBarViewId = getHandleBarViewId();
760         if (handleBarViewId == null) return;
761         View handleBar = getLayout().findViewById(handleBarViewId);
762         if (handleBar == null) return;
763         GestureDetector handleBarCloseGestureDetector =
764                 new GestureDetector(mContext, new HandleBarCloseGestureListener());
765         handleBar.setOnTouchListener((v, event) -> {
766             int action = event.getActionMasked();
767             switch (action) {
768                 case MotionEvent.ACTION_UP:
769                     maybeCompleteAnimation(event);
770                     // Intentionally not breaking here, since handleBarClosureGestureDetector's
771                     // onTouchEvent should still be called with MotionEvent.ACTION_UP.
772                 default:
773                     handleBarCloseGestureDetector.onTouchEvent(event);
774                     return true;
775             }
776         });
777     }
778 
779     /**
780      * A GestureListener to be installed on the handle bar.
781      */
782     private class HandleBarCloseGestureListener extends GestureDetector.SimpleOnGestureListener {
783 
784         @Override
onScroll(MotionEvent event1, MotionEvent event2, float distanceX, float distanceY)785         public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
786                 float distanceY) {
787             calculatePercentageFromEndingEdge(event2.getRawY());
788             calculatePercentageCursorPositionOnScreen(event2.getRawY());
789             // To prevent the jump in the clip bounds while closing the notification panel using
790             // the handle bar, we should calculate the height using the diff of event1 and event2.
791             // This will help the notification shade to clip smoothly as the event2 value changes
792             // as event1 value will be fixed.
793             float diff = mAnimateDirection * (event1.getRawY() - event2.getRawY());
794             float y = isOverlayFromTopBar()
795                     ? getLayout().getHeight() - diff
796                     : diff;
797             // Ensure the position is within the overlay panel.
798             y = Math.max(0, Math.min(y, getLayout().getHeight()));
799             setViewClipBounds((int) y);
800             return true;
801         }
802     }
803 }
804