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