• 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.R;
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 FlingAnimationUtils mFlingAnimationUtils;
85     private final CarDeviceProvisionedController mCarDeviceProvisionedController;
86     private final View.OnTouchListener mDragOpenTouchListener;
87     private final View.OnTouchListener mDragCloseTouchListener;
88 
89     protected int mAnimateDirection = POSITIVE_DIRECTION;
90 
91     private final int mSettleClosePercentage;
92     private int mPercentageFromEndingEdge;
93 
94     private boolean mPanelVisible;
95     private boolean mPanelExpanded;
96 
97     private float mOpeningVelocity = DEFAULT_FLING_VELOCITY;
98     private float mClosingVelocity = DEFAULT_FLING_VELOCITY;
99 
100     private boolean mIsAnimating;
101     private boolean mIsTracking;
102 
OverlayPanelViewController( Context context, @Main Resources resources, int stubId, OverlayViewGlobalStateController overlayViewGlobalStateController, FlingAnimationUtils.Builder flingAnimationUtilsBuilder, CarDeviceProvisionedController carDeviceProvisionedController )103     public OverlayPanelViewController(
104             Context context,
105             @Main Resources resources,
106             int stubId,
107             OverlayViewGlobalStateController overlayViewGlobalStateController,
108             FlingAnimationUtils.Builder flingAnimationUtilsBuilder,
109             CarDeviceProvisionedController carDeviceProvisionedController
110     ) {
111         super(stubId, overlayViewGlobalStateController);
112 
113         mFlingAnimationUtils = flingAnimationUtilsBuilder
114                 .setMaxLengthSeconds(FLING_ANIMATION_MAX_TIME)
115                 .setSpeedUpFactor(FLING_SPEED_UP_FACTOR)
116                 .build();
117         mCarDeviceProvisionedController = carDeviceProvisionedController;
118 
119         mSettleClosePercentage = resources.getInteger(
120                 R.integer.notification_settle_close_percentage);
121 
122         // Attached to a navigation bar to open the overlay panel
123         GestureDetector openGestureDetector = new GestureDetector(context,
124                 new OpenGestureListener() {
125                     @Override
126                     protected void open() {
127                         animateExpandPanel();
128                     }
129                 });
130 
131         // Attached to the other navigation bars to close the overlay panel
132         GestureDetector closeGestureDetector = new GestureDetector(context,
133                 new SystemBarCloseGestureListener() {
134                     @Override
135                     protected void close() {
136                         if (isPanelExpanded()) {
137                             animateCollapsePanel();
138                         }
139                     }
140                 });
141 
142         mDragOpenTouchListener = (v, event) -> {
143             if (!mCarDeviceProvisionedController.isCurrentUserFullySetup()) {
144                 return true;
145             }
146             if (!isInflated()) {
147                 getOverlayViewGlobalStateController().inflateView(this);
148             }
149 
150             boolean consumed = openGestureDetector.onTouchEvent(event);
151             if (consumed) {
152                 return true;
153             }
154             maybeCompleteAnimation(event);
155             return true;
156         };
157 
158         mDragCloseTouchListener = (v, event) -> {
159             if (!isInflated()) {
160                 return true;
161             }
162             boolean consumed = closeGestureDetector.onTouchEvent(event);
163             if (consumed) {
164                 return true;
165             }
166             maybeCompleteAnimation(event);
167             return true;
168         };
169     }
170 
171     /** Sets the overlay panel animation direction along the x or y axis. */
setOverlayDirection(@verlayDirection int direction)172     public void setOverlayDirection(@OverlayDirection int direction) {
173         if (direction == OVERLAY_FROM_TOP_BAR) {
174             mAnimateDirection = POSITIVE_DIRECTION;
175         } else if (direction == OVERLAY_FROM_BOTTOM_BAR) {
176             mAnimateDirection = NEGATIVE_DIRECTION;
177         } else {
178             throw new IllegalArgumentException("Direction not supported");
179         }
180     }
181 
182     /** Toggles the visibility of the panel. */
toggle()183     public void toggle() {
184         if (!isInflated()) {
185             getOverlayViewGlobalStateController().inflateView(this);
186         }
187         if (isPanelExpanded()) {
188             animateCollapsePanel();
189         } else {
190             animateExpandPanel();
191         }
192     }
193 
194     /** Checks if a {@link MotionEvent} is an action to open the panel.
195      * @param e {@link MotionEvent} to check.
196      * @return true only if opening action.
197      */
isOpeningAction(MotionEvent e)198     protected boolean isOpeningAction(MotionEvent e) {
199         if (mAnimateDirection == POSITIVE_DIRECTION) {
200             return e.getActionMasked() == MotionEvent.ACTION_DOWN;
201         }
202 
203         if (mAnimateDirection == NEGATIVE_DIRECTION) {
204             return e.getActionMasked() == MotionEvent.ACTION_UP;
205         }
206 
207         return false;
208     }
209 
210     /** Checks if a {@link MotionEvent} is an action to close the panel.
211      * @param e {@link MotionEvent} to check.
212      * @return true only if closing action.
213      */
isClosingAction(MotionEvent e)214     protected boolean isClosingAction(MotionEvent e) {
215         if (mAnimateDirection == POSITIVE_DIRECTION) {
216             return e.getActionMasked() == MotionEvent.ACTION_UP;
217         }
218 
219         if (mAnimateDirection == NEGATIVE_DIRECTION) {
220             return e.getActionMasked() == MotionEvent.ACTION_DOWN;
221         }
222 
223         return false;
224     }
225 
226     /* ***************************************************************************************** *
227      * Panel Animation
228      * ***************************************************************************************** */
229 
230     /** Animates the closing of the panel. */
animateCollapsePanel()231     protected void animateCollapsePanel() {
232         if (!shouldAnimateCollapsePanel()) {
233             return;
234         }
235 
236         if (!isPanelExpanded() || !isPanelVisible()) {
237             return;
238         }
239 
240         onAnimateCollapsePanel();
241         animatePanel(mClosingVelocity, /* isClosing= */ true);
242     }
243 
244     /** Determines whether {@link #animateCollapsePanel()} should collapse the panel. */
shouldAnimateCollapsePanel()245     protected abstract boolean shouldAnimateCollapsePanel();
246 
247     /** Called when the panel is beginning to collapse. */
onAnimateCollapsePanel()248     protected abstract void onAnimateCollapsePanel();
249 
250     /** Animates the expansion of the panel. */
animateExpandPanel()251     protected void animateExpandPanel() {
252         if (!shouldAnimateExpandPanel()) {
253             return;
254         }
255 
256         if (!mCarDeviceProvisionedController.isCurrentUserFullySetup()) {
257             return;
258         }
259 
260         onAnimateExpandPanel();
261         setPanelVisible(true);
262         animatePanel(mOpeningVelocity, /* isClosing= */ false);
263 
264         setPanelExpanded(true);
265     }
266 
267     /** Determines whether {@link #animateExpandPanel()}} should expand the panel. */
shouldAnimateExpandPanel()268     protected abstract boolean shouldAnimateExpandPanel();
269 
270     /** Called when the panel is beginning to expand. */
onAnimateExpandPanel()271     protected abstract void onAnimateExpandPanel();
272 
273     /**
274      * Depending on certain conditions, determines whether to fully expand or collapse the panel.
275      */
maybeCompleteAnimation(MotionEvent event)276     protected void maybeCompleteAnimation(MotionEvent event) {
277         if (isClosingAction(event) && isPanelVisible()) {
278             if (mSettleClosePercentage < mPercentageFromEndingEdge) {
279                 animatePanel(DEFAULT_FLING_VELOCITY, false);
280             } else {
281                 animatePanel(DEFAULT_FLING_VELOCITY, true);
282             }
283         }
284     }
285 
286     /**
287      * Animates the panel from one position to other. This is used to either open or
288      * close the panel completely with a velocity. If the animation is to close the
289      * panel this method also makes the view invisible after animation ends.
290      */
animatePanel(float velocity, boolean isClosing)291     protected void animatePanel(float velocity, boolean isClosing) {
292         float to = getEndPosition(isClosing);
293 
294         Rect rect = getLayout().getClipBounds();
295         if (rect != null) {
296             float from = getCurrentStartPosition(rect);
297             if (from != to) {
298                 animate(from, to, velocity, isClosing);
299             }
300 
301             // If we swipe down the notification panel all the way to the bottom of the screen
302             // (i.e. from == to), then we have finished animating the panel.
303             return;
304         }
305 
306         // We will only be here if the shade is being opened programmatically or via button when
307         // height of the layout was not calculated.
308         ViewTreeObserver panelTreeObserver = getLayout().getViewTreeObserver();
309         panelTreeObserver.addOnGlobalLayoutListener(
310                 new ViewTreeObserver.OnGlobalLayoutListener() {
311                     @Override
312                     public void onGlobalLayout() {
313                         ViewTreeObserver obs = getLayout().getViewTreeObserver();
314                         obs.removeOnGlobalLayoutListener(this);
315                         animate(
316                                 getDefaultStartPosition(),
317                                 getEndPosition(/* isClosing= */ false),
318                                 velocity,
319                                 isClosing
320                         );
321                     }
322                 });
323     }
324 
325     /* Returns the start position if the user has not started swiping. */
getDefaultStartPosition()326     private int getDefaultStartPosition() {
327         return mAnimateDirection > 0 ? 0 : getLayout().getHeight();
328     }
329 
330     /** Returns the start position if we are in the middle of swiping. */
getCurrentStartPosition(Rect clipBounds)331     private int getCurrentStartPosition(Rect clipBounds) {
332         return mAnimateDirection > 0 ? clipBounds.bottom : clipBounds.top;
333     }
334 
getEndPosition(boolean isClosing)335     private int getEndPosition(boolean isClosing) {
336         return (mAnimateDirection > 0 && !isClosing) || (mAnimateDirection == -1 && isClosing)
337                 ? getLayout().getHeight()
338                 : 0;
339     }
340 
animate(float from, float to, float velocity, boolean isClosing)341     private void animate(float from, float to, float velocity, boolean isClosing) {
342         if (mIsAnimating) {
343             return;
344         }
345         mIsAnimating = true;
346         mIsTracking = true;
347         ValueAnimator animator = ValueAnimator.ofFloat(from, to);
348         animator.addUpdateListener(
349                 animation -> {
350                     float animatedValue = (Float) animation.getAnimatedValue();
351                     setViewClipBounds((int) animatedValue);
352                 });
353         animator.addListener(new AnimatorListenerAdapter() {
354             @Override
355             public void onAnimationEnd(Animator animation) {
356                 super.onAnimationEnd(animation);
357                 mIsAnimating = false;
358                 mIsTracking = false;
359                 mOpeningVelocity = DEFAULT_FLING_VELOCITY;
360                 mClosingVelocity = DEFAULT_FLING_VELOCITY;
361                 if (isClosing) {
362                     setPanelVisible(false);
363                     getLayout().setClipBounds(null);
364                     onCollapseAnimationEnd();
365                     setPanelExpanded(false);
366                 } else {
367                     onExpandAnimationEnd();
368                     setPanelExpanded(true);
369                 }
370             }
371         });
372         getFlingAnimationUtils().apply(animator, from, to, Math.abs(velocity));
373         animator.start();
374     }
375 
376     /**
377      * Called in {@link Animator.AnimatorListener#onAnimationEnd(Animator)} when the panel is
378      * closing.
379      */
onCollapseAnimationEnd()380     protected abstract void onCollapseAnimationEnd();
381 
382     /**
383      * Called in {@link Animator.AnimatorListener#onAnimationEnd(Animator)} when the panel is
384      * opening.
385      */
onExpandAnimationEnd()386     protected abstract void onExpandAnimationEnd();
387 
388     /* ***************************************************************************************** *
389      * Panel Visibility
390      * ***************************************************************************************** */
391 
392     /** Set the panel view to be visible. */
setPanelVisible(boolean visible)393     protected final void setPanelVisible(boolean visible) {
394         mPanelVisible = visible;
395         onPanelVisible(visible);
396     }
397 
398     /** Returns {@code true} if panel is visible. */
isPanelVisible()399     public final boolean isPanelVisible() {
400         return mPanelVisible;
401     }
402 
403     /** Business logic run when panel visibility is set. */
404     @CallSuper
onPanelVisible(boolean visible)405     protected void onPanelVisible(boolean visible) {
406         if (DEBUG) {
407             Log.e(TAG, "onPanelVisible: " + visible);
408         }
409 
410         if (visible && !getOverlayViewGlobalStateController().isWindowVisible()) {
411             getOverlayViewGlobalStateController().showView(/* panelViewController= */ this);
412         }
413         if (!visible && getOverlayViewGlobalStateController().isWindowVisible()) {
414             getOverlayViewGlobalStateController().hideView(/* panelViewController= */ this);
415         }
416         getLayout().setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
417     }
418 
419     /* ***************************************************************************************** *
420      * Panel Expansion
421      * ***************************************************************************************** */
422 
423     /**
424      * Set the panel state to expanded. This will expand or collapse the overlay window if
425      * necessary.
426      */
setPanelExpanded(boolean expand)427     protected final void setPanelExpanded(boolean expand) {
428         mPanelExpanded = expand;
429         onPanelExpanded(expand);
430     }
431 
432     /** Returns {@code true} if panel is expanded. */
isPanelExpanded()433     public final boolean isPanelExpanded() {
434         return mPanelExpanded;
435     }
436 
437     @CallSuper
onPanelExpanded(boolean expand)438     protected void onPanelExpanded(boolean expand) {
439         if (DEBUG) {
440             Log.e(TAG, "onPanelExpanded: " + expand);
441         }
442     }
443 
444     /* ***************************************************************************************** *
445      * Misc
446      * ***************************************************************************************** */
447 
448     /**
449      * Given the position of the pointer dragging the panel, return the percentage of its closeness
450      * to the ending edge.
451      */
calculatePercentageFromEndingEdge(float y)452     protected void calculatePercentageFromEndingEdge(float y) {
453         if (getLayout().getHeight() > 0) {
454             float height = getVisiblePanelHeight(y);
455             mPercentageFromEndingEdge = (int) Math.abs(height / getLayout().getHeight() * 100);
456         }
457     }
458 
getVisiblePanelHeight(float y)459     private float getVisiblePanelHeight(float y) {
460         return mAnimateDirection > 0 ? y : getLayout().getHeight() - y;
461     }
462 
463     /** Sets the boundaries of the overlay panel that can be seen based on pointer position. */
setViewClipBounds(int y)464     protected void setViewClipBounds(int y) {
465         // Bound the pointer position to be within the overlay panel.
466         y = Math.max(0, Math.min(y, getLayout().getHeight()));
467         Rect clipBounds = new Rect();
468         int top, bottom;
469         if (mAnimateDirection > 0) {
470             top = 0;
471             bottom = y;
472         } else {
473             top = y;
474             bottom = getLayout().getHeight();
475         }
476         clipBounds.set(0, top, getLayout().getWidth(), bottom);
477         getLayout().setClipBounds(clipBounds);
478         onScroll(y);
479     }
480 
481     /**
482      * Called while scrolling, this passes the position of the clip boundary that is currently
483      * changing.
484      */
onScroll(int y)485     protected abstract void onScroll(int y);
486 
487     /* ***************************************************************************************** *
488      * Getters
489      * ***************************************************************************************** */
490 
491     /** Returns the open touch listener. */
getDragOpenTouchListener()492     public final View.OnTouchListener getDragOpenTouchListener() {
493         return mDragOpenTouchListener;
494     }
495 
496     /** Returns the close touch listener. */
getDragCloseTouchListener()497     public final View.OnTouchListener getDragCloseTouchListener() {
498         return mDragCloseTouchListener;
499     }
500 
501     /** Gets the fling animation utils used for animating this panel. */
getFlingAnimationUtils()502     protected final FlingAnimationUtils getFlingAnimationUtils() {
503         return mFlingAnimationUtils;
504     }
505 
506     /** Returns {@code true} if the panel is currently tracking. */
isTracking()507     protected final boolean isTracking() {
508         return mIsTracking;
509     }
510 
511     /** Sets whether the panel is currently tracking or not. */
setIsTracking(boolean isTracking)512     protected final void setIsTracking(boolean isTracking) {
513         mIsTracking = isTracking;
514     }
515 
516     /** Returns {@code true} if the panel is currently animating. */
isAnimating()517     protected final boolean isAnimating() {
518         return mIsAnimating;
519     }
520 
521     /** Returns the percentage of the panel that is open from the bottom. */
getPercentageFromEndingEdge()522     protected final int getPercentageFromEndingEdge() {
523         return mPercentageFromEndingEdge;
524     }
525 
526     /** Returns the percentage at which we've determined whether to open or close the panel. */
getSettleClosePercentage()527     protected final int getSettleClosePercentage() {
528         return mSettleClosePercentage;
529     }
530 
531     /* ***************************************************************************************** *
532      * Gesture Listeners
533      * ***************************************************************************************** */
534 
535     /** Called when the user is beginning to scroll down the panel. */
onOpenScrollStart()536     protected abstract void onOpenScrollStart();
537 
538     /**
539      * Only responsible for open hooks. Since once the panel opens it covers all elements
540      * there is no need to merge with close.
541      */
542     protected abstract class OpenGestureListener extends
543             GestureDetector.SimpleOnGestureListener {
544 
545         @Override
onScroll(MotionEvent event1, MotionEvent event2, float distanceX, float distanceY)546         public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
547                 float distanceY) {
548 
549             if (!isPanelVisible()) {
550                 onOpenScrollStart();
551             }
552             setPanelVisible(true);
553 
554             // clips the view for the panel when the user scrolls to open.
555             setViewClipBounds((int) event2.getRawY());
556 
557             // Initially the scroll starts with height being zero. This checks protects from divide
558             // by zero error.
559             calculatePercentageFromEndingEdge(event2.getRawY());
560 
561             mIsTracking = true;
562             return true;
563         }
564 
565 
566         @Override
onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY)567         public boolean onFling(MotionEvent event1, MotionEvent event2,
568                 float velocityX, float velocityY) {
569             if (mAnimateDirection * velocityY > SWIPE_THRESHOLD_VELOCITY) {
570                 mOpeningVelocity = velocityY;
571                 open();
572                 return true;
573             }
574             animatePanel(DEFAULT_FLING_VELOCITY, true);
575 
576             return false;
577         }
578 
open()579         protected abstract void open();
580     }
581 
582     /** Determines whether the scroll event should allow closing of the panel. */
shouldAllowClosingScroll()583     protected abstract boolean shouldAllowClosingScroll();
584 
585     protected abstract class CloseGestureListener extends
586             GestureDetector.SimpleOnGestureListener {
587 
588         @Override
onSingleTapUp(MotionEvent motionEvent)589         public boolean onSingleTapUp(MotionEvent motionEvent) {
590             if (isPanelExpanded()) {
591                 animatePanel(DEFAULT_FLING_VELOCITY, true);
592             }
593             return true;
594         }
595 
596         @Override
onScroll(MotionEvent event1, MotionEvent event2, float distanceX, float distanceY)597         public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
598                 float distanceY) {
599             if (!shouldAllowClosingScroll()) {
600                 return false;
601             }
602             float y = getYPositionOfPanelEndingEdge(event1, event2);
603             if (getLayout().getHeight() > 0) {
604                 mPercentageFromEndingEdge = (int) Math.abs(
605                         y / getLayout().getHeight() * 100);
606                 boolean isInClosingDirection = mAnimateDirection * distanceY > 0;
607 
608                 // This check is to figure out if onScroll was called while swiping the card at
609                 // bottom of the panel. At that time we should not allow panel to
610                 // close. We are also checking for the upwards swipe gesture here because it is
611                 // possible if a user is closing the panel and while swiping starts
612                 // to open again but does not fling. At that time we should allow the
613                 // panel to close fully or else it would stuck in between.
614                 if (Math.abs(getLayout().getHeight() - y)
615                         > SWIPE_DOWN_MIN_DISTANCE && isInClosingDirection) {
616                     setViewClipBounds((int) y);
617                     mIsTracking = true;
618                 } else if (!isInClosingDirection) {
619                     setViewClipBounds((int) y);
620                 }
621             }
622             // if we return true the items in RV won't be scrollable.
623             return false;
624         }
625 
626         /**
627          * To prevent the jump in the clip bounds while closing the panel we should calculate the y
628          * position using the diff of event1 and event2. This will help the panel clip smoothly as
629          * the event2 value changes while event1 value will be fixed.
630          * @param event1 MotionEvent that contains the position of where the event2 started.
631          * @param event2 MotionEvent that contains the position of where the user has scrolled to
632          *               on the screen.
633          */
getYPositionOfPanelEndingEdge(MotionEvent event1, MotionEvent event2)634         private float getYPositionOfPanelEndingEdge(MotionEvent event1, MotionEvent event2) {
635             float diff = mAnimateDirection * (event1.getRawY() - event2.getRawY());
636             float y = mAnimateDirection > 0 ? getLayout().getHeight() - diff : diff;
637             y = Math.max(0, Math.min(y, getLayout().getHeight()));
638             return y;
639         }
640 
641         @Override
onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY)642         public boolean onFling(MotionEvent event1, MotionEvent event2,
643                 float velocityX, float velocityY) {
644             // should not fling if the touch does not start when view is at the end of the list.
645             if (!shouldAllowClosingScroll()) {
646                 return false;
647             }
648             if (Math.abs(event1.getX() - event2.getX()) > SWIPE_MAX_OFF_PATH
649                     || Math.abs(velocityY) < SWIPE_THRESHOLD_VELOCITY) {
650                 // swipe was not vertical or was not fast enough
651                 return false;
652             }
653             boolean isInClosingDirection = mAnimateDirection * velocityY < 0;
654             if (isInClosingDirection) {
655                 close();
656                 return true;
657             } else {
658                 // we should close the shade
659                 animatePanel(velocityY, false);
660             }
661             return false;
662         }
663 
664         protected abstract void close();
665     }
666 
667     protected abstract class SystemBarCloseGestureListener extends CloseGestureListener {
668         @Override
669         public boolean onSingleTapUp(MotionEvent e) {
670             mClosingVelocity = DEFAULT_FLING_VELOCITY;
671             if (isPanelExpanded()) {
672                 close();
673             }
674             return super.onSingleTapUp(e);
675         }
676 
677         @Override
678         public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
679                 float distanceY) {
680             calculatePercentageFromEndingEdge(event2.getRawY());
681             setViewClipBounds((int) event2.getRawY());
682             return true;
683         }
684     }
685 }
686