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