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