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