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