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