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