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