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