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