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