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