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