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 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(mHeightUsed, 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 com.android.internal.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 < mHeightUsed) && isDismissable()) { 884 smoothScrollTo(mHeightUsed, 0); 885 mDismissOnScrollerFinished = true; 886 return true; 887 } 888 break; 889 } 890 891 return false; 892 } 893 894 @Override 895 public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle args) { 896 if (super.onNestedPrePerformAccessibilityAction(target, action, args)) { 897 return true; 898 } 899 900 return performAccessibilityActionCommon(action); 901 } 902 903 @Override 904 public CharSequence getAccessibilityClassName() { 905 // Since we support scrolling, make this ViewGroup look like a 906 // ScrollView. This is kind of a hack until we have support for 907 // specifying auto-scroll behavior. 908 return android.widget.ScrollView.class.getName(); 909 } 910 911 @Override 912 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { 913 super.onInitializeAccessibilityNodeInfoInternal(info); 914 915 if (isEnabled()) { 916 if (mCollapseOffset != 0) { 917 info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD); 918 info.addAction(AccessibilityAction.ACTION_EXPAND); 919 info.addAction(AccessibilityAction.ACTION_SCROLL_DOWN); 920 info.setScrollable(true); 921 } 922 if ((mCollapseOffset < mHeightUsed) 923 && ((mCollapseOffset < mCollapsibleHeight) || isDismissable())) { 924 info.addAction(AccessibilityAction.ACTION_SCROLL_UP); 925 info.setScrollable(true); 926 } 927 if (mCollapseOffset < mCollapsibleHeight) { 928 info.addAction(AccessibilityAction.ACTION_COLLAPSE); 929 } 930 if (mCollapseOffset < mHeightUsed && isDismissable()) { 931 info.addAction(AccessibilityAction.ACTION_DISMISS); 932 } 933 } 934 935 // This view should never get accessibility focus, but it's interactive 936 // via nested scrolling, so we can't hide it completely. 937 info.removeAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS); 938 } 939 940 @Override 941 public boolean performAccessibilityActionInternal(int action, Bundle arguments) { 942 if (action == AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS.getId()) { 943 // This view should never get accessibility focus. 944 return false; 945 } 946 947 if (super.performAccessibilityActionInternal(action, arguments)) { 948 return true; 949 } 950 951 return performAccessibilityActionCommon(action); 952 } 953 954 @Override 955 public void onDrawForeground(Canvas canvas) { 956 if (mScrollIndicatorDrawable != null) { 957 mScrollIndicatorDrawable.draw(canvas); 958 } 959 960 super.onDrawForeground(canvas); 961 } 962 963 @Override 964 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 965 final int sourceWidth = MeasureSpec.getSize(widthMeasureSpec); 966 int widthSize = sourceWidth; 967 final int heightSize = MeasureSpec.getSize(heightMeasureSpec); 968 969 // Single-use layout; just ignore the mode and use available space. 970 // Clamp to maxWidth. 971 if (mMaxWidth >= 0) { 972 widthSize = Math.min(widthSize, mMaxWidth + getPaddingLeft() + getPaddingRight()); 973 } 974 975 final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); 976 final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY); 977 978 // Currently we allot more height than is really needed so that the entirety of the 979 // sheet may be pulled up. 980 // TODO: Restrict the height here to be the right value. 981 int heightUsed = 0; 982 983 // Measure always-show children first. 984 final int childCount = getChildCount(); 985 for (int i = 0; i < childCount; i++) { 986 final View child = getChildAt(i); 987 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 988 if (lp.alwaysShow && child.getVisibility() != GONE) { 989 if (lp.maxHeight != -1) { 990 final int remainingHeight = heightSize - heightUsed; 991 measureChildWithMargins(child, widthSpec, 0, 992 MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST), 993 lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0); 994 } else { 995 measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed); 996 } 997 heightUsed += child.getMeasuredHeight(); 998 } 999 } 1000 1001 mAlwaysShowHeight = heightUsed; 1002 1003 // And now the rest. 1004 for (int i = 0; i < childCount; i++) { 1005 final View child = getChildAt(i); 1006 1007 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1008 if (!lp.alwaysShow && child.getVisibility() != GONE) { 1009 if (lp.maxHeight != -1) { 1010 final int remainingHeight = heightSize - heightUsed; 1011 measureChildWithMargins(child, widthSpec, 0, 1012 MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST), 1013 lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0); 1014 } else { 1015 measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed); 1016 } 1017 heightUsed += child.getMeasuredHeight(); 1018 } 1019 } 1020 1021 mHeightUsed = heightUsed; 1022 int oldCollapsibleHeight = updateCollapsibleHeight(); 1023 updateCollapseOffset(oldCollapsibleHeight, !isDragging()); 1024 1025 if (getShowAtTop()) { 1026 mTopOffset = 0; 1027 } else { 1028 mTopOffset = Math.max(0, heightSize - mHeightUsed) + (int) mCollapseOffset; 1029 } 1030 1031 setMeasuredDimension(sourceWidth, heightSize); 1032 } 1033 1034 private int updateCollapsibleHeight() { 1035 final int oldCollapsibleHeight = mCollapsibleHeight; 1036 mCollapsibleHeight = Math.max(0, mHeightUsed - mAlwaysShowHeight - getMaxCollapsedHeight()); 1037 return oldCollapsibleHeight; 1038 } 1039 1040 /** 1041 * @return The space reserved by views with 'alwaysShow=true' 1042 */ 1043 public int getAlwaysShowHeight() { 1044 return mAlwaysShowHeight; 1045 } 1046 1047 @Override 1048 protected void onLayout(boolean changed, int l, int t, int r, int b) { 1049 final int width = getWidth(); 1050 1051 View indicatorHost = null; 1052 1053 int ypos = mTopOffset; 1054 final int leftEdge = getPaddingLeft(); 1055 final int rightEdge = width - getPaddingRight(); 1056 final int widthAvailable = rightEdge - leftEdge; 1057 1058 boolean isIgnoreOffsetLimitSet = false; 1059 int ignoreOffsetLimit = 0; 1060 final int childCount = getChildCount(); 1061 for (int i = 0; i < childCount; i++) { 1062 final View child = getChildAt(i); 1063 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1064 if (lp.hasNestedScrollIndicator) { 1065 indicatorHost = child; 1066 } 1067 1068 if (child.getVisibility() == GONE) { 1069 continue; 1070 } 1071 1072 if (mIgnoreOffsetTopLimitViewId != ID_NULL && !isIgnoreOffsetLimitSet) { 1073 if (mIgnoreOffsetTopLimitViewId == child.getId()) { 1074 ignoreOffsetLimit = child.getBottom() + lp.bottomMargin; 1075 isIgnoreOffsetLimitSet = true; 1076 } 1077 } 1078 1079 int top = ypos + lp.topMargin; 1080 if (lp.ignoreOffset) { 1081 if (!isDragging()) { 1082 lp.mFixedTop = (int) (top - mCollapseOffset); 1083 } 1084 if (isIgnoreOffsetLimitSet) { 1085 top = Math.max(ignoreOffsetLimit + lp.topMargin, (int) (top - mCollapseOffset)); 1086 ignoreOffsetLimit = top + child.getMeasuredHeight() + lp.bottomMargin; 1087 } else { 1088 top -= mCollapseOffset; 1089 } 1090 } 1091 final int bottom = top + child.getMeasuredHeight(); 1092 1093 final int childWidth = child.getMeasuredWidth(); 1094 final int left = leftEdge + (widthAvailable - childWidth) / 2; 1095 final int right = left + childWidth; 1096 1097 child.layout(left, top, right, bottom); 1098 1099 ypos = bottom + lp.bottomMargin; 1100 } 1101 1102 if (mScrollIndicatorDrawable != null) { 1103 if (indicatorHost != null) { 1104 final int left = indicatorHost.getLeft(); 1105 final int right = indicatorHost.getRight(); 1106 final int bottom = indicatorHost.getTop(); 1107 final int top = bottom - mScrollIndicatorDrawable.getIntrinsicHeight(); 1108 mScrollIndicatorDrawable.setBounds(left, top, right, bottom); 1109 setWillNotDraw(!isCollapsed()); 1110 } else { 1111 mScrollIndicatorDrawable = null; 1112 setWillNotDraw(true); 1113 } 1114 } 1115 } 1116 1117 @Override 1118 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 1119 return new LayoutParams(getContext(), attrs); 1120 } 1121 1122 @Override 1123 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 1124 if (p instanceof LayoutParams) { 1125 return new LayoutParams((LayoutParams) p); 1126 } else if (p instanceof MarginLayoutParams) { 1127 return new LayoutParams((MarginLayoutParams) p); 1128 } 1129 return new LayoutParams(p); 1130 } 1131 1132 @Override 1133 protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 1134 return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); 1135 } 1136 1137 @Override 1138 protected Parcelable onSaveInstanceState() { 1139 final SavedState ss = new SavedState(super.onSaveInstanceState()); 1140 ss.open = mCollapsibleHeight > 0 && mCollapseOffset == 0; 1141 ss.mCollapsibleHeightReserved = mCollapsibleHeightReserved; 1142 return ss; 1143 } 1144 1145 @Override 1146 protected void onRestoreInstanceState(Parcelable state) { 1147 final SavedState ss = (SavedState) state; 1148 super.onRestoreInstanceState(ss.getSuperState()); 1149 mOpenOnLayout = ss.open; 1150 mCollapsibleHeightReserved = ss.mCollapsibleHeightReserved; 1151 } 1152 1153 private View findIgnoreOffsetLimitView() { 1154 if (mIgnoreOffsetTopLimitViewId == ID_NULL) { 1155 return null; 1156 } 1157 View v = findViewById(mIgnoreOffsetTopLimitViewId); 1158 if (v != null && v != this && v.getParent() == this && v.getVisibility() != View.GONE) { 1159 return v; 1160 } 1161 return null; 1162 } 1163 1164 public static class LayoutParams extends MarginLayoutParams { 1165 public boolean alwaysShow; 1166 public boolean ignoreOffset; 1167 public boolean hasNestedScrollIndicator; 1168 public int maxHeight; 1169 int mFixedTop; 1170 1171 public LayoutParams(Context c, AttributeSet attrs) { 1172 super(c, attrs); 1173 1174 final TypedArray a = c.obtainStyledAttributes(attrs, 1175 R.styleable.ResolverDrawerLayout_LayoutParams); 1176 alwaysShow = a.getBoolean( 1177 R.styleable.ResolverDrawerLayout_LayoutParams_layout_alwaysShow, 1178 false); 1179 ignoreOffset = a.getBoolean( 1180 R.styleable.ResolverDrawerLayout_LayoutParams_layout_ignoreOffset, 1181 false); 1182 hasNestedScrollIndicator = a.getBoolean( 1183 R.styleable.ResolverDrawerLayout_LayoutParams_layout_hasNestedScrollIndicator, 1184 false); 1185 maxHeight = a.getDimensionPixelSize( 1186 R.styleable.ResolverDrawerLayout_LayoutParams_layout_maxHeight, -1); 1187 a.recycle(); 1188 } 1189 1190 public LayoutParams(int width, int height) { 1191 super(width, height); 1192 } 1193 1194 public LayoutParams(LayoutParams source) { 1195 super(source); 1196 this.alwaysShow = source.alwaysShow; 1197 this.ignoreOffset = source.ignoreOffset; 1198 this.hasNestedScrollIndicator = source.hasNestedScrollIndicator; 1199 this.maxHeight = source.maxHeight; 1200 } 1201 1202 public LayoutParams(MarginLayoutParams source) { 1203 super(source); 1204 } 1205 1206 public LayoutParams(ViewGroup.LayoutParams source) { 1207 super(source); 1208 } 1209 } 1210 1211 static class SavedState extends BaseSavedState { 1212 boolean open; 1213 private int mCollapsibleHeightReserved; 1214 1215 SavedState(Parcelable superState) { 1216 super(superState); 1217 } 1218 1219 private SavedState(Parcel in) { 1220 super(in); 1221 open = in.readInt() != 0; 1222 mCollapsibleHeightReserved = in.readInt(); 1223 } 1224 1225 @Override 1226 public void writeToParcel(Parcel out, int flags) { 1227 super.writeToParcel(out, flags); 1228 out.writeInt(open ? 1 : 0); 1229 out.writeInt(mCollapsibleHeightReserved); 1230 } 1231 1232 public static final Parcelable.Creator<SavedState> CREATOR = 1233 new Parcelable.Creator<SavedState>() { 1234 @Override 1235 public SavedState createFromParcel(Parcel in) { 1236 return new SavedState(in); 1237 } 1238 1239 @Override 1240 public SavedState[] newArray(int size) { 1241 return new SavedState[size]; 1242 } 1243 }; 1244 } 1245 1246 /** 1247 * Listener for sheet dismissed events. 1248 */ 1249 public interface OnDismissedListener { 1250 /** 1251 * Callback when the sheet is dismissed by the user. 1252 */ 1253 void onDismissed(); 1254 } 1255 1256 /** 1257 * Listener for sheet collapsed / expanded events. 1258 */ 1259 public interface OnCollapsedChangedListener { 1260 /** 1261 * Callback when the sheet is either fully expanded or collapsed. 1262 * @param isCollapsed true when collapsed, false when expanded. 1263 */ 1264 void onCollapsedChanged(boolean isCollapsed); 1265 } 1266 1267 private class RunOnDismissedListener implements Runnable { 1268 @Override 1269 public void run() { 1270 dispatchOnDismissed(); 1271 } 1272 } 1273 1274 private MetricsLogger getMetricsLogger() { 1275 if (mMetricsLogger == null) { 1276 mMetricsLogger = new MetricsLogger(); 1277 } 1278 return mMetricsLogger; 1279 } 1280 } 1281