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