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 scrollNestedScrollableChildBackToTop(); 468 smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel); 469 } 470 } 471 }else { 472 smoothScrollTo( 473 mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0); 474 } 475 resetTouch(); 476 } 477 break; 478 479 case MotionEvent.ACTION_CANCEL: { 480 if (mIsDragging) { 481 smoothScrollTo( 482 mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0); 483 } 484 resetTouch(); 485 return true; 486 } 487 } 488 489 return handled; 490 } 491 492 /** 493 * Scroll nested scrollable child back to top if it has been scrolled. 494 */ 495 public void scrollNestedScrollableChildBackToTop() { 496 if (isNestedListChildScrolled()) { 497 mNestedListChild.smoothScrollToPosition(0); 498 } else if (isNestedRecyclerChildScrolled()) { 499 mNestedRecyclerChild.smoothScrollToPosition(0); 500 } 501 } 502 503 private void onSecondaryPointerUp(MotionEvent ev) { 504 final int pointerIndex = ev.getActionIndex(); 505 final int pointerId = ev.getPointerId(pointerIndex); 506 if (pointerId == mActivePointerId) { 507 // This was our active pointer going up. Choose a new 508 // active pointer and adjust accordingly. 509 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 510 mInitialTouchX = ev.getX(newPointerIndex); 511 mInitialTouchY = mLastTouchY = ev.getY(newPointerIndex); 512 mActivePointerId = ev.getPointerId(newPointerIndex); 513 } 514 } 515 516 private void resetTouch() { 517 mActivePointerId = MotionEvent.INVALID_POINTER_ID; 518 mIsDragging = false; 519 mOpenOnClick = false; 520 mInitialTouchX = mInitialTouchY = mLastTouchY = 0; 521 mVelocityTracker.clear(); 522 } 523 524 private void dismiss() { 525 mRunOnDismissedListener = new RunOnDismissedListener(); 526 post(mRunOnDismissedListener); 527 } 528 529 @Override 530 public void computeScroll() { 531 super.computeScroll(); 532 if (mScroller.computeScrollOffset()) { 533 final boolean keepGoing = !mScroller.isFinished(); 534 performDrag(mScroller.getCurrY() - mCollapseOffset); 535 if (keepGoing) { 536 postInvalidateOnAnimation(); 537 } else if (mDismissOnScrollerFinished && mOnDismissedListener != null) { 538 dismiss(); 539 } 540 } 541 } 542 543 private void abortAnimation() { 544 mScroller.abortAnimation(); 545 mRunOnDismissedListener = null; 546 mDismissOnScrollerFinished = false; 547 } 548 549 private float performDrag(float dy) { 550 if (getShowAtTop()) { 551 return 0; 552 } 553 554 final float newPos = Math.max(0, Math.min(mCollapseOffset + dy, 555 mCollapsibleHeight + mUncollapsibleHeight)); 556 if (newPos != mCollapseOffset) { 557 dy = newPos - mCollapseOffset; 558 559 mDragRemainder += dy - (int) dy; 560 if (mDragRemainder >= 1.0f) { 561 mDragRemainder -= 1.0f; 562 dy += 1.0f; 563 } else if (mDragRemainder <= -1.0f) { 564 mDragRemainder += 1.0f; 565 dy -= 1.0f; 566 } 567 568 final int childCount = getChildCount(); 569 for (int i = 0; i < childCount; i++) { 570 final View child = getChildAt(i); 571 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 572 if (!lp.ignoreOffset) { 573 child.offsetTopAndBottom((int) dy); 574 } 575 } 576 final boolean isCollapsedOld = mCollapseOffset != 0; 577 mCollapseOffset = newPos; 578 mTopOffset += dy; 579 final boolean isCollapsedNew = newPos != 0; 580 if (isCollapsedOld != isCollapsedNew) { 581 onCollapsedChanged(isCollapsedNew); 582 getMetricsLogger().write( 583 new LogMaker(MetricsEvent.ACTION_SHARESHEET_COLLAPSED_CHANGED) 584 .setSubtype(isCollapsedNew ? 1 : 0)); 585 } 586 onScrollChanged(0, (int) newPos, 0, (int) (newPos - dy)); 587 postInvalidateOnAnimation(); 588 return dy; 589 } 590 return 0; 591 } 592 593 private void onCollapsedChanged(boolean isCollapsed) { 594 notifyViewAccessibilityStateChangedIfNeeded( 595 AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); 596 597 if (mScrollIndicatorDrawable != null) { 598 setWillNotDraw(!isCollapsed); 599 } 600 601 if (mOnCollapsedChangedListener != null) { 602 mOnCollapsedChangedListener.onCollapsedChanged(isCollapsed); 603 } 604 } 605 606 void dispatchOnDismissed() { 607 if (mOnDismissedListener != null) { 608 mOnDismissedListener.onDismissed(); 609 } 610 if (mRunOnDismissedListener != null) { 611 removeCallbacks(mRunOnDismissedListener); 612 mRunOnDismissedListener = null; 613 } 614 } 615 616 private void smoothScrollTo(int yOffset, float velocity) { 617 abortAnimation(); 618 final int sy = (int) mCollapseOffset; 619 int dy = yOffset - sy; 620 if (dy == 0) { 621 return; 622 } 623 624 final int height = getHeight(); 625 final int halfHeight = height / 2; 626 final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / height); 627 final float distance = halfHeight + halfHeight * 628 distanceInfluenceForSnapDuration(distanceRatio); 629 630 int duration = 0; 631 velocity = Math.abs(velocity); 632 if (velocity > 0) { 633 duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 634 } else { 635 final float pageDelta = (float) Math.abs(dy) / height; 636 duration = (int) ((pageDelta + 1) * 100); 637 } 638 duration = Math.min(duration, 300); 639 640 mScroller.startScroll(0, sy, 0, dy, duration); 641 postInvalidateOnAnimation(); 642 } 643 644 private float distanceInfluenceForSnapDuration(float f) { 645 f -= 0.5f; // center the values about 0. 646 f *= 0.3f * Math.PI / 2.0f; 647 return (float) Math.sin(f); 648 } 649 650 /** 651 * Note: this method doesn't take Z into account for overlapping views 652 * since it is only used in contexts where this doesn't affect the outcome. 653 */ 654 private View findChildUnder(float x, float y) { 655 return findChildUnder(this, x, y); 656 } 657 658 private static View findChildUnder(ViewGroup parent, float x, float y) { 659 final int childCount = parent.getChildCount(); 660 for (int i = childCount - 1; i >= 0; i--) { 661 final View child = parent.getChildAt(i); 662 if (isChildUnder(child, x, y)) { 663 return child; 664 } 665 } 666 return null; 667 } 668 669 private View findListChildUnder(float x, float y) { 670 View v = findChildUnder(x, y); 671 while (v != null) { 672 x -= v.getX(); 673 y -= v.getY(); 674 if (v instanceof AbsListView) { 675 // One more after this. 676 return findChildUnder((ViewGroup) v, x, y); 677 } 678 v = v instanceof ViewGroup ? findChildUnder((ViewGroup) v, x, y) : null; 679 } 680 return v; 681 } 682 683 /** 684 * This only checks clipping along the bottom edge. 685 */ 686 private boolean isListChildUnderClipped(float x, float y) { 687 final View listChild = findListChildUnder(x, y); 688 return listChild != null && isDescendantClipped(listChild); 689 } 690 691 private boolean isDescendantClipped(View child) { 692 mTempRect.set(0, 0, child.getWidth(), child.getHeight()); 693 offsetDescendantRectToMyCoords(child, mTempRect); 694 View directChild; 695 if (child.getParent() == this) { 696 directChild = child; 697 } else { 698 View v = child; 699 ViewParent p = child.getParent(); 700 while (p != this) { 701 v = (View) p; 702 p = v.getParent(); 703 } 704 directChild = v; 705 } 706 707 // ResolverDrawerLayout lays out vertically in child order; 708 // the next view and forward is what to check against. 709 int clipEdge = getHeight() - getPaddingBottom(); 710 final int childCount = getChildCount(); 711 for (int i = indexOfChild(directChild) + 1; i < childCount; i++) { 712 final View nextChild = getChildAt(i); 713 if (nextChild.getVisibility() == GONE) { 714 continue; 715 } 716 clipEdge = Math.min(clipEdge, nextChild.getTop()); 717 } 718 return mTempRect.bottom > clipEdge; 719 } 720 721 private static boolean isChildUnder(View child, float x, float y) { 722 final float left = child.getX(); 723 final float top = child.getY(); 724 final float right = left + child.getWidth(); 725 final float bottom = top + child.getHeight(); 726 return x >= left && y >= top && x < right && y < bottom; 727 } 728 729 @Override 730 public void requestChildFocus(View child, View focused) { 731 super.requestChildFocus(child, focused); 732 if (!isInTouchMode() && isDescendantClipped(focused)) { 733 smoothScrollTo(0, 0); 734 } 735 } 736 737 @Override 738 protected void onAttachedToWindow() { 739 super.onAttachedToWindow(); 740 getViewTreeObserver().addOnTouchModeChangeListener(mTouchModeChangeListener); 741 } 742 743 @Override 744 protected void onDetachedFromWindow() { 745 super.onDetachedFromWindow(); 746 getViewTreeObserver().removeOnTouchModeChangeListener(mTouchModeChangeListener); 747 abortAnimation(); 748 } 749 750 @Override 751 public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { 752 if ((nestedScrollAxes & View.SCROLL_AXIS_VERTICAL) != 0) { 753 if (target instanceof AbsListView) { 754 mNestedListChild = (AbsListView) target; 755 } 756 if (target instanceof RecyclerView) { 757 mNestedRecyclerChild = (RecyclerView) target; 758 } 759 return true; 760 } 761 return false; 762 } 763 764 @Override 765 public void onNestedScrollAccepted(View child, View target, int axes) { 766 super.onNestedScrollAccepted(child, target, axes); 767 } 768 769 @Override 770 public void onStopNestedScroll(View child) { 771 super.onStopNestedScroll(child); 772 if (mScroller.isFinished()) { 773 smoothScrollTo(mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0); 774 } 775 } 776 777 @Override 778 public void onNestedScroll(View target, int dxConsumed, int dyConsumed, 779 int dxUnconsumed, int dyUnconsumed) { 780 if (dyUnconsumed < 0) { 781 performDrag(-dyUnconsumed); 782 } 783 } 784 785 @Override 786 public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { 787 if (dy > 0) { 788 consumed[1] = (int) -performDrag(-dy); 789 } 790 } 791 792 @Override 793 public boolean onNestedPreFling(View target, float velocityX, float velocityY) { 794 if (!getShowAtTop() && velocityY > mMinFlingVelocity && mCollapseOffset != 0) { 795 smoothScrollTo(0, velocityY); 796 return true; 797 } 798 return false; 799 } 800 801 @Override 802 public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { 803 if (!consumed && Math.abs(velocityY) > mMinFlingVelocity) { 804 if (getShowAtTop()) { 805 if (isDismissable() && velocityY > 0) { 806 abortAnimation(); 807 dismiss(); 808 } else { 809 smoothScrollTo(velocityY < 0 ? mCollapsibleHeight : 0, velocityY); 810 } 811 } else { 812 if (isDismissable() 813 && velocityY < 0 && mCollapseOffset > mCollapsibleHeight) { 814 smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, velocityY); 815 mDismissOnScrollerFinished = true; 816 } else { 817 smoothScrollTo(velocityY > 0 ? 0 : mCollapsibleHeight, velocityY); 818 } 819 } 820 return true; 821 } 822 return false; 823 } 824 825 private boolean performAccessibilityActionCommon(int action) { 826 switch (action) { 827 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: 828 case AccessibilityNodeInfo.ACTION_EXPAND: 829 case R.id.accessibilityActionScrollDown: 830 if (mCollapseOffset != 0) { 831 smoothScrollTo(0, 0); 832 return true; 833 } 834 break; 835 case AccessibilityNodeInfo.ACTION_COLLAPSE: 836 if (mCollapseOffset < mCollapsibleHeight) { 837 smoothScrollTo(mCollapsibleHeight, 0); 838 return true; 839 } 840 break; 841 case AccessibilityNodeInfo.ACTION_DISMISS: 842 if ((mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight) 843 && isDismissable()) { 844 smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, 0); 845 mDismissOnScrollerFinished = true; 846 return true; 847 } 848 break; 849 } 850 851 return false; 852 } 853 854 @Override 855 public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle args) { 856 if (super.onNestedPrePerformAccessibilityAction(target, action, args)) { 857 return true; 858 } 859 860 return performAccessibilityActionCommon(action); 861 } 862 863 @Override 864 public CharSequence getAccessibilityClassName() { 865 // Since we support scrolling, make this ViewGroup look like a 866 // ScrollView. This is kind of a hack until we have support for 867 // specifying auto-scroll behavior. 868 return android.widget.ScrollView.class.getName(); 869 } 870 871 @Override 872 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { 873 super.onInitializeAccessibilityNodeInfoInternal(info); 874 875 if (isEnabled()) { 876 if (mCollapseOffset != 0) { 877 info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD); 878 info.addAction(AccessibilityAction.ACTION_EXPAND); 879 info.addAction(AccessibilityAction.ACTION_SCROLL_DOWN); 880 info.setScrollable(true); 881 } 882 if ((mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight) 883 && ((mCollapseOffset < mCollapsibleHeight) || isDismissable())) { 884 info.addAction(AccessibilityAction.ACTION_SCROLL_UP); 885 info.setScrollable(true); 886 } 887 if (mCollapseOffset < mCollapsibleHeight) { 888 info.addAction(AccessibilityAction.ACTION_COLLAPSE); 889 } 890 if (mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight && isDismissable()) { 891 info.addAction(AccessibilityAction.ACTION_DISMISS); 892 } 893 } 894 895 // This view should never get accessibility focus, but it's interactive 896 // via nested scrolling, so we can't hide it completely. 897 info.removeAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS); 898 } 899 900 @Override 901 public boolean performAccessibilityActionInternal(int action, Bundle arguments) { 902 if (action == AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS.getId()) { 903 // This view should never get accessibility focus. 904 return false; 905 } 906 907 if (super.performAccessibilityActionInternal(action, arguments)) { 908 return true; 909 } 910 911 return performAccessibilityActionCommon(action); 912 } 913 914 @Override 915 public void onDrawForeground(Canvas canvas) { 916 if (mScrollIndicatorDrawable != null) { 917 mScrollIndicatorDrawable.draw(canvas); 918 } 919 920 super.onDrawForeground(canvas); 921 } 922 923 @Override 924 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 925 final int sourceWidth = MeasureSpec.getSize(widthMeasureSpec); 926 int widthSize = sourceWidth; 927 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 928 929 // Single-use layout; just ignore the mode and use available space. 930 // Clamp to maxWidth. 931 if (mMaxWidth >= 0) { 932 widthSize = Math.min(widthSize, mMaxWidth); 933 } 934 935 final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); 936 final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY); 937 938 // Currently we allot more height than is really needed so that the entirety of the 939 // sheet may be pulled up. 940 // TODO: Restrict the height here to be the right value. 941 int heightUsed = 0; 942 943 // Measure always-show children first. 944 final int childCount = getChildCount(); 945 for (int i = 0; i < childCount; i++) { 946 final View child = getChildAt(i); 947 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 948 if (lp.alwaysShow && child.getVisibility() != GONE) { 949 if (lp.maxHeight != -1) { 950 final int remainingHeight = heightSize - heightUsed; 951 measureChildWithMargins(child, widthSpec, 0, 952 MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST), 953 lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0); 954 } else { 955 measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed); 956 } 957 heightUsed += child.getMeasuredHeight(); 958 } 959 } 960 961 mAlwaysShowHeight = heightUsed; 962 963 // And now the rest. 964 for (int i = 0; i < childCount; i++) { 965 final View child = getChildAt(i); 966 967 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 968 if (!lp.alwaysShow && child.getVisibility() != GONE) { 969 if (lp.maxHeight != -1) { 970 final int remainingHeight = heightSize - heightUsed; 971 measureChildWithMargins(child, widthSpec, 0, 972 MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST), 973 lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0); 974 } else { 975 measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed); 976 } 977 heightUsed += child.getMeasuredHeight(); 978 } 979 } 980 981 final int oldCollapsibleHeight = mCollapsibleHeight; 982 mCollapsibleHeight = Math.max(0, 983 heightUsed - mAlwaysShowHeight - getMaxCollapsedHeight()); 984 mUncollapsibleHeight = heightUsed - mCollapsibleHeight; 985 986 updateCollapseOffset(oldCollapsibleHeight, !isDragging()); 987 988 if (getShowAtTop()) { 989 mTopOffset = 0; 990 } else { 991 mTopOffset = Math.max(0, heightSize - heightUsed) + (int) mCollapseOffset; 992 } 993 994 setMeasuredDimension(sourceWidth, heightSize); 995 } 996 997 /** 998 * @return The space reserved by views with 'alwaysShow=true' 999 */ 1000 public int getAlwaysShowHeight() { 1001 return mAlwaysShowHeight; 1002 } 1003 1004 @Override 1005 protected void onLayout(boolean changed, int l, int t, int r, int b) { 1006 final int width = getWidth(); 1007 1008 View indicatorHost = null; 1009 1010 int ypos = mTopOffset; 1011 int leftEdge = getPaddingLeft(); 1012 int rightEdge = width - getPaddingRight(); 1013 1014 final int childCount = getChildCount(); 1015 for (int i = 0; i < childCount; i++) { 1016 final View child = getChildAt(i); 1017 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1018 if (lp.hasNestedScrollIndicator) { 1019 indicatorHost = child; 1020 } 1021 1022 if (child.getVisibility() == GONE) { 1023 continue; 1024 } 1025 1026 int top = ypos + lp.topMargin; 1027 if (lp.ignoreOffset) { 1028 top -= mCollapseOffset; 1029 } 1030 final int bottom = top + child.getMeasuredHeight(); 1031 1032 final int childWidth = child.getMeasuredWidth(); 1033 final int widthAvailable = rightEdge - leftEdge; 1034 final int left = leftEdge + (widthAvailable - childWidth) / 2; 1035 final int right = left + childWidth; 1036 1037 child.layout(left, top, right, bottom); 1038 1039 ypos = bottom + lp.bottomMargin; 1040 } 1041 1042 if (mScrollIndicatorDrawable != null) { 1043 if (indicatorHost != null) { 1044 final int left = indicatorHost.getLeft(); 1045 final int right = indicatorHost.getRight(); 1046 final int bottom = indicatorHost.getTop(); 1047 final int top = bottom - mScrollIndicatorDrawable.getIntrinsicHeight(); 1048 mScrollIndicatorDrawable.setBounds(left, top, right, bottom); 1049 setWillNotDraw(!isCollapsed()); 1050 } else { 1051 mScrollIndicatorDrawable = null; 1052 setWillNotDraw(true); 1053 } 1054 } 1055 } 1056 1057 @Override 1058 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 1059 return new LayoutParams(getContext(), attrs); 1060 } 1061 1062 @Override 1063 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 1064 if (p instanceof LayoutParams) { 1065 return new LayoutParams((LayoutParams) p); 1066 } else if (p instanceof MarginLayoutParams) { 1067 return new LayoutParams((MarginLayoutParams) p); 1068 } 1069 return new LayoutParams(p); 1070 } 1071 1072 @Override 1073 protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 1074 return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); 1075 } 1076 1077 @Override 1078 protected Parcelable onSaveInstanceState() { 1079 final SavedState ss = new SavedState(super.onSaveInstanceState()); 1080 ss.open = mCollapsibleHeight > 0 && mCollapseOffset == 0; 1081 ss.mCollapsibleHeightReserved = mCollapsibleHeightReserved; 1082 return ss; 1083 } 1084 1085 @Override 1086 protected void onRestoreInstanceState(Parcelable state) { 1087 final SavedState ss = (SavedState) state; 1088 super.onRestoreInstanceState(ss.getSuperState()); 1089 mOpenOnLayout = ss.open; 1090 mCollapsibleHeightReserved = ss.mCollapsibleHeightReserved; 1091 } 1092 1093 public static class LayoutParams extends MarginLayoutParams { 1094 public boolean alwaysShow; 1095 public boolean ignoreOffset; 1096 public boolean hasNestedScrollIndicator; 1097 public int maxHeight; 1098 1099 public LayoutParams(Context c, AttributeSet attrs) { 1100 super(c, attrs); 1101 1102 final TypedArray a = c.obtainStyledAttributes(attrs, 1103 R.styleable.ResolverDrawerLayout_LayoutParams); 1104 alwaysShow = a.getBoolean( 1105 R.styleable.ResolverDrawerLayout_LayoutParams_layout_alwaysShow, 1106 false); 1107 ignoreOffset = a.getBoolean( 1108 R.styleable.ResolverDrawerLayout_LayoutParams_layout_ignoreOffset, 1109 false); 1110 hasNestedScrollIndicator = a.getBoolean( 1111 R.styleable.ResolverDrawerLayout_LayoutParams_layout_hasNestedScrollIndicator, 1112 false); 1113 maxHeight = a.getDimensionPixelSize( 1114 R.styleable.ResolverDrawerLayout_LayoutParams_layout_maxHeight, -1); 1115 a.recycle(); 1116 } 1117 1118 public LayoutParams(int width, int height) { 1119 super(width, height); 1120 } 1121 1122 public LayoutParams(LayoutParams source) { 1123 super(source); 1124 this.alwaysShow = source.alwaysShow; 1125 this.ignoreOffset = source.ignoreOffset; 1126 this.hasNestedScrollIndicator = source.hasNestedScrollIndicator; 1127 this.maxHeight = source.maxHeight; 1128 } 1129 1130 public LayoutParams(MarginLayoutParams source) { 1131 super(source); 1132 } 1133 1134 public LayoutParams(ViewGroup.LayoutParams source) { 1135 super(source); 1136 } 1137 } 1138 1139 static class SavedState extends BaseSavedState { 1140 boolean open; 1141 private int mCollapsibleHeightReserved; 1142 1143 SavedState(Parcelable superState) { 1144 super(superState); 1145 } 1146 1147 private SavedState(Parcel in) { 1148 super(in); 1149 open = in.readInt() != 0; 1150 mCollapsibleHeightReserved = in.readInt(); 1151 } 1152 1153 @Override 1154 public void writeToParcel(Parcel out, int flags) { 1155 super.writeToParcel(out, flags); 1156 out.writeInt(open ? 1 : 0); 1157 out.writeInt(mCollapsibleHeightReserved); 1158 } 1159 1160 public static final Parcelable.Creator<SavedState> CREATOR = 1161 new Parcelable.Creator<SavedState>() { 1162 @Override 1163 public SavedState createFromParcel(Parcel in) { 1164 return new SavedState(in); 1165 } 1166 1167 @Override 1168 public SavedState[] newArray(int size) { 1169 return new SavedState[size]; 1170 } 1171 }; 1172 } 1173 1174 /** 1175 * Listener for sheet dismissed events. 1176 */ 1177 public interface OnDismissedListener { 1178 /** 1179 * Callback when the sheet is dismissed by the user. 1180 */ 1181 void onDismissed(); 1182 } 1183 1184 /** 1185 * Listener for sheet collapsed / expanded events. 1186 */ 1187 public interface OnCollapsedChangedListener { 1188 /** 1189 * Callback when the sheet is either fully expanded or collapsed. 1190 * @param isCollapsed true when collapsed, false when expanded. 1191 */ 1192 void onCollapsedChanged(boolean isCollapsed); 1193 } 1194 1195 private class RunOnDismissedListener implements Runnable { 1196 @Override 1197 public void run() { 1198 dispatchOnDismissed(); 1199 } 1200 } 1201 1202 private MetricsLogger getMetricsLogger() { 1203 if (mMetricsLogger == null) { 1204 mMetricsLogger = new MetricsLogger(); 1205 } 1206 return mMetricsLogger; 1207 } 1208 } 1209