1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.car.window; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.ValueAnimator; 22 import android.annotation.IntDef; 23 import android.content.Context; 24 import android.content.res.Resources; 25 import android.graphics.Rect; 26 import android.util.Log; 27 import android.view.GestureDetector; 28 import android.view.MotionEvent; 29 import android.view.View; 30 import android.view.ViewTreeObserver; 31 32 import androidx.annotation.CallSuper; 33 34 import com.android.systemui.car.CarDeviceProvisionedController; 35 import com.android.systemui.dagger.qualifiers.Main; 36 import com.android.wm.shell.animation.FlingAnimationUtils; 37 38 import java.lang.annotation.Retention; 39 import java.lang.annotation.RetentionPolicy; 40 41 /** 42 * The {@link OverlayPanelViewController} provides additional dragging animation capabilities to 43 * {@link OverlayViewController}. 44 */ 45 public abstract class OverlayPanelViewController extends OverlayViewController { 46 47 /** @hide */ 48 @IntDef(flag = true, prefix = { "OVERLAY_" }, value = { 49 OVERLAY_FROM_TOP_BAR, 50 OVERLAY_FROM_BOTTOM_BAR 51 }) 52 @Retention(RetentionPolicy.SOURCE) 53 public @interface OverlayDirection {} 54 55 /** 56 * Indicates that the overlay panel should be opened from the top bar and expanded by dragging 57 * towards the bottom bar. 58 */ 59 public static final int OVERLAY_FROM_TOP_BAR = 0; 60 61 /** 62 * Indicates that the overlay panel should be opened from the bottom bar and expanded by 63 * dragging towards the top bar. 64 */ 65 public static final int OVERLAY_FROM_BOTTOM_BAR = 1; 66 67 private static final boolean DEBUG = false; 68 private static final String TAG = "OverlayPanelViewController"; 69 70 // used to calculate how fast to open or close the window 71 protected static final float DEFAULT_FLING_VELOCITY = 0; 72 // max time a fling animation takes 73 protected static final float FLING_ANIMATION_MAX_TIME = 0.5f; 74 // acceleration rate for the fling animation 75 protected static final float FLING_SPEED_UP_FACTOR = 0.6f; 76 77 protected static final int SWIPE_DOWN_MIN_DISTANCE = 25; 78 protected static final int SWIPE_MAX_OFF_PATH = 75; 79 protected static final int SWIPE_THRESHOLD_VELOCITY = 200; 80 private static final int POSITIVE_DIRECTION = 1; 81 private static final int NEGATIVE_DIRECTION = -1; 82 83 private final Context mContext; 84 private final int mScreenHeightPx; 85 private final FlingAnimationUtils mFlingAnimationUtils; 86 private final CarDeviceProvisionedController mCarDeviceProvisionedController; 87 private final View.OnTouchListener mDragOpenTouchListener; 88 private final View.OnTouchListener mDragCloseTouchListener; 89 90 protected int mAnimateDirection = POSITIVE_DIRECTION; 91 92 private int mSettleClosePercentage; 93 private int mPercentageFromEndingEdge; 94 private int mPercentageCursorPositionOnScreen; 95 96 private boolean mPanelVisible; 97 private boolean mPanelExpanded; 98 99 protected float mOpeningVelocity = DEFAULT_FLING_VELOCITY; 100 protected float mClosingVelocity = DEFAULT_FLING_VELOCITY; 101 102 protected boolean mIsAnimating; 103 private boolean mIsTracking; 104 OverlayPanelViewController( Context context, @Main Resources resources, int stubId, OverlayViewGlobalStateController overlayViewGlobalStateController, FlingAnimationUtils.Builder flingAnimationUtilsBuilder, CarDeviceProvisionedController carDeviceProvisionedController )105 public OverlayPanelViewController( 106 Context context, 107 @Main Resources resources, 108 int stubId, 109 OverlayViewGlobalStateController overlayViewGlobalStateController, 110 FlingAnimationUtils.Builder flingAnimationUtilsBuilder, 111 CarDeviceProvisionedController carDeviceProvisionedController 112 ) { 113 super(stubId, overlayViewGlobalStateController); 114 115 mContext = context; 116 mScreenHeightPx = Resources.getSystem().getDisplayMetrics().heightPixels; 117 mFlingAnimationUtils = flingAnimationUtilsBuilder 118 .setMaxLengthSeconds(FLING_ANIMATION_MAX_TIME) 119 .setSpeedUpFactor(FLING_SPEED_UP_FACTOR) 120 .build(); 121 mCarDeviceProvisionedController = carDeviceProvisionedController; 122 123 // Attached to a navigation bar to open the overlay panel 124 GestureDetector openGestureDetector = new GestureDetector(context, 125 new OpenGestureListener() { 126 @Override 127 protected void open() { 128 animateExpandPanel(); 129 } 130 }); 131 132 // Attached to the other navigation bars to close the overlay panel 133 GestureDetector closeGestureDetector = new GestureDetector(context, 134 new SystemBarCloseGestureListener() { 135 @Override 136 protected void close() { 137 if (isPanelExpanded()) { 138 animateCollapsePanel(); 139 } 140 } 141 }); 142 143 mDragOpenTouchListener = (v, event) -> { 144 if (!mCarDeviceProvisionedController.isCurrentUserFullySetup()) { 145 return true; 146 } 147 if (!isInflated()) { 148 getOverlayViewGlobalStateController().inflateView(this); 149 } 150 151 boolean consumed = openGestureDetector.onTouchEvent(event); 152 if (consumed) { 153 return true; 154 } 155 int action = event.getActionMasked(); 156 if (action == MotionEvent.ACTION_UP) { 157 maybeCompleteAnimation(event); 158 } 159 160 return true; 161 }; 162 163 mDragCloseTouchListener = (v, event) -> { 164 if (!isInflated()) { 165 return true; 166 } 167 boolean consumed = closeGestureDetector.onTouchEvent(event); 168 if (consumed) { 169 return true; 170 } 171 int action = event.getActionMasked(); 172 if (action == MotionEvent.ACTION_UP) { 173 maybeCompleteAnimation(event); 174 } 175 return true; 176 }; 177 } 178 179 @Override onFinishInflate()180 protected void onFinishInflate() { 181 setUpHandleBar(); 182 } 183 184 /** Sets the overlay panel animation direction along the x or y axis. */ setOverlayDirection(@verlayDirection int direction)185 public void setOverlayDirection(@OverlayDirection int direction) { 186 if (direction == OVERLAY_FROM_TOP_BAR) { 187 mAnimateDirection = POSITIVE_DIRECTION; 188 } else if (direction == OVERLAY_FROM_BOTTOM_BAR) { 189 mAnimateDirection = NEGATIVE_DIRECTION; 190 } else { 191 throw new IllegalArgumentException("Direction not supported"); 192 } 193 } 194 195 /** Toggles the visibility of the panel. */ toggle()196 public void toggle() { 197 if (!isInflated()) { 198 getOverlayViewGlobalStateController().inflateView(this); 199 } 200 if (isPanelExpanded()) { 201 animateCollapsePanel(); 202 } else { 203 animateExpandPanel(); 204 } 205 } 206 207 /** Checks if a {@link MotionEvent} is an action to open the panel. 208 * @param e {@link MotionEvent} to check. 209 * @return true only if opening action. 210 */ isOpeningAction(MotionEvent e)211 protected boolean isOpeningAction(MotionEvent e) { 212 if (mAnimateDirection == POSITIVE_DIRECTION) { 213 return e.getActionMasked() == MotionEvent.ACTION_DOWN; 214 } 215 216 if (mAnimateDirection == NEGATIVE_DIRECTION) { 217 return e.getActionMasked() == MotionEvent.ACTION_UP; 218 } 219 220 return false; 221 } 222 223 /** Checks if a {@link MotionEvent} is an action to close the panel. 224 * @param e {@link MotionEvent} to check. 225 * @return true only if closing action. 226 */ isClosingAction(MotionEvent e)227 protected boolean isClosingAction(MotionEvent e) { 228 if (mAnimateDirection == POSITIVE_DIRECTION) { 229 return e.getActionMasked() == MotionEvent.ACTION_UP; 230 } 231 232 if (mAnimateDirection == NEGATIVE_DIRECTION) { 233 return e.getActionMasked() == MotionEvent.ACTION_DOWN; 234 } 235 236 return false; 237 } 238 239 /* ***************************************************************************************** * 240 * Panel Animation 241 * ***************************************************************************************** */ 242 243 /** Animates the closing of the panel. */ animateCollapsePanel()244 protected void animateCollapsePanel() { 245 if (!shouldAnimateCollapsePanel()) { 246 return; 247 } 248 249 if (!isPanelExpanded() || !isPanelVisible()) { 250 return; 251 } 252 253 onAnimateCollapsePanel(); 254 animatePanel(mClosingVelocity, /* isClosing= */ true); 255 } 256 257 /** Determines whether {@link #animateCollapsePanel()} should collapse the panel. */ shouldAnimateCollapsePanel()258 protected abstract boolean shouldAnimateCollapsePanel(); 259 260 /** Called when the panel is beginning to collapse. */ onAnimateCollapsePanel()261 protected abstract void onAnimateCollapsePanel(); 262 263 /** Animates the expansion of the panel. */ animateExpandPanel()264 protected void animateExpandPanel() { 265 if (!shouldAnimateExpandPanel()) { 266 return; 267 } 268 269 if (!mCarDeviceProvisionedController.isCurrentUserFullySetup()) { 270 return; 271 } 272 273 onAnimateExpandPanel(); 274 setPanelVisible(true); 275 animatePanel(mOpeningVelocity, /* isClosing= */ false); 276 277 setPanelExpanded(true); 278 } 279 280 /** Determines whether {@link #animateExpandPanel()}} should expand the panel. */ shouldAnimateExpandPanel()281 protected abstract boolean shouldAnimateExpandPanel(); 282 283 /** Called when the panel is beginning to expand. */ onAnimateExpandPanel()284 protected abstract void onAnimateExpandPanel(); 285 286 /** Returns the percentage at which we've determined whether to open or close the panel. */ getSettleClosePercentage()287 protected abstract int getSettleClosePercentage(); 288 289 /** 290 * Depending on certain conditions, determines whether to fully expand or collapse the panel. 291 */ maybeCompleteAnimation(MotionEvent event)292 protected void maybeCompleteAnimation(MotionEvent event) { 293 if (isPanelVisible()) { 294 if (mSettleClosePercentage == 0) { 295 mSettleClosePercentage = getSettleClosePercentage(); 296 } 297 298 boolean closePanel = mAnimateDirection == POSITIVE_DIRECTION 299 ? mSettleClosePercentage > mPercentageCursorPositionOnScreen 300 : mSettleClosePercentage < mPercentageCursorPositionOnScreen; 301 animatePanel(DEFAULT_FLING_VELOCITY, closePanel); 302 } 303 } 304 305 /** 306 * Animates the panel from one position to other. This is used to either open or 307 * close the panel completely with a velocity. If the animation is to close the 308 * panel this method also makes the view invisible after animation ends. 309 */ 310 protected void animatePanel(float velocity, boolean isClosing) { 311 float to = getEndPosition(isClosing); 312 313 Rect rect = getLayout().getClipBounds(); 314 if (rect != null) { 315 float from = getCurrentStartPosition(rect); 316 if (from != to) { 317 animate(from, to, velocity, isClosing); 318 } else if (isClosing) { 319 resetPanelVisibility(); 320 } else if (!mIsAnimating && !mPanelExpanded) { 321 // This case can happen when the touch ends in the navigation bar. 322 // It is important to check for mIsAnimation, because sometime a closing animation 323 // starts and the following calls will grey out the navigation bar for a sec, this 324 // looks awful ;) 325 onExpandAnimationEnd(); 326 setPanelExpanded(true); 327 } 328 329 // If we swipe down the notification panel all the way to the bottom of the screen 330 // (i.e. from == to), then we have finished animating the panel. 331 return; 332 } 333 334 // We will only be here if the shade is being opened programmatically or via button when 335 // height of the layout was not calculated. 336 ViewTreeObserver panelTreeObserver = getLayout().getViewTreeObserver(); 337 panelTreeObserver.addOnGlobalLayoutListener( 338 new ViewTreeObserver.OnGlobalLayoutListener() { 339 @Override 340 public void onGlobalLayout() { 341 ViewTreeObserver obs = getLayout().getViewTreeObserver(); 342 obs.removeOnGlobalLayoutListener(this); 343 animate( 344 getDefaultStartPosition(), 345 getEndPosition(/* isClosing= */ false), 346 velocity, 347 isClosing 348 ); 349 } 350 }); 351 } 352 353 /* Returns the start position if the user has not started swiping. */ 354 private int getDefaultStartPosition() { 355 return mAnimateDirection > 0 ? 0 : getLayout().getHeight(); 356 } 357 358 /** Returns the start position if we are in the middle of swiping. */ getCurrentStartPosition(Rect clipBounds)359 protected int getCurrentStartPosition(Rect clipBounds) { 360 return mAnimateDirection > 0 ? clipBounds.bottom : clipBounds.top; 361 } 362 getEndPosition(boolean isClosing)363 private int getEndPosition(boolean isClosing) { 364 return (mAnimateDirection > 0 && !isClosing) || (mAnimateDirection == -1 && isClosing) 365 ? getLayout().getHeight() 366 : 0; 367 } 368 animate(float from, float to, float velocity, boolean isClosing)369 protected void animate(float from, float to, float velocity, boolean isClosing) { 370 if (mIsAnimating) { 371 return; 372 } 373 mIsAnimating = true; 374 mIsTracking = true; 375 ValueAnimator animator = ValueAnimator.ofFloat(from, to); 376 animator.addUpdateListener( 377 animation -> { 378 float animatedValue = (Float) animation.getAnimatedValue(); 379 setViewClipBounds((int) animatedValue); 380 }); 381 animator.addListener(new AnimatorListenerAdapter() { 382 @Override 383 public void onAnimationEnd(Animator animation) { 384 super.onAnimationEnd(animation); 385 mIsAnimating = false; 386 mIsTracking = false; 387 mOpeningVelocity = DEFAULT_FLING_VELOCITY; 388 mClosingVelocity = DEFAULT_FLING_VELOCITY; 389 if (isClosing) { 390 resetPanelVisibility(); 391 } else { 392 onExpandAnimationEnd(); 393 setPanelExpanded(true); 394 } 395 } 396 }); 397 getFlingAnimationUtils().apply(animator, from, to, Math.abs(velocity)); 398 animator.start(); 399 } 400 resetPanelVisibility()401 protected void resetPanelVisibility() { 402 setPanelVisible(false); 403 getLayout().setClipBounds(null); 404 onCollapseAnimationEnd(); 405 setPanelExpanded(false); 406 } 407 408 /** 409 * Called in {@link Animator.AnimatorListener#onAnimationEnd(Animator)} when the panel is 410 * closing. 411 */ 412 protected abstract void onCollapseAnimationEnd(); 413 414 /** 415 * Called in {@link Animator.AnimatorListener#onAnimationEnd(Animator)} when the panel is 416 * opening. 417 */ 418 protected abstract void onExpandAnimationEnd(); 419 420 /* ***************************************************************************************** * 421 * Panel Visibility 422 * ***************************************************************************************** */ 423 424 /** Set the panel view to be visible. */ setPanelVisible(boolean visible)425 protected final void setPanelVisible(boolean visible) { 426 mPanelVisible = visible; 427 onPanelVisible(visible); 428 } 429 430 /** Returns {@code true} if panel is visible. */ isPanelVisible()431 public final boolean isPanelVisible() { 432 return mPanelVisible; 433 } 434 435 /** Business logic run when panel visibility is set. */ 436 @CallSuper onPanelVisible(boolean visible)437 protected void onPanelVisible(boolean visible) { 438 if (DEBUG) { 439 Log.e(TAG, "onPanelVisible: " + visible); 440 } 441 442 if (visible) { 443 getOverlayViewGlobalStateController().showView(/* panelViewController= */ this); 444 } 445 else if (getOverlayViewGlobalStateController().isWindowVisible()) { 446 getOverlayViewGlobalStateController().hideView(/* panelViewController= */ this); 447 } 448 getLayout().setVisibility(visible ? View.VISIBLE : View.INVISIBLE); 449 450 // TODO(b/202890142): Unify OverlayPanelViewController with super class show and hide 451 for (OverlayViewStateListener l : mViewStateListeners) { 452 l.onVisibilityChanged(visible); 453 } 454 } 455 456 /* ***************************************************************************************** * 457 * Panel Expansion 458 * ***************************************************************************************** */ 459 460 /** 461 * Set the panel state to expanded. This will expand or collapse the overlay window if 462 * necessary. 463 */ setPanelExpanded(boolean expand)464 protected final void setPanelExpanded(boolean expand) { 465 mPanelExpanded = expand; 466 onPanelExpanded(expand); 467 } 468 469 /** Returns {@code true} if panel is expanded. */ isPanelExpanded()470 public final boolean isPanelExpanded() { 471 return mPanelExpanded; 472 } 473 474 @CallSuper onPanelExpanded(boolean expand)475 protected void onPanelExpanded(boolean expand) { 476 if (DEBUG) { 477 Log.e(TAG, "onPanelExpanded: " + expand); 478 } 479 } 480 481 /* ***************************************************************************************** * 482 * Misc 483 * ***************************************************************************************** */ 484 485 /** 486 * Given the position of the pointer dragging the panel, return the percentage of its closeness 487 * to the ending edge. 488 */ calculatePercentageFromEndingEdge(float y)489 protected void calculatePercentageFromEndingEdge(float y) { 490 if (getLayout().getHeight() > 0) { 491 float height = getVisiblePanelHeight(y); 492 mPercentageFromEndingEdge = Math.round( 493 Math.abs(height / getLayout().getHeight() * 100)); 494 } 495 } 496 497 /** 498 * Given the position of the pointer dragging the panel, update its vertical position in terms 499 * of the percentage of the total height of the screen. 500 */ calculatePercentageCursorPositionOnScreen(float y)501 protected void calculatePercentageCursorPositionOnScreen(float y) { 502 mPercentageCursorPositionOnScreen = Math.round(Math.abs(y / mScreenHeightPx * 100)); 503 } 504 getVisiblePanelHeight(float y)505 private float getVisiblePanelHeight(float y) { 506 return mAnimateDirection > 0 ? y : getLayout().getHeight() - y; 507 } 508 509 /** Sets the boundaries of the overlay panel that can be seen based on pointer position. */ setViewClipBounds(int y)510 protected void setViewClipBounds(int y) { 511 // Bound the pointer position to be within the overlay panel. 512 y = Math.max(0, Math.min(y, getLayout().getHeight())); 513 Rect clipBounds = new Rect(); 514 int top, bottom; 515 if (mAnimateDirection > 0) { 516 top = 0; 517 bottom = y; 518 } else { 519 top = y; 520 bottom = getLayout().getHeight(); 521 } 522 clipBounds.set(0, top, getLayout().getWidth(), bottom); 523 getLayout().setClipBounds(clipBounds); 524 onScroll(y); 525 } 526 527 /** 528 * Called while scrolling, this passes the position of the clip boundary that is currently 529 * changing. 530 */ onScroll(int y)531 protected void onScroll(int y) { 532 if (getHandleBarViewId() == null) return; 533 View handleBar = getLayout().findViewById(getHandleBarViewId()); 534 if (handleBar == null) return; 535 536 handleBar.setTranslationY(y); 537 } 538 539 /* ***************************************************************************************** * 540 * Getters 541 * ***************************************************************************************** */ 542 543 /** Returns the open touch listener. */ getDragOpenTouchListener()544 public final View.OnTouchListener getDragOpenTouchListener() { 545 return mDragOpenTouchListener; 546 } 547 548 /** Returns the close touch listener. */ getDragCloseTouchListener()549 public final View.OnTouchListener getDragCloseTouchListener() { 550 return mDragCloseTouchListener; 551 } 552 553 /** Gets the fling animation utils used for animating this panel. */ getFlingAnimationUtils()554 protected final FlingAnimationUtils getFlingAnimationUtils() { 555 return mFlingAnimationUtils; 556 } 557 558 /** Returns {@code true} if the panel is currently tracking. */ isTracking()559 protected final boolean isTracking() { 560 return mIsTracking; 561 } 562 563 /** Sets whether the panel is currently tracking or not. */ setIsTracking(boolean isTracking)564 protected final void setIsTracking(boolean isTracking) { 565 mIsTracking = isTracking; 566 } 567 568 /** Returns {@code true} if the panel is currently animating. */ isAnimating()569 protected final boolean isAnimating() { 570 return mIsAnimating; 571 } 572 573 /** Returns the percentage of the panel that is open from the bottom. */ getPercentageFromEndingEdge()574 protected final int getPercentageFromEndingEdge() { 575 return mPercentageFromEndingEdge; 576 } 577 578 /* ***************************************************************************************** * 579 * Gesture Listeners 580 * ***************************************************************************************** */ 581 582 /** Called when the user is beginning to scroll down the panel. */ 583 protected abstract void onOpenScrollStart(); 584 585 /** 586 * Only responsible for open hooks. Since once the panel opens it covers all elements 587 * there is no need to merge with close. 588 */ 589 protected abstract class OpenGestureListener extends 590 GestureDetector.SimpleOnGestureListener { 591 592 @Override onScroll(MotionEvent event1, MotionEvent event2, float distanceX, float distanceY)593 public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX, 594 float distanceY) { 595 596 if (!isPanelVisible()) { 597 onOpenScrollStart(); 598 } 599 setPanelVisible(true); 600 601 // clips the view for the panel when the user scrolls to open. 602 setViewClipBounds((int) event2.getRawY()); 603 604 // Initially the scroll starts with height being zero. This checks protects from divide 605 // by zero error. 606 calculatePercentageFromEndingEdge(event2.getRawY()); 607 calculatePercentageCursorPositionOnScreen(event2.getRawY()); 608 609 mIsTracking = true; 610 return true; 611 } 612 613 614 @Override onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY)615 public boolean onFling(MotionEvent event1, MotionEvent event2, 616 float velocityX, float velocityY) { 617 if (mAnimateDirection * velocityY > SWIPE_THRESHOLD_VELOCITY) { 618 mOpeningVelocity = velocityY; 619 open(); 620 return true; 621 } 622 animatePanel(DEFAULT_FLING_VELOCITY, true); 623 624 return false; 625 } 626 627 protected abstract void open(); 628 } 629 630 /** Determines whether the scroll event should allow closing of the panel. */ 631 protected abstract boolean shouldAllowClosingScroll(); 632 633 protected abstract class CloseGestureListener extends 634 GestureDetector.SimpleOnGestureListener { 635 636 @Override onSingleTapUp(MotionEvent motionEvent)637 public boolean onSingleTapUp(MotionEvent motionEvent) { 638 if (isPanelExpanded()) { 639 animatePanel(DEFAULT_FLING_VELOCITY, true); 640 } 641 return true; 642 } 643 644 @Override onScroll(MotionEvent event1, MotionEvent event2, float distanceX, float distanceY)645 public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX, 646 float distanceY) { 647 if (!shouldAllowClosingScroll()) { 648 return false; 649 } 650 float y = getYPositionOfPanelEndingEdge(event1, event2); 651 if (getLayout().getHeight() > 0) { 652 mPercentageFromEndingEdge = (int) Math.abs( 653 y / getLayout().getHeight() * 100); 654 mPercentageCursorPositionOnScreen = (int) Math.abs(y / mScreenHeightPx * 100); 655 boolean isInClosingDirection = mAnimateDirection * distanceY > 0; 656 657 // This check is to figure out if onScroll was called while swiping the card at 658 // bottom of the panel. At that time we should not allow panel to 659 // close. We are also checking for the upwards swipe gesture here because it is 660 // possible if a user is closing the panel and while swiping starts 661 // to open again but does not fling. At that time we should allow the 662 // panel to close fully or else it would stuck in between. 663 if (Math.abs(getLayout().getHeight() - y) 664 > SWIPE_DOWN_MIN_DISTANCE && isInClosingDirection) { 665 setViewClipBounds((int) y); 666 mIsTracking = true; 667 } else if (!isInClosingDirection) { 668 setViewClipBounds((int) y); 669 } 670 } 671 // if we return true the items in RV won't be scrollable. 672 return false; 673 } 674 675 /** 676 * To prevent the jump in the clip bounds while closing the panel we should calculate the y 677 * position using the diff of event1 and event2. This will help the panel clip smoothly as 678 * the event2 value changes while event1 value will be fixed. 679 * @param event1 MotionEvent that contains the position of where the event2 started. 680 * @param event2 MotionEvent that contains the position of where the user has scrolled to 681 * on the screen. 682 */ getYPositionOfPanelEndingEdge(MotionEvent event1, MotionEvent event2)683 private float getYPositionOfPanelEndingEdge(MotionEvent event1, MotionEvent event2) { 684 float diff = mAnimateDirection * (event1.getRawY() - event2.getRawY()); 685 float y = mAnimateDirection > 0 ? getLayout().getHeight() - diff : diff; 686 y = Math.max(0, Math.min(y, getLayout().getHeight())); 687 return y; 688 } 689 690 @Override onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY)691 public boolean onFling(MotionEvent event1, MotionEvent event2, 692 float velocityX, float velocityY) { 693 // should not fling if the touch does not start when view is at the end of the list. 694 if (!shouldAllowClosingScroll()) { 695 return false; 696 } 697 if (Math.abs(event1.getX() - event2.getX()) > SWIPE_MAX_OFF_PATH 698 || Math.abs(velocityY) < SWIPE_THRESHOLD_VELOCITY) { 699 // swipe was not vertical or was not fast enough 700 return false; 701 } 702 boolean isInClosingDirection = mAnimateDirection * velocityY < 0; 703 if (isInClosingDirection) { 704 close(); 705 return true; 706 } else { 707 // we should close the shade 708 animatePanel(velocityY, false); 709 } 710 return false; 711 } 712 713 protected abstract void close(); 714 } 715 716 protected abstract class SystemBarCloseGestureListener extends CloseGestureListener { 717 @Override 718 public boolean onSingleTapUp(MotionEvent e) { 719 mClosingVelocity = DEFAULT_FLING_VELOCITY; 720 if (isPanelExpanded()) { 721 close(); 722 } 723 return super.onSingleTapUp(e); 724 } 725 726 @Override 727 public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX, 728 float distanceY) { 729 calculatePercentageFromEndingEdge(event2.getRawY()); 730 calculatePercentageCursorPositionOnScreen(event2.getRawY()); 731 setViewClipBounds((int) event2.getRawY()); 732 return true; 733 } 734 } 735 736 /** 737 * Optionally returns the ID of the handle bar view which enables dragging the panel to close 738 * it. Return null if no handle bar is to be set up. 739 */ 740 protected Integer getHandleBarViewId() { 741 return null; 742 }; 743 744 protected void setUpHandleBar() { 745 Integer handleBarViewId = getHandleBarViewId(); 746 if (handleBarViewId == null) return; 747 View handleBar = getLayout().findViewById(handleBarViewId); 748 if (handleBar == null) return; 749 GestureDetector handleBarCloseGestureDetector = 750 new GestureDetector(mContext, new HandleBarCloseGestureListener()); 751 handleBar.setOnTouchListener((v, event) -> { 752 int action = event.getActionMasked(); 753 switch (action) { 754 case MotionEvent.ACTION_UP: 755 maybeCompleteAnimation(event); 756 // Intentionally not breaking here, since handleBarClosureGestureDetector's 757 // onTouchEvent should still be called with MotionEvent.ACTION_UP. 758 default: 759 handleBarCloseGestureDetector.onTouchEvent(event); 760 return true; 761 } 762 }); 763 } 764 765 /** 766 * A GestureListener to be installed on the handle bar. 767 */ 768 private class HandleBarCloseGestureListener extends GestureDetector.SimpleOnGestureListener { 769 770 @Override onScroll(MotionEvent event1, MotionEvent event2, float distanceX, float distanceY)771 public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX, 772 float distanceY) { 773 calculatePercentageFromEndingEdge(event2.getRawY()); 774 calculatePercentageCursorPositionOnScreen(event2.getRawY()); 775 // To prevent the jump in the clip bounds while closing the notification panel using 776 // the handle bar, we should calculate the height using the diff of event1 and event2. 777 // This will help the notification shade to clip smoothly as the event2 value changes 778 // as event1 value will be fixed. 779 float diff = mAnimateDirection * (event1.getRawY() - event2.getRawY()); 780 float y = mAnimateDirection > 0 781 ? getLayout().getHeight() - diff 782 : diff; 783 // Ensure the position is within the overlay panel. 784 y = Math.max(0, Math.min(y, getLayout().getHeight())); 785 setViewClipBounds((int) y); 786 return true; 787 } 788 } 789 } 790