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