1 /* 2 * Copyright (C) 2014 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 18 package com.android.intentresolver.widget; 19 20 import static android.content.res.Resources.ID_NULL; 21 22 import android.content.Context; 23 import android.content.res.TypedArray; 24 import android.graphics.Canvas; 25 import android.graphics.Rect; 26 import android.graphics.drawable.Drawable; 27 import android.metrics.LogMaker; 28 import android.os.Bundle; 29 import android.os.Parcel; 30 import android.os.Parcelable; 31 import android.util.AttributeSet; 32 import android.util.Log; 33 import android.view.MotionEvent; 34 import android.view.VelocityTracker; 35 import android.view.View; 36 import android.view.ViewConfiguration; 37 import android.view.ViewGroup; 38 import android.view.ViewParent; 39 import android.view.ViewTreeObserver; 40 import android.view.accessibility.AccessibilityEvent; 41 import android.view.accessibility.AccessibilityNodeInfo; 42 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 43 import android.view.animation.AnimationUtils; 44 import android.widget.AbsListView; 45 import android.widget.OverScroller; 46 47 import androidx.annotation.IdRes; 48 import androidx.annotation.NonNull; 49 import androidx.annotation.Nullable; 50 import androidx.core.view.ScrollingView; 51 import androidx.recyclerview.widget.RecyclerView; 52 53 import com.android.intentresolver.R; 54 import com.android.internal.logging.MetricsLogger; 55 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 56 57 public class ResolverDrawerLayout extends ViewGroup { 58 private static final String TAG = "ResolverDrawerLayout"; 59 private MetricsLogger mMetricsLogger; 60 61 /** 62 * Max width of the whole drawer layout 63 */ 64 private int mMaxWidth; 65 66 /** 67 * Max total visible height of views not marked always-show when in the closed/initial state 68 */ 69 private int mMaxCollapsedHeight; 70 71 /** 72 * Max total visible height of views not marked always-show when in the closed/initial state 73 * when a default option is present 74 */ 75 private int mMaxCollapsedHeightSmall; 76 77 /** 78 * Whether {@code mMaxCollapsedHeightSmall} was set explicitly as a layout attribute or 79 * inferred by {@code mMaxCollapsedHeight}. 80 */ 81 private final boolean mIsMaxCollapsedHeightSmallExplicit; 82 83 private boolean mSmallCollapsed; 84 85 /** 86 * Move views down from the top by this much in px 87 */ 88 private float mCollapseOffset; 89 90 /** 91 * Track fractions of pixels from drag calculations. Without this, the view offsets get 92 * out of sync due to frequently dropping fractions of a pixel from '(int) dy' casts. 93 */ 94 private float mDragRemainder = 0.0f; 95 private int mHeightUsed; 96 private int mCollapsibleHeight; 97 private int mAlwaysShowHeight; 98 99 /** 100 * The height in pixels of reserved space added to the top of the collapsed UI; 101 * e.g. chooser targets 102 */ 103 private int mCollapsibleHeightReserved; 104 105 private int mTopOffset; 106 private boolean mShowAtTop; 107 @IdRes 108 private int mIgnoreOffsetTopLimitViewId = ID_NULL; 109 110 private boolean mIsDragging; 111 private boolean mOpenOnClick; 112 private boolean mOpenOnLayout; 113 private boolean mDismissOnScrollerFinished; 114 private final int mTouchSlop; 115 private final float mMinFlingVelocity; 116 private final OverScroller mScroller; 117 private final VelocityTracker mVelocityTracker; 118 119 private Drawable mScrollIndicatorDrawable; 120 121 private OnDismissedListener mOnDismissedListener; 122 private RunOnDismissedListener mRunOnDismissedListener; 123 private OnCollapsedChangedListener mOnCollapsedChangedListener; 124 125 private boolean mDismissLocked; 126 127 private float mInitialTouchX; 128 private float mInitialTouchY; 129 private float mLastTouchY; 130 private int mActivePointerId = MotionEvent.INVALID_POINTER_ID; 131 132 private final Rect mTempRect = new Rect(); 133 134 private AbsListView mNestedListChild; 135 private RecyclerView mNestedRecyclerChild; 136 137 @Nullable 138 private final ScrollablePreviewFlingLogicDelegate mFlingLogicDelegate; 139 140 private final ViewTreeObserver.OnTouchModeChangeListener mTouchModeChangeListener = 141 new ViewTreeObserver.OnTouchModeChangeListener() { 142 @Override 143 public void onTouchModeChanged(boolean isInTouchMode) { 144 if (!isInTouchMode && hasFocus() && isDescendantClipped(getFocusedChild())) { 145 smoothScrollTo(0, 0); 146 } 147 } 148 }; 149 ResolverDrawerLayout(Context context)150 public ResolverDrawerLayout(Context context) { 151 this(context, null); 152 } 153 ResolverDrawerLayout(Context context, AttributeSet attrs)154 public ResolverDrawerLayout(Context context, AttributeSet attrs) { 155 this(context, attrs, 0); 156 } 157 ResolverDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr)158 public ResolverDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) { 159 super(context, attrs, defStyleAttr); 160 161 final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ResolverDrawerLayout, 162 defStyleAttr, 0); 163 mMaxWidth = a.getDimensionPixelSize(R.styleable.ResolverDrawerLayout_android_maxWidth, -1); 164 mMaxCollapsedHeight = a.getDimensionPixelSize( 165 R.styleable.ResolverDrawerLayout_maxCollapsedHeight, 0); 166 mMaxCollapsedHeightSmall = a.getDimensionPixelSize( 167 R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall, 168 mMaxCollapsedHeight); 169 mIsMaxCollapsedHeightSmallExplicit = 170 a.hasValue(R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall); 171 mShowAtTop = a.getBoolean(R.styleable.ResolverDrawerLayout_showAtTop, false); 172 if (a.hasValue(R.styleable.ResolverDrawerLayout_ignoreOffsetTopLimit)) { 173 mIgnoreOffsetTopLimitViewId = a.getResourceId( 174 R.styleable.ResolverDrawerLayout_ignoreOffsetTopLimit, ID_NULL); 175 } 176 mFlingLogicDelegate = 177 a.getBoolean( 178 R.styleable.ResolverDrawerLayout_useScrollablePreviewNestedFlingLogic, 179 false) 180 ? new ScrollablePreviewFlingLogicDelegate() {} 181 : null; 182 a.recycle(); 183 184 mScrollIndicatorDrawable = mContext.getDrawable( 185 com.android.internal.R.drawable.scroll_indicator_material); 186 187 mScroller = new OverScroller(context, AnimationUtils.loadInterpolator(context, 188 android.R.interpolator.decelerate_quint)); 189 mVelocityTracker = VelocityTracker.obtain(); 190 191 final ViewConfiguration vc = ViewConfiguration.get(context); 192 mTouchSlop = vc.getScaledTouchSlop(); 193 mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); 194 195 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); 196 } 197 198 /** 199 * Dynamically set the max collapsed height. Note this also updates the small collapsed 200 * height if it wasn't specified explicitly. 201 */ setMaxCollapsedHeight(int heightInPixels)202 public void setMaxCollapsedHeight(int heightInPixels) { 203 if (heightInPixels == mMaxCollapsedHeight) { 204 return; 205 } 206 mMaxCollapsedHeight = heightInPixels; 207 if (!mIsMaxCollapsedHeightSmallExplicit) { 208 mMaxCollapsedHeightSmall = mMaxCollapsedHeight; 209 } 210 requestLayout(); 211 } 212 setSmallCollapsed(boolean smallCollapsed)213 public void setSmallCollapsed(boolean smallCollapsed) { 214 if (mSmallCollapsed != smallCollapsed) { 215 mSmallCollapsed = smallCollapsed; 216 requestLayout(); 217 } 218 } 219 isSmallCollapsed()220 public boolean isSmallCollapsed() { 221 return mSmallCollapsed; 222 } 223 isCollapsed()224 public boolean isCollapsed() { 225 return mCollapseOffset > 0; 226 } 227 setShowAtTop(boolean showOnTop)228 public void setShowAtTop(boolean showOnTop) { 229 if (mShowAtTop != showOnTop) { 230 mShowAtTop = showOnTop; 231 requestLayout(); 232 } 233 } 234 getShowAtTop()235 public boolean getShowAtTop() { 236 return mShowAtTop; 237 } 238 setCollapsed(boolean collapsed)239 public void setCollapsed(boolean collapsed) { 240 if (!isLaidOut()) { 241 mOpenOnLayout = !collapsed; 242 } else { 243 smoothScrollTo(collapsed ? mCollapsibleHeight : 0, 0); 244 } 245 } 246 setCollapsibleHeightReserved(int heightPixels)247 public void setCollapsibleHeightReserved(int heightPixels) { 248 final int oldReserved = mCollapsibleHeightReserved; 249 mCollapsibleHeightReserved = heightPixels; 250 if (oldReserved != mCollapsibleHeightReserved) { 251 requestLayout(); 252 } 253 254 final int dReserved = mCollapsibleHeightReserved - oldReserved; 255 if (dReserved != 0 && mIsDragging) { 256 mLastTouchY -= dReserved; 257 } 258 259 final int oldCollapsibleHeight = updateCollapsibleHeight(); 260 if (updateCollapseOffset(oldCollapsibleHeight, !isDragging())) { 261 return; 262 } 263 264 invalidate(); 265 } 266 267 /** 268 * Sets max drawer width. 269 */ setMaxWidth(int maxWidth)270 public void setMaxWidth(int maxWidth) { 271 if (mMaxWidth != maxWidth) { 272 mMaxWidth = maxWidth; 273 requestLayout(); 274 } 275 } 276 setDismissLocked(boolean locked)277 public void setDismissLocked(boolean locked) { 278 mDismissLocked = locked; 279 } 280 getTopOffset()281 int getTopOffset() { 282 return mTopOffset; 283 } 284 isMoving()285 private boolean isMoving() { 286 return mIsDragging || !mScroller.isFinished(); 287 } 288 isDragging()289 private boolean isDragging() { 290 return mIsDragging || getNestedScrollAxes() == SCROLL_AXIS_VERTICAL; 291 } 292 updateCollapseOffset(int oldCollapsibleHeight, boolean remainClosed)293 private boolean updateCollapseOffset(int oldCollapsibleHeight, boolean remainClosed) { 294 if (oldCollapsibleHeight == mCollapsibleHeight) { 295 return false; 296 } 297 298 if (getShowAtTop()) { 299 // Keep the drawer fully open. 300 setCollapseOffset(0); 301 return false; 302 } 303 304 if (isLaidOut()) { 305 final boolean isCollapsedOld = mCollapseOffset != 0; 306 if (remainClosed && (oldCollapsibleHeight < mCollapsibleHeight 307 && mCollapseOffset == oldCollapsibleHeight)) { 308 // Stay closed even at the new height. 309 setCollapseOffset(mCollapsibleHeight); 310 } else { 311 setCollapseOffset(Math.min(mCollapseOffset, mCollapsibleHeight)); 312 } 313 final boolean isCollapsedNew = mCollapseOffset != 0; 314 if (isCollapsedOld != isCollapsedNew) { 315 onCollapsedChanged(isCollapsedNew); 316 } 317 } else { 318 // Start out collapsed at first unless we restored state for otherwise 319 setCollapseOffset(mOpenOnLayout ? 0 : mCollapsibleHeight); 320 } 321 return true; 322 } 323 setCollapseOffset(float collapseOffset)324 private void setCollapseOffset(float collapseOffset) { 325 if (mCollapseOffset != collapseOffset) { 326 mCollapseOffset = collapseOffset; 327 requestLayout(); 328 } 329 } 330 getMaxCollapsedHeight()331 private int getMaxCollapsedHeight() { 332 return (isSmallCollapsed() ? mMaxCollapsedHeightSmall : mMaxCollapsedHeight) 333 + mCollapsibleHeightReserved; 334 } 335 setOnDismissedListener(OnDismissedListener listener)336 public void setOnDismissedListener(OnDismissedListener listener) { 337 mOnDismissedListener = listener; 338 } 339 isDismissable()340 private boolean isDismissable() { 341 return mOnDismissedListener != null && !mDismissLocked; 342 } 343 setOnCollapsedChangedListener(OnCollapsedChangedListener listener)344 public void setOnCollapsedChangedListener(OnCollapsedChangedListener listener) { 345 mOnCollapsedChangedListener = listener; 346 } 347 348 @Override onInterceptTouchEvent(MotionEvent ev)349 public boolean onInterceptTouchEvent(MotionEvent ev) { 350 final int action = ev.getActionMasked(); 351 352 if (action == MotionEvent.ACTION_DOWN) { 353 mVelocityTracker.clear(); 354 } 355 356 mVelocityTracker.addMovement(ev); 357 358 switch (action) { 359 case MotionEvent.ACTION_DOWN: { 360 final float x = ev.getX(); 361 final float y = ev.getY(); 362 mInitialTouchX = x; 363 mInitialTouchY = mLastTouchY = y; 364 mOpenOnClick = isListChildUnderClipped(x, y) && mCollapseOffset > 0; 365 } 366 break; 367 368 case MotionEvent.ACTION_MOVE: { 369 final float x = ev.getX(); 370 final float y = ev.getY(); 371 final float dy = y - mInitialTouchY; 372 if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null && 373 (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) { 374 mActivePointerId = ev.getPointerId(0); 375 mIsDragging = true; 376 mLastTouchY = Math.max(mLastTouchY - mTouchSlop, 377 Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop)); 378 } 379 } 380 break; 381 382 case MotionEvent.ACTION_POINTER_UP: { 383 onSecondaryPointerUp(ev); 384 } 385 break; 386 387 case MotionEvent.ACTION_CANCEL: 388 case MotionEvent.ACTION_UP: { 389 resetTouch(); 390 } 391 break; 392 } 393 394 if (mIsDragging) { 395 abortAnimation(); 396 } 397 return mIsDragging || mOpenOnClick; 398 } 399 isNestedListChildScrolled()400 private boolean isNestedListChildScrolled() { 401 return mNestedListChild != null 402 && mNestedListChild.getChildCount() > 0 403 && (mNestedListChild.getFirstVisiblePosition() > 0 404 || mNestedListChild.getChildAt(0).getTop() < 0); 405 } 406 isNestedRecyclerChildScrolled()407 private boolean isNestedRecyclerChildScrolled() { 408 if (mNestedRecyclerChild != null && mNestedRecyclerChild.getChildCount() > 0) { 409 final RecyclerView.ViewHolder vh = 410 mNestedRecyclerChild.findViewHolderForAdapterPosition(0); 411 return vh == null || vh.itemView.getTop() < 0; 412 } 413 return false; 414 } 415 416 @Override onTouchEvent(MotionEvent ev)417 public boolean onTouchEvent(MotionEvent ev) { 418 final int action = ev.getActionMasked(); 419 420 mVelocityTracker.addMovement(ev); 421 422 boolean handled = false; 423 switch (action) { 424 case MotionEvent.ACTION_DOWN: { 425 final float x = ev.getX(); 426 final float y = ev.getY(); 427 mInitialTouchX = x; 428 mInitialTouchY = mLastTouchY = y; 429 mActivePointerId = ev.getPointerId(0); 430 final boolean hitView = findChildUnder(mInitialTouchX, mInitialTouchY) != null; 431 handled = isDismissable() || mCollapsibleHeight > 0; 432 mIsDragging = hitView && handled; 433 abortAnimation(); 434 } 435 break; 436 437 case MotionEvent.ACTION_MOVE: { 438 int index = ev.findPointerIndex(mActivePointerId); 439 if (index < 0) { 440 Log.e(TAG, "Bad pointer id " + mActivePointerId + ", resetting"); 441 index = 0; 442 mActivePointerId = ev.getPointerId(0); 443 mInitialTouchX = ev.getX(); 444 mInitialTouchY = mLastTouchY = ev.getY(); 445 } 446 final float x = ev.getX(index); 447 final float y = ev.getY(index); 448 if (!mIsDragging) { 449 final float dy = y - mInitialTouchY; 450 if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null) { 451 handled = mIsDragging = true; 452 mLastTouchY = Math.max(mLastTouchY - mTouchSlop, 453 Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop)); 454 } 455 } 456 if (mIsDragging) { 457 final float dy = y - mLastTouchY; 458 if (dy > 0 && isNestedListChildScrolled()) { 459 mNestedListChild.smoothScrollBy((int) -dy, 0); 460 } else if (dy > 0 && isNestedRecyclerChildScrolled()) { 461 mNestedRecyclerChild.scrollBy(0, (int) -dy); 462 } else { 463 performDrag(dy); 464 } 465 } 466 mLastTouchY = y; 467 } 468 break; 469 470 case MotionEvent.ACTION_POINTER_DOWN: { 471 final int pointerIndex = ev.getActionIndex(); 472 mActivePointerId = ev.getPointerId(pointerIndex); 473 mInitialTouchX = ev.getX(pointerIndex); 474 mInitialTouchY = mLastTouchY = ev.getY(pointerIndex); 475 } 476 break; 477 478 case MotionEvent.ACTION_POINTER_UP: { 479 onSecondaryPointerUp(ev); 480 } 481 break; 482 483 case MotionEvent.ACTION_UP: { 484 final boolean wasDragging = mIsDragging; 485 mIsDragging = false; 486 if (!wasDragging && findChildUnder(mInitialTouchX, mInitialTouchY) == null && 487 findChildUnder(ev.getX(), ev.getY()) == null) { 488 if (isDismissable()) { 489 dispatchOnDismissed(); 490 resetTouch(); 491 return true; 492 } 493 } 494 if (mOpenOnClick && Math.abs(ev.getX() - mInitialTouchX) < mTouchSlop && 495 Math.abs(ev.getY() - mInitialTouchY) < mTouchSlop) { 496 smoothScrollTo(0, 0); 497 return true; 498 } 499 mVelocityTracker.computeCurrentVelocity(1000); 500 final float yvel = mVelocityTracker.getYVelocity(mActivePointerId); 501 if (Math.abs(yvel) > mMinFlingVelocity) { 502 if (getShowAtTop()) { 503 if (isDismissable() && yvel < 0) { 504 abortAnimation(); 505 dismiss(); 506 } else { 507 smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel); 508 } 509 } else { 510 if (isDismissable() 511 && yvel > 0 && mCollapseOffset > mCollapsibleHeight) { 512 smoothScrollTo(mHeightUsed, yvel); 513 mDismissOnScrollerFinished = true; 514 } else { 515 scrollNestedScrollableChildBackToTop(); 516 smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel); 517 } 518 } 519 }else { 520 smoothScrollTo( 521 mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0); 522 } 523 resetTouch(); 524 } 525 break; 526 527 case MotionEvent.ACTION_CANCEL: { 528 if (mIsDragging) { 529 smoothScrollTo( 530 mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0); 531 } 532 resetTouch(); 533 return true; 534 } 535 } 536 537 return handled; 538 } 539 540 /** 541 * Scroll nested scrollable child back to top if it has been scrolled. 542 */ 543 public void scrollNestedScrollableChildBackToTop() { 544 if (isNestedListChildScrolled()) { 545 mNestedListChild.smoothScrollToPosition(0); 546 } else if (isNestedRecyclerChildScrolled()) { 547 mNestedRecyclerChild.smoothScrollToPosition(0); 548 } 549 } 550 551 private void onSecondaryPointerUp(MotionEvent ev) { 552 final int pointerIndex = ev.getActionIndex(); 553 final int pointerId = ev.getPointerId(pointerIndex); 554 if (pointerId == mActivePointerId) { 555 // This was our active pointer going up. Choose a new 556 // active pointer and adjust accordingly. 557 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 558 mInitialTouchX = ev.getX(newPointerIndex); 559 mInitialTouchY = mLastTouchY = ev.getY(newPointerIndex); 560 mActivePointerId = ev.getPointerId(newPointerIndex); 561 } 562 } 563 564 private void resetTouch() { 565 mActivePointerId = MotionEvent.INVALID_POINTER_ID; 566 mIsDragging = false; 567 mOpenOnClick = false; 568 mInitialTouchX = mInitialTouchY = mLastTouchY = 0; 569 mVelocityTracker.clear(); 570 } 571 572 private void dismiss() { 573 mRunOnDismissedListener = new RunOnDismissedListener(); 574 post(mRunOnDismissedListener); 575 } 576 577 @Override 578 public void computeScroll() { 579 super.computeScroll(); 580 if (mScroller.computeScrollOffset()) { 581 final boolean keepGoing = !mScroller.isFinished(); 582 performDrag(mScroller.getCurrY() - mCollapseOffset); 583 if (keepGoing) { 584 postInvalidateOnAnimation(); 585 } else if (mDismissOnScrollerFinished && mOnDismissedListener != null) { 586 dismiss(); 587 } 588 } 589 } 590 591 private void abortAnimation() { 592 mScroller.abortAnimation(); 593 mRunOnDismissedListener = null; 594 mDismissOnScrollerFinished = false; 595 } 596 597 private float performDrag(float dy) { 598 if (getShowAtTop()) { 599 return 0; 600 } 601 602 final float newPos = Math.max(0, Math.min(mCollapseOffset + dy, mHeightUsed)); 603 if (newPos != mCollapseOffset) { 604 dy = newPos - mCollapseOffset; 605 606 mDragRemainder += dy - (int) dy; 607 if (mDragRemainder >= 1.0f) { 608 mDragRemainder -= 1.0f; 609 dy += 1.0f; 610 } else if (mDragRemainder <= -1.0f) { 611 mDragRemainder += 1.0f; 612 dy -= 1.0f; 613 } 614 615 boolean isIgnoreOffsetLimitSet = false; 616 int ignoreOffsetLimit = 0; 617 View ignoreOffsetLimitView = findIgnoreOffsetLimitView(); 618 if (ignoreOffsetLimitView != null) { 619 LayoutParams lp = (LayoutParams) ignoreOffsetLimitView.getLayoutParams(); 620 ignoreOffsetLimit = ignoreOffsetLimitView.getBottom() + lp.bottomMargin; 621 isIgnoreOffsetLimitSet = true; 622 } 623 final int childCount = getChildCount(); 624 for (int i = 0; i < childCount; i++) { 625 final View child = getChildAt(i); 626 if (child.getVisibility() == View.GONE) { 627 continue; 628 } 629 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 630 if (!lp.ignoreOffset) { 631 child.offsetTopAndBottom((int) dy); 632 } else if (isIgnoreOffsetLimitSet) { 633 int top = child.getTop(); 634 int targetTop = Math.max( 635 (int) (ignoreOffsetLimit + lp.topMargin + dy), 636 lp.mFixedTop); 637 if (top != targetTop) { 638 child.offsetTopAndBottom(targetTop - top); 639 } 640 ignoreOffsetLimit = child.getBottom() + lp.bottomMargin; 641 } 642 } 643 final boolean isCollapsedOld = mCollapseOffset != 0; 644 mCollapseOffset = newPos; 645 mTopOffset += dy; 646 final boolean isCollapsedNew = newPos != 0; 647 if (isCollapsedOld != isCollapsedNew) { 648 onCollapsedChanged(isCollapsedNew); 649 getMetricsLogger().write( 650 new LogMaker(MetricsEvent.ACTION_SHARESHEET_COLLAPSED_CHANGED) 651 .setSubtype(isCollapsedNew ? 1 : 0)); 652 } 653 onScrollChanged(0, (int) newPos, 0, (int) (newPos - dy)); 654 postInvalidateOnAnimation(); 655 return dy; 656 } 657 return 0; 658 } 659 660 private void onCollapsedChanged(boolean isCollapsed) { 661 notifyViewAccessibilityStateChangedIfNeeded( 662 AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); 663 664 if (mScrollIndicatorDrawable != null) { 665 setWillNotDraw(!isCollapsed); 666 } 667 668 if (mOnCollapsedChangedListener != null) { 669 mOnCollapsedChangedListener.onCollapsedChanged(isCollapsed); 670 } 671 } 672 673 void dispatchOnDismissed() { 674 if (mOnDismissedListener != null) { 675 mOnDismissedListener.onDismissed(); 676 } 677 if (mRunOnDismissedListener != null) { 678 removeCallbacks(mRunOnDismissedListener); 679 mRunOnDismissedListener = null; 680 } 681 } 682 683 private void smoothScrollTo(int yOffset, float velocity) { 684 abortAnimation(); 685 final int sy = (int) mCollapseOffset; 686 int dy = yOffset - sy; 687 if (dy == 0) { 688 return; 689 } 690 691 final int height = getHeight(); 692 final int halfHeight = height / 2; 693 final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / height); 694 final float distance = halfHeight + halfHeight * 695 distanceInfluenceForSnapDuration(distanceRatio); 696 697 int duration = 0; 698 velocity = Math.abs(velocity); 699 if (velocity > 0) { 700 duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 701 } else { 702 final float pageDelta = (float) Math.abs(dy) / height; 703 duration = (int) ((pageDelta + 1) * 100); 704 } 705 duration = Math.min(duration, 300); 706 707 mScroller.startScroll(0, sy, 0, dy, duration); 708 postInvalidateOnAnimation(); 709 } 710 711 private float distanceInfluenceForSnapDuration(float f) { 712 f -= 0.5f; // center the values about 0. 713 f *= 0.3f * Math.PI / 2.0f; 714 return (float) Math.sin(f); 715 } 716 717 /** 718 * Note: this method doesn't take Z into account for overlapping views 719 * since it is only used in contexts where this doesn't affect the outcome. 720 */ 721 private View findChildUnder(float x, float y) { 722 return findChildUnder(this, x, y); 723 } 724 725 private static View findChildUnder(ViewGroup parent, float x, float y) { 726 final int childCount = parent.getChildCount(); 727 for (int i = childCount - 1; i >= 0; i--) { 728 final View child = parent.getChildAt(i); 729 if (isChildUnder(child, x, y)) { 730 return child; 731 } 732 } 733 return null; 734 } 735 736 private View findListChildUnder(float x, float y) { 737 View v = findChildUnder(x, y); 738 while (v != null) { 739 x -= v.getX(); 740 y -= v.getY(); 741 if (v instanceof AbsListView) { 742 // One more after this. 743 return findChildUnder((ViewGroup) v, x, y); 744 } 745 v = v instanceof ViewGroup ? findChildUnder((ViewGroup) v, x, y) : null; 746 } 747 return v; 748 } 749 750 /** 751 * This only checks clipping along the bottom edge. 752 */ 753 private boolean isListChildUnderClipped(float x, float y) { 754 final View listChild = findListChildUnder(x, y); 755 return listChild != null && isDescendantClipped(listChild); 756 } 757 758 private boolean isDescendantClipped(View child) { 759 mTempRect.set(0, 0, child.getWidth(), child.getHeight()); 760 offsetDescendantRectToMyCoords(child, mTempRect); 761 View directChild; 762 if (child.getParent() == this) { 763 directChild = child; 764 } else { 765 View v = child; 766 ViewParent p = child.getParent(); 767 while (p != this) { 768 v = (View) p; 769 p = v.getParent(); 770 } 771 directChild = v; 772 } 773 774 // ResolverDrawerLayout lays out vertically in child order; 775 // the next view and forward is what to check against. 776 int clipEdge = getHeight() - getPaddingBottom(); 777 final int childCount = getChildCount(); 778 for (int i = indexOfChild(directChild) + 1; i < childCount; i++) { 779 final View nextChild = getChildAt(i); 780 if (nextChild.getVisibility() == GONE) { 781 continue; 782 } 783 clipEdge = Math.min(clipEdge, nextChild.getTop()); 784 } 785 return mTempRect.bottom > clipEdge; 786 } 787 788 private static boolean isChildUnder(View child, float x, float y) { 789 final float left = child.getX(); 790 final float top = child.getY(); 791 final float right = left + child.getWidth(); 792 final float bottom = top + child.getHeight(); 793 return x >= left && y >= top && x < right && y < bottom; 794 } 795 796 @Override 797 public void requestChildFocus(View child, View focused) { 798 super.requestChildFocus(child, focused); 799 if (!isInTouchMode() && isDescendantClipped(focused)) { 800 smoothScrollTo(0, 0); 801 } 802 } 803 804 @Override 805 protected void onAttachedToWindow() { 806 super.onAttachedToWindow(); 807 getViewTreeObserver().addOnTouchModeChangeListener(mTouchModeChangeListener); 808 } 809 810 @Override 811 protected void onDetachedFromWindow() { 812 super.onDetachedFromWindow(); 813 getViewTreeObserver().removeOnTouchModeChangeListener(mTouchModeChangeListener); 814 abortAnimation(); 815 } 816 817 @Override 818 public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { 819 if ((nestedScrollAxes & View.SCROLL_AXIS_VERTICAL) != 0) { 820 if (target instanceof AbsListView) { 821 mNestedListChild = (AbsListView) target; 822 } 823 if (target instanceof RecyclerView) { 824 mNestedRecyclerChild = (RecyclerView) target; 825 } 826 return true; 827 } 828 return false; 829 } 830 831 @Override 832 public void onNestedScrollAccepted(View child, View target, int axes) { 833 super.onNestedScrollAccepted(child, target, axes); 834 } 835 836 @Override 837 public void onStopNestedScroll(View child) { 838 super.onStopNestedScroll(child); 839 if (mScroller.isFinished()) { 840 smoothScrollTo(mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0); 841 } 842 } 843 844 @Override 845 public void onNestedScroll(View target, int dxConsumed, int dyConsumed, 846 int dxUnconsumed, int dyUnconsumed) { 847 if (dyUnconsumed < 0) { 848 performDrag(-dyUnconsumed); 849 } 850 } 851 852 @Override 853 public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { 854 if (dy > 0) { 855 consumed[1] = (int) -performDrag(-dy); 856 } 857 } 858 859 @Override 860 public boolean onNestedPreFling(View target, float velocityX, float velocityY) { 861 if (mFlingLogicDelegate != null) { 862 return mFlingLogicDelegate.onNestedPreFling(this, target, velocityX, velocityY); 863 } 864 if (!getShowAtTop() && velocityY > mMinFlingVelocity && mCollapseOffset != 0) { 865 smoothScrollTo(0, velocityY); 866 return true; 867 } 868 return false; 869 } 870 871 @Override 872 public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { 873 if (mFlingLogicDelegate != null) { 874 return mFlingLogicDelegate.onNestedFling(this, target, velocityX, velocityY, consumed); 875 } 876 // TODO: find a more suitable way to fix it. 877 // RecyclerView started reporting `consumed` as true whenever a scrolling is enabled, 878 // previously the value was based on whether the fling can be performed in given direction 879 // i.e. whether it is at the top or at the bottom. isRecyclerViewAtTheTop method is a 880 // workaround that restores the legacy functionality. 881 boolean shouldConsume = (Math.abs(velocityY) > mMinFlingVelocity) 882 && (!consumed || (velocityY < 0 && isRecyclerViewAtTheTop(target))); 883 if (shouldConsume) { 884 if (getShowAtTop()) { 885 if (isDismissable() && velocityY > 0) { 886 abortAnimation(); 887 dismiss(); 888 } else { 889 smoothScrollTo(velocityY < 0 ? mCollapsibleHeight : 0, velocityY); 890 } 891 } else { 892 if (isDismissable() 893 && velocityY < 0 && mCollapseOffset > mCollapsibleHeight) { 894 smoothScrollTo(mHeightUsed, velocityY); 895 mDismissOnScrollerFinished = true; 896 } else { 897 smoothScrollTo(velocityY > 0 ? 0 : mCollapsibleHeight, velocityY); 898 } 899 } 900 return true; 901 } 902 return false; 903 } 904 905 private static boolean isRecyclerViewAtTheTop(View target) { 906 // TODO: there's a very similar functionality in #isNestedRecyclerChildScrolled(), 907 // consolidate the two. 908 if (!(target instanceof RecyclerView)) { 909 return false; 910 } 911 RecyclerView recyclerView = (RecyclerView) target; 912 if (recyclerView.getChildCount() == 0) { 913 return true; 914 } 915 View firstChild = recyclerView.getChildAt(0); 916 return recyclerView.getChildAdapterPosition(firstChild) == 0 917 && firstChild.getTop() >= recyclerView.getPaddingTop(); 918 } 919 920 private static boolean isFlingTargetAtTop(View target) { 921 if (target instanceof ScrollingView) { 922 return !target.canScrollVertically(-1); 923 } 924 return false; 925 } 926 927 private boolean performAccessibilityActionCommon(int action) { 928 switch (action) { 929 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: 930 case AccessibilityNodeInfo.ACTION_EXPAND: 931 case com.android.internal.R.id.accessibilityActionScrollDown: 932 if (mCollapseOffset != 0) { 933 smoothScrollTo(0, 0); 934 return true; 935 } 936 break; 937 case AccessibilityNodeInfo.ACTION_COLLAPSE: 938 if (mCollapseOffset < mCollapsibleHeight) { 939 smoothScrollTo(mCollapsibleHeight, 0); 940 return true; 941 } 942 break; 943 case AccessibilityNodeInfo.ACTION_DISMISS: 944 if ((mCollapseOffset < mHeightUsed) && isDismissable()) { 945 smoothScrollTo(mHeightUsed, 0); 946 mDismissOnScrollerFinished = true; 947 return true; 948 } 949 break; 950 } 951 952 return false; 953 } 954 955 @Override 956 public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle args) { 957 if (super.onNestedPrePerformAccessibilityAction(target, action, args)) { 958 return true; 959 } 960 961 return performAccessibilityActionCommon(action); 962 } 963 964 @Override 965 public CharSequence getAccessibilityClassName() { 966 // Since we support scrolling, make this ViewGroup look like a 967 // ScrollView. This is kind of a hack until we have support for 968 // specifying auto-scroll behavior. 969 return android.widget.ScrollView.class.getName(); 970 } 971 972 @Override 973 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { 974 super.onInitializeAccessibilityNodeInfoInternal(info); 975 976 if (isEnabled()) { 977 if (mCollapseOffset != 0) { 978 info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD); 979 info.addAction(AccessibilityAction.ACTION_EXPAND); 980 info.addAction(AccessibilityAction.ACTION_SCROLL_DOWN); 981 info.setScrollable(true); 982 } 983 if ((mCollapseOffset < mHeightUsed) 984 && ((mCollapseOffset < mCollapsibleHeight) || isDismissable())) { 985 info.addAction(AccessibilityAction.ACTION_SCROLL_UP); 986 info.setScrollable(true); 987 } 988 if (mCollapseOffset < mCollapsibleHeight) { 989 info.addAction(AccessibilityAction.ACTION_COLLAPSE); 990 } 991 if (mCollapseOffset < mHeightUsed && isDismissable()) { 992 info.addAction(AccessibilityAction.ACTION_DISMISS); 993 } 994 } 995 996 // This view should never get accessibility focus, but it's interactive 997 // via nested scrolling, so we can't hide it completely. 998 info.removeAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS); 999 } 1000 1001 @Override 1002 public boolean performAccessibilityActionInternal(int action, Bundle arguments) { 1003 if (action == AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS.getId()) { 1004 // This view should never get accessibility focus. 1005 return false; 1006 } 1007 1008 if (super.performAccessibilityActionInternal(action, arguments)) { 1009 return true; 1010 } 1011 1012 return performAccessibilityActionCommon(action); 1013 } 1014 1015 @Override 1016 public void onDrawForeground(@NonNull Canvas canvas) { 1017 if (mScrollIndicatorDrawable != null) { 1018 mScrollIndicatorDrawable.draw(canvas); 1019 } 1020 1021 super.onDrawForeground(canvas); 1022 } 1023 1024 @Override 1025 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1026 final int sourceWidth = MeasureSpec.getSize(widthMeasureSpec); 1027 int widthSize = sourceWidth; 1028 final int heightSize = MeasureSpec.getSize(heightMeasureSpec); 1029 1030 // Single-use layout; just ignore the mode and use available space. 1031 // Clamp to maxWidth. 1032 if (mMaxWidth >= 0) { 1033 widthSize = Math.min(widthSize, mMaxWidth + getPaddingLeft() + getPaddingRight()); 1034 } 1035 1036 final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); 1037 final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY); 1038 1039 // Currently we allot more height than is really needed so that the entirety of the 1040 // sheet may be pulled up. 1041 // TODO: Restrict the height here to be the right value. 1042 int heightUsed = 0; 1043 1044 // Measure always-show children first. 1045 final int childCount = getChildCount(); 1046 for (int i = 0; i < childCount; i++) { 1047 final View child = getChildAt(i); 1048 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1049 if (lp.alwaysShow && child.getVisibility() != GONE) { 1050 if (lp.maxHeight != -1) { 1051 final int remainingHeight = heightSize - heightUsed; 1052 measureChildWithMargins(child, widthSpec, 0, 1053 MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST), 1054 lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0); 1055 } else { 1056 measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed); 1057 } 1058 heightUsed += child.getMeasuredHeight(); 1059 } 1060 } 1061 1062 mAlwaysShowHeight = heightUsed; 1063 1064 // And now the rest. 1065 for (int i = 0; i < childCount; i++) { 1066 final View child = getChildAt(i); 1067 1068 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1069 if (!lp.alwaysShow && child.getVisibility() != GONE) { 1070 if (lp.maxHeight != -1) { 1071 final int remainingHeight = heightSize - heightUsed; 1072 measureChildWithMargins(child, widthSpec, 0, 1073 MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST), 1074 lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0); 1075 } else { 1076 measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed); 1077 } 1078 heightUsed += child.getMeasuredHeight(); 1079 } 1080 } 1081 1082 mHeightUsed = heightUsed; 1083 int oldCollapsibleHeight = updateCollapsibleHeight(); 1084 updateCollapseOffset(oldCollapsibleHeight, !isDragging()); 1085 1086 if (getShowAtTop()) { 1087 mTopOffset = 0; 1088 } else { 1089 mTopOffset = Math.max(0, heightSize - mHeightUsed) + (int) mCollapseOffset; 1090 } 1091 1092 setMeasuredDimension(sourceWidth, heightSize); 1093 } 1094 1095 private int updateCollapsibleHeight() { 1096 final int oldCollapsibleHeight = mCollapsibleHeight; 1097 mCollapsibleHeight = Math.max(0, mHeightUsed - mAlwaysShowHeight - getMaxCollapsedHeight()); 1098 return oldCollapsibleHeight; 1099 } 1100 1101 /** 1102 * @return The space reserved by views with 'alwaysShow=true' 1103 */ 1104 public int getAlwaysShowHeight() { 1105 return mAlwaysShowHeight; 1106 } 1107 1108 @Override 1109 protected void onLayout(boolean changed, int l, int t, int r, int b) { 1110 final int width = getWidth(); 1111 1112 View indicatorHost = null; 1113 1114 int ypos = mTopOffset; 1115 final int leftEdge = getPaddingLeft(); 1116 final int rightEdge = width - getPaddingRight(); 1117 final int widthAvailable = rightEdge - leftEdge; 1118 1119 boolean isIgnoreOffsetLimitSet = false; 1120 int ignoreOffsetLimit = 0; 1121 final int childCount = getChildCount(); 1122 for (int i = 0; i < childCount; i++) { 1123 final View child = getChildAt(i); 1124 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1125 if (lp.hasNestedScrollIndicator) { 1126 indicatorHost = child; 1127 } 1128 1129 if (child.getVisibility() == GONE) { 1130 continue; 1131 } 1132 1133 if (mIgnoreOffsetTopLimitViewId != ID_NULL && !isIgnoreOffsetLimitSet) { 1134 if (mIgnoreOffsetTopLimitViewId == child.getId()) { 1135 ignoreOffsetLimit = child.getBottom() + lp.bottomMargin; 1136 isIgnoreOffsetLimitSet = true; 1137 } 1138 } 1139 1140 int top = ypos + lp.topMargin; 1141 if (lp.ignoreOffset) { 1142 if (!isDragging()) { 1143 lp.mFixedTop = (int) (top - mCollapseOffset); 1144 } 1145 if (isIgnoreOffsetLimitSet) { 1146 top = Math.max(ignoreOffsetLimit + lp.topMargin, (int) (top - mCollapseOffset)); 1147 ignoreOffsetLimit = top + child.getMeasuredHeight() + lp.bottomMargin; 1148 } else { 1149 top -= mCollapseOffset; 1150 } 1151 } 1152 final int bottom = top + child.getMeasuredHeight(); 1153 1154 final int childWidth = child.getMeasuredWidth(); 1155 final int left = leftEdge + (widthAvailable - childWidth) / 2; 1156 final int right = left + childWidth; 1157 1158 child.layout(left, top, right, bottom); 1159 1160 ypos = bottom + lp.bottomMargin; 1161 } 1162 1163 if (mScrollIndicatorDrawable != null) { 1164 if (indicatorHost != null) { 1165 final int left = indicatorHost.getLeft(); 1166 final int right = indicatorHost.getRight(); 1167 final int bottom = indicatorHost.getTop(); 1168 final int top = bottom - mScrollIndicatorDrawable.getIntrinsicHeight(); 1169 mScrollIndicatorDrawable.setBounds(left, top, right, bottom); 1170 setWillNotDraw(!isCollapsed()); 1171 } else { 1172 mScrollIndicatorDrawable = null; 1173 setWillNotDraw(true); 1174 } 1175 } 1176 } 1177 1178 @Override 1179 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 1180 return new LayoutParams(getContext(), attrs); 1181 } 1182 1183 @Override 1184 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 1185 if (p instanceof LayoutParams) { 1186 return new LayoutParams((LayoutParams) p); 1187 } else if (p instanceof MarginLayoutParams) { 1188 return new LayoutParams((MarginLayoutParams) p); 1189 } 1190 return new LayoutParams(p); 1191 } 1192 1193 @Override 1194 protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 1195 return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); 1196 } 1197 1198 @Override 1199 protected Parcelable onSaveInstanceState() { 1200 final SavedState ss = new SavedState(super.onSaveInstanceState()); 1201 ss.open = mCollapsibleHeight > 0 && mCollapseOffset == 0; 1202 ss.mCollapsibleHeightReserved = mCollapsibleHeightReserved; 1203 return ss; 1204 } 1205 1206 @Override 1207 protected void onRestoreInstanceState(Parcelable state) { 1208 final SavedState ss = (SavedState) state; 1209 super.onRestoreInstanceState(ss.getSuperState()); 1210 mOpenOnLayout = ss.open; 1211 mCollapsibleHeightReserved = ss.mCollapsibleHeightReserved; 1212 } 1213 1214 private View findIgnoreOffsetLimitView() { 1215 if (mIgnoreOffsetTopLimitViewId == ID_NULL) { 1216 return null; 1217 } 1218 View v = findViewById(mIgnoreOffsetTopLimitViewId); 1219 if (v != null && v != this && v.getParent() == this && v.getVisibility() != View.GONE) { 1220 return v; 1221 } 1222 return null; 1223 } 1224 1225 public static class LayoutParams extends MarginLayoutParams { 1226 public boolean alwaysShow; 1227 public boolean ignoreOffset; 1228 public boolean hasNestedScrollIndicator; 1229 public int maxHeight; 1230 int mFixedTop; 1231 1232 public LayoutParams(Context c, AttributeSet attrs) { 1233 super(c, attrs); 1234 1235 final TypedArray a = c.obtainStyledAttributes(attrs, 1236 R.styleable.ResolverDrawerLayout_LayoutParams); 1237 alwaysShow = a.getBoolean( 1238 R.styleable.ResolverDrawerLayout_LayoutParams_layout_alwaysShow, 1239 false); 1240 ignoreOffset = a.getBoolean( 1241 R.styleable.ResolverDrawerLayout_LayoutParams_layout_ignoreOffset, 1242 false); 1243 hasNestedScrollIndicator = a.getBoolean( 1244 R.styleable.ResolverDrawerLayout_LayoutParams_layout_hasNestedScrollIndicator, 1245 false); 1246 maxHeight = a.getDimensionPixelSize( 1247 R.styleable.ResolverDrawerLayout_LayoutParams_layout_maxHeight, -1); 1248 a.recycle(); 1249 } 1250 1251 public LayoutParams(int width, int height) { 1252 super(width, height); 1253 } 1254 1255 public LayoutParams(LayoutParams source) { 1256 super(source); 1257 this.alwaysShow = source.alwaysShow; 1258 this.ignoreOffset = source.ignoreOffset; 1259 this.hasNestedScrollIndicator = source.hasNestedScrollIndicator; 1260 this.maxHeight = source.maxHeight; 1261 } 1262 1263 public LayoutParams(MarginLayoutParams source) { 1264 super(source); 1265 } 1266 1267 public LayoutParams(ViewGroup.LayoutParams source) { 1268 super(source); 1269 } 1270 } 1271 1272 static class SavedState extends BaseSavedState { 1273 boolean open; 1274 private int mCollapsibleHeightReserved; 1275 1276 SavedState(Parcelable superState) { 1277 super(superState); 1278 } 1279 1280 private SavedState(Parcel in) { 1281 super(in); 1282 open = in.readInt() != 0; 1283 mCollapsibleHeightReserved = in.readInt(); 1284 } 1285 1286 @Override 1287 public void writeToParcel(Parcel out, int flags) { 1288 super.writeToParcel(out, flags); 1289 out.writeInt(open ? 1 : 0); 1290 out.writeInt(mCollapsibleHeightReserved); 1291 } 1292 1293 public static final Parcelable.Creator<SavedState> CREATOR = 1294 new Parcelable.Creator<SavedState>() { 1295 @Override 1296 public SavedState createFromParcel(Parcel in) { 1297 return new SavedState(in); 1298 } 1299 1300 @Override 1301 public SavedState[] newArray(int size) { 1302 return new SavedState[size]; 1303 } 1304 }; 1305 } 1306 1307 /** 1308 * Listener for sheet dismissed events. 1309 */ 1310 public interface OnDismissedListener { 1311 /** 1312 * Callback when the sheet is dismissed by the user. 1313 */ 1314 void onDismissed(); 1315 } 1316 1317 /** 1318 * Listener for sheet collapsed / expanded events. 1319 */ 1320 public interface OnCollapsedChangedListener { 1321 /** 1322 * Callback when the sheet is either fully expanded or collapsed. 1323 * @param isCollapsed true when collapsed, false when expanded. 1324 */ 1325 void onCollapsedChanged(boolean isCollapsed); 1326 } 1327 1328 private class RunOnDismissedListener implements Runnable { 1329 @Override 1330 public void run() { 1331 dispatchOnDismissed(); 1332 } 1333 } 1334 1335 private MetricsLogger getMetricsLogger() { 1336 if (mMetricsLogger == null) { 1337 mMetricsLogger = new MetricsLogger(); 1338 } 1339 return mMetricsLogger; 1340 } 1341 1342 /** 1343 * Controlled by 1344 * {@link com.android.intentresolver.Flags#FLAG_SCROLLABLE_PREVIEW} 1345 */ 1346 private interface ScrollablePreviewFlingLogicDelegate { 1347 default boolean onNestedPreFling( 1348 ResolverDrawerLayout drawer, View target, float velocityX, float velocityY) { 1349 boolean shouldScroll = !drawer.getShowAtTop() && velocityY > drawer.mMinFlingVelocity 1350 && drawer.mCollapseOffset != 0; 1351 if (shouldScroll) { 1352 drawer.smoothScrollTo(0, velocityY); 1353 return true; 1354 } 1355 boolean shouldDismiss = (Math.abs(velocityY) > drawer.mMinFlingVelocity) 1356 && velocityY < 0 1357 && isFlingTargetAtTop(target); 1358 if (shouldDismiss) { 1359 if (drawer.getShowAtTop()) { 1360 drawer.smoothScrollTo(drawer.mCollapsibleHeight, velocityY); 1361 } else { 1362 if (drawer.isDismissable() 1363 && drawer.mCollapseOffset > drawer.mCollapsibleHeight) { 1364 drawer.smoothScrollTo(drawer.mHeightUsed, velocityY); 1365 drawer.mDismissOnScrollerFinished = true; 1366 } else { 1367 drawer.smoothScrollTo(drawer.mCollapsibleHeight, velocityY); 1368 } 1369 } 1370 return true; 1371 } 1372 return false; 1373 } 1374 1375 default boolean onNestedFling( 1376 ResolverDrawerLayout drawer, 1377 View target, 1378 float velocityX, 1379 float velocityY, 1380 boolean consumed) { 1381 // TODO: find a more suitable way to fix it. 1382 // RecyclerView started reporting `consumed` as true whenever a scrolling is enabled, 1383 // previously the value was based on whether the fling can be performed in given 1384 // direction i.e. whether it is at the top or at the bottom. isRecyclerViewAtTheTop 1385 // method is a workaround that restores the legacy functionality. 1386 boolean shouldConsume = (Math.abs(velocityY) > drawer.mMinFlingVelocity) && !consumed; 1387 if (shouldConsume) { 1388 if (drawer.getShowAtTop()) { 1389 if (drawer.isDismissable() && velocityY > 0) { 1390 drawer.abortAnimation(); 1391 drawer.dismiss(); 1392 } else { 1393 drawer.smoothScrollTo( 1394 velocityY < 0 ? drawer.mCollapsibleHeight : 0, velocityY); 1395 } 1396 } else { 1397 if (drawer.isDismissable() 1398 && velocityY < 0 1399 && drawer.mCollapseOffset > drawer.mCollapsibleHeight) { 1400 drawer.smoothScrollTo(drawer.mHeightUsed, velocityY); 1401 drawer.mDismissOnScrollerFinished = true; 1402 } else { 1403 drawer.smoothScrollTo( 1404 velocityY > 0 ? 0 : drawer.mCollapsibleHeight, velocityY); 1405 } 1406 } 1407 } 1408 return shouldConsume; 1409 } 1410 } 1411 } 1412