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