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.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 com.android.internal.R; 49 import com.android.internal.logging.MetricsLogger; 50 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 51 52 public class ResolverDrawerLayout extends ViewGroup { 53 private static final String TAG = "ResolverDrawerLayout"; 54 private MetricsLogger mMetricsLogger; 55 56 /** 57 * Max width of the whole drawer layout 58 */ 59 private final int mMaxWidth; 60 61 /** 62 * Max total visible height of views not marked always-show when in the closed/initial state 63 */ 64 private int mMaxCollapsedHeight; 65 66 /** 67 * Max total visible height of views not marked always-show when in the closed/initial state 68 * when a default option is present 69 */ 70 private int mMaxCollapsedHeightSmall; 71 72 /** 73 * Whether {@code mMaxCollapsedHeightSmall} was set explicitly as a layout attribute or 74 * inferred by {@code mMaxCollapsedHeight}. 75 */ 76 private final boolean mIsMaxCollapsedHeightSmallExplicit; 77 78 private boolean mSmallCollapsed; 79 80 /** 81 * Move views down from the top by this much in px 82 */ 83 private float mCollapseOffset; 84 85 /** 86 * Track fractions of pixels from drag calculations. Without this, the view offsets get 87 * out of sync due to frequently dropping fractions of a pixel from '(int) dy' casts. 88 */ 89 private float mDragRemainder = 0.0f; 90 private int mCollapsibleHeight; 91 private int mUncollapsibleHeight; 92 private int mAlwaysShowHeight; 93 94 /** 95 * The height in pixels of reserved space added to the top of the collapsed UI; 96 * e.g. chooser targets 97 */ 98 private int mCollapsibleHeightReserved; 99 100 private int mTopOffset; 101 private boolean mShowAtTop; 102 @IdRes 103 private int mIgnoreOffsetTopLimitViewId = ID_NULL; 104 105 private boolean mIsDragging; 106 private boolean mOpenOnClick; 107 private boolean mOpenOnLayout; 108 private boolean mDismissOnScrollerFinished; 109 private final int mTouchSlop; 110 private final float mMinFlingVelocity; 111 private final OverScroller mScroller; 112 private final VelocityTracker mVelocityTracker; 113 114 private Drawable mScrollIndicatorDrawable; 115 116 private OnDismissedListener mOnDismissedListener; 117 private RunOnDismissedListener mRunOnDismissedListener; 118 private OnCollapsedChangedListener mOnCollapsedChangedListener; 119 120 private boolean mDismissLocked; 121 122 private float mInitialTouchX; 123 private float mInitialTouchY; 124 private float mLastTouchY; 125 private int mActivePointerId = MotionEvent.INVALID_POINTER_ID; 126 127 private final Rect mTempRect = new Rect(); 128 129 private AbsListView mNestedListChild; 130 private RecyclerView mNestedRecyclerChild; 131 132 private final ViewTreeObserver.OnTouchModeChangeListener mTouchModeChangeListener = 133 new ViewTreeObserver.OnTouchModeChangeListener() { 134 @Override 135 public void onTouchModeChanged(boolean isInTouchMode) { 136 if (!isInTouchMode && hasFocus() && isDescendantClipped(getFocusedChild())) { 137 smoothScrollTo(0, 0); 138 } 139 } 140 }; 141 ResolverDrawerLayout(Context context)142 public ResolverDrawerLayout(Context context) { 143 this(context, null); 144 } 145 ResolverDrawerLayout(Context context, AttributeSet attrs)146 public ResolverDrawerLayout(Context context, AttributeSet attrs) { 147 this(context, attrs, 0); 148 } 149 ResolverDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr)150 public ResolverDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) { 151 super(context, attrs, defStyleAttr); 152 153 final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ResolverDrawerLayout, 154 defStyleAttr, 0); 155 mMaxWidth = a.getDimensionPixelSize(R.styleable.ResolverDrawerLayout_maxWidth, -1); 156 mMaxCollapsedHeight = a.getDimensionPixelSize( 157 R.styleable.ResolverDrawerLayout_maxCollapsedHeight, 0); 158 mMaxCollapsedHeightSmall = a.getDimensionPixelSize( 159 R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall, 160 mMaxCollapsedHeight); 161 mIsMaxCollapsedHeightSmallExplicit = 162 a.hasValue(R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall); 163 mShowAtTop = a.getBoolean(R.styleable.ResolverDrawerLayout_showAtTop, false); 164 if (a.hasValue(R.styleable.ResolverDrawerLayout_ignoreOffsetTopLimit)) { 165 mIgnoreOffsetTopLimitViewId = a.getResourceId( 166 R.styleable.ResolverDrawerLayout_ignoreOffsetTopLimit, ID_NULL); 167 } 168 a.recycle(); 169 170 mScrollIndicatorDrawable = mContext.getDrawable(R.drawable.scroll_indicator_material); 171 172 mScroller = new OverScroller(context, AnimationUtils.loadInterpolator(context, 173 android.R.interpolator.decelerate_quint)); 174 mVelocityTracker = VelocityTracker.obtain(); 175 176 final ViewConfiguration vc = ViewConfiguration.get(context); 177 mTouchSlop = vc.getScaledTouchSlop(); 178 mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); 179 180 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); 181 } 182 183 /** 184 * Dynamically set the max collapsed height. Note this also updates the small collapsed 185 * height if it wasn't specified explicitly. 186 */ setMaxCollapsedHeight(int heightInPixels)187 public void setMaxCollapsedHeight(int heightInPixels) { 188 if (heightInPixels == mMaxCollapsedHeight) { 189 return; 190 } 191 mMaxCollapsedHeight = heightInPixels; 192 if (!mIsMaxCollapsedHeightSmallExplicit) { 193 mMaxCollapsedHeightSmall = mMaxCollapsedHeight; 194 } 195 requestLayout(); 196 } 197 setSmallCollapsed(boolean smallCollapsed)198 public void setSmallCollapsed(boolean smallCollapsed) { 199 if (mSmallCollapsed != smallCollapsed) { 200 mSmallCollapsed = smallCollapsed; 201 requestLayout(); 202 } 203 } 204 isSmallCollapsed()205 public boolean isSmallCollapsed() { 206 return mSmallCollapsed; 207 } 208 isCollapsed()209 public boolean isCollapsed() { 210 return mCollapseOffset > 0; 211 } 212 setShowAtTop(boolean showOnTop)213 public void setShowAtTop(boolean showOnTop) { 214 if (mShowAtTop != showOnTop) { 215 mShowAtTop = showOnTop; 216 requestLayout(); 217 } 218 } 219 getShowAtTop()220 public boolean getShowAtTop() { 221 return mShowAtTop; 222 } 223 setCollapsed(boolean collapsed)224 public void setCollapsed(boolean collapsed) { 225 if (!isLaidOut()) { 226 mOpenOnLayout = !collapsed; 227 } else { 228 smoothScrollTo(collapsed ? mCollapsibleHeight : 0, 0); 229 } 230 } 231 setCollapsibleHeightReserved(int heightPixels)232 public void setCollapsibleHeightReserved(int heightPixels) { 233 final int oldReserved = mCollapsibleHeightReserved; 234 mCollapsibleHeightReserved = heightPixels; 235 if (oldReserved != mCollapsibleHeightReserved) { 236 requestLayout(); 237 } 238 239 final int dReserved = mCollapsibleHeightReserved - oldReserved; 240 if (dReserved != 0 && mIsDragging) { 241 mLastTouchY -= dReserved; 242 } 243 244 final int oldCollapsibleHeight = mCollapsibleHeight; 245 mCollapsibleHeight = Math.min(mCollapsibleHeight, getMaxCollapsedHeight()); 246 247 if (updateCollapseOffset(oldCollapsibleHeight, !isDragging())) { 248 return; 249 } 250 251 invalidate(); 252 } 253 setDismissLocked(boolean locked)254 public void setDismissLocked(boolean locked) { 255 mDismissLocked = locked; 256 } 257 isMoving()258 private boolean isMoving() { 259 return mIsDragging || !mScroller.isFinished(); 260 } 261 isDragging()262 private boolean isDragging() { 263 return mIsDragging || getNestedScrollAxes() == SCROLL_AXIS_VERTICAL; 264 } 265 updateCollapseOffset(int oldCollapsibleHeight, boolean remainClosed)266 private boolean updateCollapseOffset(int oldCollapsibleHeight, boolean remainClosed) { 267 if (oldCollapsibleHeight == mCollapsibleHeight) { 268 return false; 269 } 270 271 if (getShowAtTop()) { 272 // Keep the drawer fully open. 273 setCollapseOffset(0); 274 return false; 275 } 276 277 if (isLaidOut()) { 278 final boolean isCollapsedOld = mCollapseOffset != 0; 279 if (remainClosed && (oldCollapsibleHeight < mCollapsibleHeight 280 && mCollapseOffset == oldCollapsibleHeight)) { 281 // Stay closed even at the new height. 282 setCollapseOffset(mCollapsibleHeight); 283 } else { 284 setCollapseOffset(Math.min(mCollapseOffset, mCollapsibleHeight)); 285 } 286 final boolean isCollapsedNew = mCollapseOffset != 0; 287 if (isCollapsedOld != isCollapsedNew) { 288 onCollapsedChanged(isCollapsedNew); 289 } 290 } else { 291 // Start out collapsed at first unless we restored state for otherwise 292 setCollapseOffset(mOpenOnLayout ? 0 : mCollapsibleHeight); 293 } 294 return true; 295 } 296 setCollapseOffset(float collapseOffset)297 private void setCollapseOffset(float collapseOffset) { 298 if (mCollapseOffset != collapseOffset) { 299 mCollapseOffset = collapseOffset; 300 requestLayout(); 301 } 302 } 303 getMaxCollapsedHeight()304 private int getMaxCollapsedHeight() { 305 return (isSmallCollapsed() ? mMaxCollapsedHeightSmall : mMaxCollapsedHeight) 306 + mCollapsibleHeightReserved; 307 } 308 setOnDismissedListener(OnDismissedListener listener)309 public void setOnDismissedListener(OnDismissedListener listener) { 310 mOnDismissedListener = listener; 311 } 312 isDismissable()313 private boolean isDismissable() { 314 return mOnDismissedListener != null && !mDismissLocked; 315 } 316 setOnCollapsedChangedListener(OnCollapsedChangedListener listener)317 public void setOnCollapsedChangedListener(OnCollapsedChangedListener listener) { 318 mOnCollapsedChangedListener = listener; 319 } 320 321 @Override onInterceptTouchEvent(MotionEvent ev)322 public boolean onInterceptTouchEvent(MotionEvent ev) { 323 final int action = ev.getActionMasked(); 324 325 if (action == MotionEvent.ACTION_DOWN) { 326 mVelocityTracker.clear(); 327 } 328 329 mVelocityTracker.addMovement(ev); 330 331 switch (action) { 332 case MotionEvent.ACTION_DOWN: { 333 final float x = ev.getX(); 334 final float y = ev.getY(); 335 mInitialTouchX = x; 336 mInitialTouchY = mLastTouchY = y; 337 mOpenOnClick = isListChildUnderClipped(x, y) && mCollapseOffset > 0; 338 } 339 break; 340 341 case MotionEvent.ACTION_MOVE: { 342 final float x = ev.getX(); 343 final float y = ev.getY(); 344 final float dy = y - mInitialTouchY; 345 if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null && 346 (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) { 347 mActivePointerId = ev.getPointerId(0); 348 mIsDragging = true; 349 mLastTouchY = Math.max(mLastTouchY - mTouchSlop, 350 Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop)); 351 } 352 } 353 break; 354 355 case MotionEvent.ACTION_POINTER_UP: { 356 onSecondaryPointerUp(ev); 357 } 358 break; 359 360 case MotionEvent.ACTION_CANCEL: 361 case MotionEvent.ACTION_UP: { 362 resetTouch(); 363 } 364 break; 365 } 366 367 if (mIsDragging) { 368 abortAnimation(); 369 } 370 return mIsDragging || mOpenOnClick; 371 } 372 isNestedListChildScrolled()373 private boolean isNestedListChildScrolled() { 374 return mNestedListChild != null 375 && mNestedListChild.getChildCount() > 0 376 && (mNestedListChild.getFirstVisiblePosition() > 0 377 || mNestedListChild.getChildAt(0).getTop() < 0); 378 } 379 isNestedRecyclerChildScrolled()380 private boolean isNestedRecyclerChildScrolled() { 381 if (mNestedRecyclerChild != null && mNestedRecyclerChild.getChildCount() > 0) { 382 final RecyclerView.ViewHolder vh = 383 mNestedRecyclerChild.findViewHolderForAdapterPosition(0); 384 return vh == null || vh.itemView.getTop() < 0; 385 } 386 return false; 387 } 388 389 @Override onTouchEvent(MotionEvent ev)390 public boolean onTouchEvent(MotionEvent ev) { 391 final int action = ev.getActionMasked(); 392 393 mVelocityTracker.addMovement(ev); 394 395 boolean handled = false; 396 switch (action) { 397 case MotionEvent.ACTION_DOWN: { 398 final float x = ev.getX(); 399 final float y = ev.getY(); 400 mInitialTouchX = x; 401 mInitialTouchY = mLastTouchY = y; 402 mActivePointerId = ev.getPointerId(0); 403 final boolean hitView = findChildUnder(mInitialTouchX, mInitialTouchY) != null; 404 handled = isDismissable() || mCollapsibleHeight > 0; 405 mIsDragging = hitView && handled; 406 abortAnimation(); 407 } 408 break; 409 410 case MotionEvent.ACTION_MOVE: { 411 int index = ev.findPointerIndex(mActivePointerId); 412 if (index < 0) { 413 Log.e(TAG, "Bad pointer id " + mActivePointerId + ", resetting"); 414 index = 0; 415 mActivePointerId = ev.getPointerId(0); 416 mInitialTouchX = ev.getX(); 417 mInitialTouchY = mLastTouchY = ev.getY(); 418 } 419 final float x = ev.getX(index); 420 final float y = ev.getY(index); 421 if (!mIsDragging) { 422 final float dy = y - mInitialTouchY; 423 if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null) { 424 handled = mIsDragging = true; 425 mLastTouchY = Math.max(mLastTouchY - mTouchSlop, 426 Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop)); 427 } 428 } 429 if (mIsDragging) { 430 final float dy = y - mLastTouchY; 431 if (dy > 0 && isNestedListChildScrolled()) { 432 mNestedListChild.smoothScrollBy((int) -dy, 0); 433 } else if (dy > 0 && isNestedRecyclerChildScrolled()) { 434 mNestedRecyclerChild.scrollBy(0, (int) -dy); 435 } else { 436 performDrag(dy); 437 } 438 } 439 mLastTouchY = y; 440 } 441 break; 442 443 case MotionEvent.ACTION_POINTER_DOWN: { 444 final int pointerIndex = ev.getActionIndex(); 445 mActivePointerId = ev.getPointerId(pointerIndex); 446 mInitialTouchX = ev.getX(pointerIndex); 447 mInitialTouchY = mLastTouchY = ev.getY(pointerIndex); 448 } 449 break; 450 451 case MotionEvent.ACTION_POINTER_UP: { 452 onSecondaryPointerUp(ev); 453 } 454 break; 455 456 case MotionEvent.ACTION_UP: { 457 final boolean wasDragging = mIsDragging; 458 mIsDragging = false; 459 if (!wasDragging && findChildUnder(mInitialTouchX, mInitialTouchY) == null && 460 findChildUnder(ev.getX(), ev.getY()) == null) { 461 if (isDismissable()) { 462 dispatchOnDismissed(); 463 resetTouch(); 464 return true; 465 } 466 } 467 if (mOpenOnClick && Math.abs(ev.getX() - mInitialTouchX) < mTouchSlop && 468 Math.abs(ev.getY() - mInitialTouchY) < mTouchSlop) { 469 smoothScrollTo(0, 0); 470 return true; 471 } 472 mVelocityTracker.computeCurrentVelocity(1000); 473 final float yvel = mVelocityTracker.getYVelocity(mActivePointerId); 474 if (Math.abs(yvel) > mMinFlingVelocity) { 475 if (getShowAtTop()) { 476 if (isDismissable() && yvel < 0) { 477 abortAnimation(); 478 dismiss(); 479 } else { 480 smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel); 481 } 482 } else { 483 if (isDismissable() 484 && yvel > 0 && mCollapseOffset > mCollapsibleHeight) { 485 smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, yvel); 486 mDismissOnScrollerFinished = true; 487 } else { 488 scrollNestedScrollableChildBackToTop(); 489 smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel); 490 } 491 } 492 }else { 493 smoothScrollTo( 494 mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0); 495 } 496 resetTouch(); 497 } 498 break; 499 500 case MotionEvent.ACTION_CANCEL: { 501 if (mIsDragging) { 502 smoothScrollTo( 503 mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0); 504 } 505 resetTouch(); 506 return true; 507 } 508 } 509 510 return handled; 511 } 512 513 /** 514 * Scroll nested scrollable child back to top if it has been scrolled. 515 */ 516 public void scrollNestedScrollableChildBackToTop() { 517 if (isNestedListChildScrolled()) { 518 mNestedListChild.smoothScrollToPosition(0); 519 } else if (isNestedRecyclerChildScrolled()) { 520 mNestedRecyclerChild.smoothScrollToPosition(0); 521 } 522 } 523 524 private void onSecondaryPointerUp(MotionEvent ev) { 525 final int pointerIndex = ev.getActionIndex(); 526 final int pointerId = ev.getPointerId(pointerIndex); 527 if (pointerId == mActivePointerId) { 528 // This was our active pointer going up. Choose a new 529 // active pointer and adjust accordingly. 530 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 531 mInitialTouchX = ev.getX(newPointerIndex); 532 mInitialTouchY = mLastTouchY = ev.getY(newPointerIndex); 533 mActivePointerId = ev.getPointerId(newPointerIndex); 534 } 535 } 536 537 private void resetTouch() { 538 mActivePointerId = MotionEvent.INVALID_POINTER_ID; 539 mIsDragging = false; 540 mOpenOnClick = false; 541 mInitialTouchX = mInitialTouchY = mLastTouchY = 0; 542 mVelocityTracker.clear(); 543 } 544 545 private void dismiss() { 546 mRunOnDismissedListener = new RunOnDismissedListener(); 547 post(mRunOnDismissedListener); 548 } 549 550 @Override 551 public void computeScroll() { 552 super.computeScroll(); 553 if (mScroller.computeScrollOffset()) { 554 final boolean keepGoing = !mScroller.isFinished(); 555 performDrag(mScroller.getCurrY() - mCollapseOffset); 556 if (keepGoing) { 557 postInvalidateOnAnimation(); 558 } else if (mDismissOnScrollerFinished && mOnDismissedListener != null) { 559 dismiss(); 560 } 561 } 562 } 563 564 private void abortAnimation() { 565 mScroller.abortAnimation(); 566 mRunOnDismissedListener = null; 567 mDismissOnScrollerFinished = false; 568 } 569 570 private float performDrag(float dy) { 571 if (getShowAtTop()) { 572 return 0; 573 } 574 575 final float newPos = Math.max(0, Math.min(mCollapseOffset + dy, 576 mCollapsibleHeight + mUncollapsibleHeight)); 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 if (!consumed && Math.abs(velocityY) > mMinFlingVelocity) { 845 if (getShowAtTop()) { 846 if (isDismissable() && velocityY > 0) { 847 abortAnimation(); 848 dismiss(); 849 } else { 850 smoothScrollTo(velocityY < 0 ? mCollapsibleHeight : 0, velocityY); 851 } 852 } else { 853 if (isDismissable() 854 && velocityY < 0 && mCollapseOffset > mCollapsibleHeight) { 855 smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, velocityY); 856 mDismissOnScrollerFinished = true; 857 } else { 858 smoothScrollTo(velocityY > 0 ? 0 : mCollapsibleHeight, velocityY); 859 } 860 } 861 return true; 862 } 863 return false; 864 } 865 866 private boolean performAccessibilityActionCommon(int action) { 867 switch (action) { 868 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: 869 case AccessibilityNodeInfo.ACTION_EXPAND: 870 case R.id.accessibilityActionScrollDown: 871 if (mCollapseOffset != 0) { 872 smoothScrollTo(0, 0); 873 return true; 874 } 875 break; 876 case AccessibilityNodeInfo.ACTION_COLLAPSE: 877 if (mCollapseOffset < mCollapsibleHeight) { 878 smoothScrollTo(mCollapsibleHeight, 0); 879 return true; 880 } 881 break; 882 case AccessibilityNodeInfo.ACTION_DISMISS: 883 if ((mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight) 884 && isDismissable()) { 885 smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, 0); 886 mDismissOnScrollerFinished = true; 887 return true; 888 } 889 break; 890 } 891 892 return false; 893 } 894 895 @Override 896 public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle args) { 897 if (super.onNestedPrePerformAccessibilityAction(target, action, args)) { 898 return true; 899 } 900 901 return performAccessibilityActionCommon(action); 902 } 903 904 @Override 905 public CharSequence getAccessibilityClassName() { 906 // Since we support scrolling, make this ViewGroup look like a 907 // ScrollView. This is kind of a hack until we have support for 908 // specifying auto-scroll behavior. 909 return android.widget.ScrollView.class.getName(); 910 } 911 912 @Override 913 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { 914 super.onInitializeAccessibilityNodeInfoInternal(info); 915 916 if (isEnabled()) { 917 if (mCollapseOffset != 0) { 918 info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD); 919 info.addAction(AccessibilityAction.ACTION_EXPAND); 920 info.addAction(AccessibilityAction.ACTION_SCROLL_DOWN); 921 info.setScrollable(true); 922 } 923 if ((mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight) 924 && ((mCollapseOffset < mCollapsibleHeight) || isDismissable())) { 925 info.addAction(AccessibilityAction.ACTION_SCROLL_UP); 926 info.setScrollable(true); 927 } 928 if (mCollapseOffset < mCollapsibleHeight) { 929 info.addAction(AccessibilityAction.ACTION_COLLAPSE); 930 } 931 if (mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight && isDismissable()) { 932 info.addAction(AccessibilityAction.ACTION_DISMISS); 933 } 934 } 935 936 // This view should never get accessibility focus, but it's interactive 937 // via nested scrolling, so we can't hide it completely. 938 info.removeAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS); 939 } 940 941 @Override 942 public boolean performAccessibilityActionInternal(int action, Bundle arguments) { 943 if (action == AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS.getId()) { 944 // This view should never get accessibility focus. 945 return false; 946 } 947 948 if (super.performAccessibilityActionInternal(action, arguments)) { 949 return true; 950 } 951 952 return performAccessibilityActionCommon(action); 953 } 954 955 @Override 956 public void onDrawForeground(Canvas canvas) { 957 if (mScrollIndicatorDrawable != null) { 958 mScrollIndicatorDrawable.draw(canvas); 959 } 960 961 super.onDrawForeground(canvas); 962 } 963 964 @Override 965 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 966 final int sourceWidth = MeasureSpec.getSize(widthMeasureSpec); 967 int widthSize = sourceWidth; 968 final int heightSize = MeasureSpec.getSize(heightMeasureSpec); 969 970 // Single-use layout; just ignore the mode and use available space. 971 // Clamp to maxWidth. 972 if (mMaxWidth >= 0) { 973 widthSize = Math.min(widthSize, mMaxWidth + getPaddingLeft() + getPaddingRight()); 974 } 975 976 final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); 977 final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY); 978 979 // Currently we allot more height than is really needed so that the entirety of the 980 // sheet may be pulled up. 981 // TODO: Restrict the height here to be the right value. 982 int heightUsed = 0; 983 984 // Measure always-show children first. 985 final int childCount = getChildCount(); 986 for (int i = 0; i < childCount; i++) { 987 final View child = getChildAt(i); 988 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 989 if (lp.alwaysShow && child.getVisibility() != GONE) { 990 if (lp.maxHeight != -1) { 991 final int remainingHeight = heightSize - heightUsed; 992 measureChildWithMargins(child, widthSpec, 0, 993 MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST), 994 lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0); 995 } else { 996 measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed); 997 } 998 heightUsed += child.getMeasuredHeight(); 999 } 1000 } 1001 1002 mAlwaysShowHeight = heightUsed; 1003 1004 // And now the rest. 1005 for (int i = 0; i < childCount; i++) { 1006 final View child = getChildAt(i); 1007 1008 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1009 if (!lp.alwaysShow && child.getVisibility() != GONE) { 1010 if (lp.maxHeight != -1) { 1011 final int remainingHeight = heightSize - heightUsed; 1012 measureChildWithMargins(child, widthSpec, 0, 1013 MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST), 1014 lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0); 1015 } else { 1016 measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed); 1017 } 1018 heightUsed += child.getMeasuredHeight(); 1019 } 1020 } 1021 1022 final int oldCollapsibleHeight = mCollapsibleHeight; 1023 mCollapsibleHeight = Math.max(0, 1024 heightUsed - mAlwaysShowHeight - getMaxCollapsedHeight()); 1025 mUncollapsibleHeight = heightUsed - mCollapsibleHeight; 1026 1027 updateCollapseOffset(oldCollapsibleHeight, !isDragging()); 1028 1029 if (getShowAtTop()) { 1030 mTopOffset = 0; 1031 } else { 1032 mTopOffset = Math.max(0, heightSize - heightUsed) + (int) mCollapseOffset; 1033 } 1034 1035 setMeasuredDimension(sourceWidth, heightSize); 1036 } 1037 1038 /** 1039 * @return The space reserved by views with 'alwaysShow=true' 1040 */ 1041 public int getAlwaysShowHeight() { 1042 return mAlwaysShowHeight; 1043 } 1044 1045 @Override 1046 protected void onLayout(boolean changed, int l, int t, int r, int b) { 1047 final int width = getWidth(); 1048 1049 View indicatorHost = null; 1050 1051 int ypos = mTopOffset; 1052 final int leftEdge = getPaddingLeft(); 1053 final int rightEdge = width - getPaddingRight(); 1054 final int widthAvailable = rightEdge - leftEdge; 1055 1056 boolean isIgnoreOffsetLimitSet = false; 1057 int ignoreOffsetLimit = 0; 1058 final int childCount = getChildCount(); 1059 for (int i = 0; i < childCount; i++) { 1060 final View child = getChildAt(i); 1061 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1062 if (lp.hasNestedScrollIndicator) { 1063 indicatorHost = child; 1064 } 1065 1066 if (child.getVisibility() == GONE) { 1067 continue; 1068 } 1069 1070 if (mIgnoreOffsetTopLimitViewId != ID_NULL && !isIgnoreOffsetLimitSet) { 1071 if (mIgnoreOffsetTopLimitViewId == child.getId()) { 1072 ignoreOffsetLimit = child.getBottom() + lp.bottomMargin; 1073 isIgnoreOffsetLimitSet = true; 1074 } 1075 } 1076 1077 int top = ypos + lp.topMargin; 1078 if (lp.ignoreOffset) { 1079 if (!isDragging()) { 1080 lp.mFixedTop = (int) (top - mCollapseOffset); 1081 } 1082 if (isIgnoreOffsetLimitSet) { 1083 top = Math.max(ignoreOffsetLimit + lp.topMargin, (int) (top - mCollapseOffset)); 1084 ignoreOffsetLimit = top + child.getMeasuredHeight() + lp.bottomMargin; 1085 } else { 1086 top -= mCollapseOffset; 1087 } 1088 } 1089 final int bottom = top + child.getMeasuredHeight(); 1090 1091 final int childWidth = child.getMeasuredWidth(); 1092 final int left = leftEdge + (widthAvailable - childWidth) / 2; 1093 final int right = left + childWidth; 1094 1095 child.layout(left, top, right, bottom); 1096 1097 ypos = bottom + lp.bottomMargin; 1098 } 1099 1100 if (mScrollIndicatorDrawable != null) { 1101 if (indicatorHost != null) { 1102 final int left = indicatorHost.getLeft(); 1103 final int right = indicatorHost.getRight(); 1104 final int bottom = indicatorHost.getTop(); 1105 final int top = bottom - mScrollIndicatorDrawable.getIntrinsicHeight(); 1106 mScrollIndicatorDrawable.setBounds(left, top, right, bottom); 1107 setWillNotDraw(!isCollapsed()); 1108 } else { 1109 mScrollIndicatorDrawable = null; 1110 setWillNotDraw(true); 1111 } 1112 } 1113 } 1114 1115 @Override 1116 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 1117 return new LayoutParams(getContext(), attrs); 1118 } 1119 1120 @Override 1121 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 1122 if (p instanceof LayoutParams) { 1123 return new LayoutParams((LayoutParams) p); 1124 } else if (p instanceof MarginLayoutParams) { 1125 return new LayoutParams((MarginLayoutParams) p); 1126 } 1127 return new LayoutParams(p); 1128 } 1129 1130 @Override 1131 protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 1132 return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); 1133 } 1134 1135 @Override 1136 protected Parcelable onSaveInstanceState() { 1137 final SavedState ss = new SavedState(super.onSaveInstanceState()); 1138 ss.open = mCollapsibleHeight > 0 && mCollapseOffset == 0; 1139 ss.mCollapsibleHeightReserved = mCollapsibleHeightReserved; 1140 return ss; 1141 } 1142 1143 @Override 1144 protected void onRestoreInstanceState(Parcelable state) { 1145 final SavedState ss = (SavedState) state; 1146 super.onRestoreInstanceState(ss.getSuperState()); 1147 mOpenOnLayout = ss.open; 1148 mCollapsibleHeightReserved = ss.mCollapsibleHeightReserved; 1149 } 1150 1151 private View findIgnoreOffsetLimitView() { 1152 if (mIgnoreOffsetTopLimitViewId == ID_NULL) { 1153 return null; 1154 } 1155 View v = findViewById(mIgnoreOffsetTopLimitViewId); 1156 if (v != null && v != this && v.getParent() == this && v.getVisibility() != View.GONE) { 1157 return v; 1158 } 1159 return null; 1160 } 1161 1162 public static class LayoutParams extends MarginLayoutParams { 1163 public boolean alwaysShow; 1164 public boolean ignoreOffset; 1165 public boolean hasNestedScrollIndicator; 1166 public int maxHeight; 1167 int mFixedTop; 1168 1169 public LayoutParams(Context c, AttributeSet attrs) { 1170 super(c, attrs); 1171 1172 final TypedArray a = c.obtainStyledAttributes(attrs, 1173 R.styleable.ResolverDrawerLayout_LayoutParams); 1174 alwaysShow = a.getBoolean( 1175 R.styleable.ResolverDrawerLayout_LayoutParams_layout_alwaysShow, 1176 false); 1177 ignoreOffset = a.getBoolean( 1178 R.styleable.ResolverDrawerLayout_LayoutParams_layout_ignoreOffset, 1179 false); 1180 hasNestedScrollIndicator = a.getBoolean( 1181 R.styleable.ResolverDrawerLayout_LayoutParams_layout_hasNestedScrollIndicator, 1182 false); 1183 maxHeight = a.getDimensionPixelSize( 1184 R.styleable.ResolverDrawerLayout_LayoutParams_layout_maxHeight, -1); 1185 a.recycle(); 1186 } 1187 1188 public LayoutParams(int width, int height) { 1189 super(width, height); 1190 } 1191 1192 public LayoutParams(LayoutParams source) { 1193 super(source); 1194 this.alwaysShow = source.alwaysShow; 1195 this.ignoreOffset = source.ignoreOffset; 1196 this.hasNestedScrollIndicator = source.hasNestedScrollIndicator; 1197 this.maxHeight = source.maxHeight; 1198 } 1199 1200 public LayoutParams(MarginLayoutParams source) { 1201 super(source); 1202 } 1203 1204 public LayoutParams(ViewGroup.LayoutParams source) { 1205 super(source); 1206 } 1207 } 1208 1209 static class SavedState extends BaseSavedState { 1210 boolean open; 1211 private int mCollapsibleHeightReserved; 1212 1213 SavedState(Parcelable superState) { 1214 super(superState); 1215 } 1216 1217 private SavedState(Parcel in) { 1218 super(in); 1219 open = in.readInt() != 0; 1220 mCollapsibleHeightReserved = in.readInt(); 1221 } 1222 1223 @Override 1224 public void writeToParcel(Parcel out, int flags) { 1225 super.writeToParcel(out, flags); 1226 out.writeInt(open ? 1 : 0); 1227 out.writeInt(mCollapsibleHeightReserved); 1228 } 1229 1230 public static final Parcelable.Creator<SavedState> CREATOR = 1231 new Parcelable.Creator<SavedState>() { 1232 @Override 1233 public SavedState createFromParcel(Parcel in) { 1234 return new SavedState(in); 1235 } 1236 1237 @Override 1238 public SavedState[] newArray(int size) { 1239 return new SavedState[size]; 1240 } 1241 }; 1242 } 1243 1244 /** 1245 * Listener for sheet dismissed events. 1246 */ 1247 public interface OnDismissedListener { 1248 /** 1249 * Callback when the sheet is dismissed by the user. 1250 */ 1251 void onDismissed(); 1252 } 1253 1254 /** 1255 * Listener for sheet collapsed / expanded events. 1256 */ 1257 public interface OnCollapsedChangedListener { 1258 /** 1259 * Callback when the sheet is either fully expanded or collapsed. 1260 * @param isCollapsed true when collapsed, false when expanded. 1261 */ 1262 void onCollapsedChanged(boolean isCollapsed); 1263 } 1264 1265 private class RunOnDismissedListener implements Runnable { 1266 @Override 1267 public void run() { 1268 dispatchOnDismissed(); 1269 } 1270 } 1271 1272 private MetricsLogger getMetricsLogger() { 1273 if (mMetricsLogger == null) { 1274 mMetricsLogger = new MetricsLogger(); 1275 } 1276 return mMetricsLogger; 1277 } 1278 } 1279