ResolverDrawerLayout.java revision 4f6c2050a847f4089330b4b0aa4d1deb173e5bd0
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.util.AttributeSet; 24import android.util.Log; 25import android.view.Gravity; 26import android.view.MotionEvent; 27import android.view.VelocityTracker; 28import android.view.View; 29import android.view.ViewConfiguration; 30import android.view.ViewGroup; 31 32import android.view.ViewParent; 33import android.view.ViewTreeObserver; 34import android.view.animation.AnimationUtils; 35import android.widget.AbsListView; 36import android.widget.OverScroller; 37import com.android.internal.R; 38 39public class ResolverDrawerLayout extends ViewGroup { 40 private static final String TAG = "ResolverDrawerLayout"; 41 42 /** 43 * Max width of the whole drawer layout 44 */ 45 private int mMaxWidth; 46 47 /** 48 * Max total visible height of views not marked always-show when in the closed/initial state 49 */ 50 private int mMaxCollapsedHeight; 51 52 /** 53 * Max total visible height of views not marked always-show when in the closed/initial state 54 * when a default option is present 55 */ 56 private int mMaxCollapsedHeightSmall; 57 58 private boolean mSmallCollapsed; 59 60 /** 61 * Move views down from the top by this much in px 62 */ 63 private float mCollapseOffset; 64 65 private int mCollapsibleHeight; 66 67 private int mTopOffset; 68 69 private boolean mIsDragging; 70 private boolean mOpenOnClick; 71 private final int mTouchSlop; 72 private final float mMinFlingVelocity; 73 private final OverScroller mScroller; 74 private final VelocityTracker mVelocityTracker; 75 76 private OnClickListener mClickOutsideListener; 77 private float mInitialTouchX; 78 private float mInitialTouchY; 79 private float mLastTouchY; 80 private int mActivePointerId = MotionEvent.INVALID_POINTER_ID; 81 82 private final Rect mTempRect = new Rect(); 83 84 private final ViewTreeObserver.OnTouchModeChangeListener mTouchModeChangeListener = 85 new ViewTreeObserver.OnTouchModeChangeListener() { 86 @Override 87 public void onTouchModeChanged(boolean isInTouchMode) { 88 if (!isInTouchMode && hasFocus() && isDescendantClipped(getFocusedChild())) { 89 smoothScrollTo(0, 0); 90 } 91 } 92 }; 93 94 public ResolverDrawerLayout(Context context) { 95 this(context, null); 96 } 97 98 public ResolverDrawerLayout(Context context, AttributeSet attrs) { 99 this(context, attrs, 0); 100 } 101 102 public ResolverDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) { 103 super(context, attrs, defStyleAttr); 104 105 final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ResolverDrawerLayout, 106 defStyleAttr, 0); 107 mMaxWidth = a.getDimensionPixelSize(R.styleable.ResolverDrawerLayout_maxWidth, -1); 108 mMaxCollapsedHeight = a.getDimensionPixelSize( 109 R.styleable.ResolverDrawerLayout_maxCollapsedHeight, 0); 110 mMaxCollapsedHeightSmall = a.getDimensionPixelSize( 111 R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall, 112 mMaxCollapsedHeight); 113 a.recycle(); 114 115 mScroller = new OverScroller(context, AnimationUtils.loadInterpolator(context, 116 android.R.interpolator.decelerate_quint)); 117 mVelocityTracker = VelocityTracker.obtain(); 118 119 final ViewConfiguration vc = ViewConfiguration.get(context); 120 mTouchSlop = vc.getScaledTouchSlop(); 121 mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); 122 } 123 124 public void setSmallCollapsed(boolean smallCollapsed) { 125 mSmallCollapsed = smallCollapsed; 126 requestLayout(); 127 } 128 129 public boolean isSmallCollapsed() { 130 return mSmallCollapsed; 131 } 132 133 public boolean isCollapsed() { 134 return mCollapseOffset > 0; 135 } 136 137 private boolean isMoving() { 138 return mIsDragging || !mScroller.isFinished(); 139 } 140 141 private int getMaxCollapsedHeight() { 142 return isSmallCollapsed() ? mMaxCollapsedHeightSmall : mMaxCollapsedHeight; 143 } 144 145 public void setOnClickOutsideListener(OnClickListener listener) { 146 mClickOutsideListener = listener; 147 } 148 149 @Override 150 public boolean onInterceptTouchEvent(MotionEvent ev) { 151 final int action = ev.getActionMasked(); 152 153 if (action == MotionEvent.ACTION_DOWN) { 154 mVelocityTracker.clear(); 155 } 156 157 mVelocityTracker.addMovement(ev); 158 159 switch (action) { 160 case MotionEvent.ACTION_DOWN: { 161 final float x = ev.getX(); 162 final float y = ev.getY(); 163 mInitialTouchX = x; 164 mInitialTouchY = mLastTouchY = y; 165 mOpenOnClick = isListChildUnderClipped(x, y) && mCollapsibleHeight > 0; 166 } 167 break; 168 169 case MotionEvent.ACTION_MOVE: { 170 final float x = ev.getX(); 171 final float y = ev.getY(); 172 final float dy = y - mInitialTouchY; 173 if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null && 174 (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) { 175 mActivePointerId = ev.getPointerId(0); 176 mIsDragging = true; 177 mLastTouchY = Math.max(mLastTouchY - mTouchSlop, 178 Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop)); 179 } 180 } 181 break; 182 183 case MotionEvent.ACTION_POINTER_UP: { 184 onSecondaryPointerUp(ev); 185 } 186 break; 187 188 case MotionEvent.ACTION_CANCEL: 189 case MotionEvent.ACTION_UP: { 190 resetTouch(); 191 } 192 break; 193 } 194 195 if (mIsDragging) { 196 mScroller.abortAnimation(); 197 } 198 return mIsDragging || mOpenOnClick; 199 } 200 201 @Override 202 public boolean onTouchEvent(MotionEvent ev) { 203 final int action = ev.getActionMasked(); 204 205 boolean handled = false; 206 switch (action) { 207 case MotionEvent.ACTION_DOWN: { 208 final float x = ev.getX(); 209 final float y = ev.getY(); 210 mInitialTouchX = x; 211 mInitialTouchY = mLastTouchY = y; 212 mActivePointerId = ev.getPointerId(0); 213 if (findChildUnder(mInitialTouchX, mInitialTouchY) == null && 214 mClickOutsideListener != null) { 215 mIsDragging = handled = true; 216 } 217 handled |= mCollapsibleHeight > 0; 218 mScroller.abortAnimation(); 219 } 220 break; 221 222 case MotionEvent.ACTION_MOVE: { 223 int index = ev.findPointerIndex(mActivePointerId); 224 if (index < 0) { 225 Log.e(TAG, "Bad pointer id " + mActivePointerId + ", resetting"); 226 index = 0; 227 mActivePointerId = ev.getPointerId(0); 228 mInitialTouchX = ev.getX(); 229 mInitialTouchY = mLastTouchY = ev.getY(); 230 } 231 final float x = ev.getX(index); 232 final float y = ev.getY(index); 233 if (!mIsDragging) { 234 final float dy = y - mInitialTouchY; 235 if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null) { 236 handled = mIsDragging = true; 237 mLastTouchY = Math.max(mLastTouchY - mTouchSlop, 238 Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop)); 239 } 240 } 241 if (mIsDragging) { 242 final float dy = y - mLastTouchY; 243 performDrag(dy); 244 } 245 mLastTouchY = y; 246 } 247 break; 248 249 case MotionEvent.ACTION_POINTER_DOWN: { 250 final int pointerIndex = ev.getActionIndex(); 251 final int pointerId = ev.getPointerId(pointerIndex); 252 mActivePointerId = pointerId; 253 mInitialTouchX = ev.getX(pointerIndex); 254 mInitialTouchY = mLastTouchY = ev.getY(pointerIndex); 255 } 256 break; 257 258 case MotionEvent.ACTION_POINTER_UP: { 259 onSecondaryPointerUp(ev); 260 } 261 break; 262 263 case MotionEvent.ACTION_UP: { 264 mIsDragging = false; 265 if (!mIsDragging && findChildUnder(mInitialTouchX, mInitialTouchY) == null && 266 findChildUnder(ev.getX(), ev.getY()) == null) { 267 if (mClickOutsideListener != null) { 268 mClickOutsideListener.onClick(this); 269 resetTouch(); 270 return true; 271 } 272 } 273 if (mOpenOnClick && Math.abs(ev.getX() - mInitialTouchX) < mTouchSlop && 274 Math.abs(ev.getY() - mInitialTouchY) < mTouchSlop) { 275 smoothScrollTo(0, 0); 276 return true; 277 } 278 mVelocityTracker.computeCurrentVelocity(1000); 279 final float yvel = mVelocityTracker.getYVelocity(mActivePointerId); 280 if (Math.abs(yvel) > mMinFlingVelocity) { 281 smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel); 282 } else { 283 smoothScrollTo( 284 mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0); 285 } 286 resetTouch(); 287 } 288 break; 289 290 case MotionEvent.ACTION_CANCEL: { 291 resetTouch(); 292 return true; 293 } 294 } 295 296 return handled; 297 } 298 299 private void onSecondaryPointerUp(MotionEvent ev) { 300 final int pointerIndex = ev.getActionIndex(); 301 final int pointerId = ev.getPointerId(pointerIndex); 302 if (pointerId == mActivePointerId) { 303 // This was our active pointer going up. Choose a new 304 // active pointer and adjust accordingly. 305 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 306 mInitialTouchX = ev.getX(newPointerIndex); 307 mInitialTouchY = mLastTouchY = ev.getY(newPointerIndex); 308 mActivePointerId = ev.getPointerId(newPointerIndex); 309 } 310 } 311 312 private void resetTouch() { 313 mActivePointerId = MotionEvent.INVALID_POINTER_ID; 314 mIsDragging = false; 315 mOpenOnClick = false; 316 mInitialTouchX = mInitialTouchY = mLastTouchY = 0; 317 mVelocityTracker.clear(); 318 } 319 320 @Override 321 public void computeScroll() { 322 super.computeScroll(); 323 if (!mScroller.isFinished()) { 324 final boolean keepGoing = mScroller.computeScrollOffset(); 325 performDrag(mScroller.getCurrY() - mCollapseOffset); 326 if (keepGoing) { 327 postInvalidateOnAnimation(); 328 } 329 } 330 } 331 332 private float performDrag(float dy) { 333 final float newPos = Math.max(0, Math.min(mCollapseOffset + dy, mCollapsibleHeight)); 334 if (newPos != mCollapseOffset) { 335 dy = newPos - mCollapseOffset; 336 final int childCount = getChildCount(); 337 for (int i = 0; i < childCount; i++) { 338 final View child = getChildAt(i); 339 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 340 if (!lp.ignoreOffset) { 341 child.offsetTopAndBottom((int) dy); 342 } 343 } 344 mCollapseOffset = newPos; 345 mTopOffset += dy; 346 postInvalidateOnAnimation(); 347 return dy; 348 } 349 return 0; 350 } 351 352 private void smoothScrollTo(int yOffset, float velocity) { 353 if (getMaxCollapsedHeight() == 0) { 354 return; 355 } 356 mScroller.abortAnimation(); 357 final int sy = (int) mCollapseOffset; 358 int dy = yOffset - sy; 359 if (dy == 0) { 360 return; 361 } 362 363 final int height = getHeight(); 364 final int halfHeight = height / 2; 365 final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / height); 366 final float distance = halfHeight + halfHeight * 367 distanceInfluenceForSnapDuration(distanceRatio); 368 369 int duration = 0; 370 velocity = Math.abs(velocity); 371 if (velocity > 0) { 372 duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 373 } else { 374 final float pageDelta = (float) Math.abs(dy) / height; 375 duration = (int) ((pageDelta + 1) * 100); 376 } 377 duration = Math.min(duration, 300); 378 379 mScroller.startScroll(0, sy, 0, dy, duration); 380 postInvalidateOnAnimation(); 381 } 382 383 private float distanceInfluenceForSnapDuration(float f) { 384 f -= 0.5f; // center the values about 0. 385 f *= 0.3f * Math.PI / 2.0f; 386 return (float) Math.sin(f); 387 } 388 389 /** 390 * Note: this method doesn't take Z into account for overlapping views 391 * since it is only used in contexts where this doesn't affect the outcome. 392 */ 393 private View findChildUnder(float x, float y) { 394 return findChildUnder(this, x, y); 395 } 396 397 private static View findChildUnder(ViewGroup parent, float x, float y) { 398 final int childCount = parent.getChildCount(); 399 for (int i = childCount - 1; i >= 0; i--) { 400 final View child = parent.getChildAt(i); 401 if (isChildUnder(child, x, y)) { 402 return child; 403 } 404 } 405 return null; 406 } 407 408 private View findListChildUnder(float x, float y) { 409 View v = findChildUnder(x, y); 410 while (v != null) { 411 x -= v.getX(); 412 y -= v.getY(); 413 if (v instanceof AbsListView) { 414 // One more after this. 415 return findChildUnder((ViewGroup) v, x, y); 416 } 417 v = v instanceof ViewGroup ? findChildUnder((ViewGroup) v, x, y) : null; 418 } 419 return v; 420 } 421 422 /** 423 * This only checks clipping along the bottom edge. 424 */ 425 private boolean isListChildUnderClipped(float x, float y) { 426 final View listChild = findListChildUnder(x, y); 427 return listChild != null && isDescendantClipped(listChild); 428 } 429 430 private boolean isDescendantClipped(View child) { 431 mTempRect.set(0, 0, child.getWidth(), child.getHeight()); 432 offsetDescendantRectToMyCoords(child, mTempRect); 433 View directChild; 434 if (child.getParent() == this) { 435 directChild = child; 436 } else { 437 View v = child; 438 ViewParent p = child.getParent(); 439 while (p != this) { 440 v = (View) p; 441 p = v.getParent(); 442 } 443 directChild = v; 444 } 445 446 // ResolverDrawerLayout lays out vertically in child order; 447 // the next view and forward is what to check against. 448 int clipEdge = getHeight() - getPaddingBottom(); 449 final int childCount = getChildCount(); 450 for (int i = indexOfChild(directChild) + 1; i < childCount; i++) { 451 final View nextChild = getChildAt(i); 452 if (nextChild.getVisibility() == GONE) { 453 continue; 454 } 455 clipEdge = Math.min(clipEdge, nextChild.getTop()); 456 } 457 return mTempRect.bottom > clipEdge; 458 } 459 460 private static boolean isChildUnder(View child, float x, float y) { 461 final float left = child.getX(); 462 final float top = child.getY(); 463 final float right = left + child.getWidth(); 464 final float bottom = top + child.getHeight(); 465 return x >= left && y >= top && x < right && y < bottom; 466 } 467 468 @Override 469 public void requestChildFocus(View child, View focused) { 470 super.requestChildFocus(child, focused); 471 if (!isInTouchMode() && isDescendantClipped(focused)) { 472 smoothScrollTo(0, 0); 473 } 474 } 475 476 @Override 477 protected void onAttachedToWindow() { 478 super.onAttachedToWindow(); 479 getViewTreeObserver().addOnTouchModeChangeListener(mTouchModeChangeListener); 480 } 481 482 @Override 483 protected void onDetachedFromWindow() { 484 super.onDetachedFromWindow(); 485 getViewTreeObserver().removeOnTouchModeChangeListener(mTouchModeChangeListener); 486 } 487 488 @Override 489 public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { 490 return (nestedScrollAxes & View.SCROLL_AXIS_VERTICAL) != 0; 491 } 492 493 @Override 494 public void onNestedScrollAccepted(View child, View target, int axes) { 495 super.onNestedScrollAccepted(child, target, axes); 496 } 497 498 @Override 499 public void onStopNestedScroll(View child) { 500 super.onStopNestedScroll(child); 501 smoothScrollTo(mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0); 502 } 503 504 @Override 505 public void onNestedScroll(View target, int dxConsumed, int dyConsumed, 506 int dxUnconsumed, int dyUnconsumed) { 507 if (dyUnconsumed > 0) { 508 performDrag(-dyUnconsumed); 509 } 510 } 511 512 @Override 513 public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { 514 if (dy < 0) { 515 consumed[1] = (int) performDrag(-dy); 516 } 517 } 518 519 @Override 520 public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { 521 if (!consumed && Math.abs(velocityY) > mMinFlingVelocity) { 522 smoothScrollTo(velocityY < 0 ? 0 : mCollapsibleHeight, velocityY); 523 return true; 524 } 525 return false; 526 } 527 528 @Override 529 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 530 final int sourceWidth = MeasureSpec.getSize(widthMeasureSpec); 531 int widthSize = sourceWidth; 532 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 533 534 // Single-use layout; just ignore the mode and use available space. 535 // Clamp to maxWidth. 536 if (mMaxWidth >= 0) { 537 widthSize = Math.min(widthSize, mMaxWidth); 538 } 539 540 final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); 541 final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY); 542 final int widthPadding = getPaddingLeft() + getPaddingRight(); 543 int heightUsed = getPaddingTop() + getPaddingBottom(); 544 545 // Measure always-show children first. 546 final int childCount = getChildCount(); 547 for (int i = 0; i < childCount; i++) { 548 final View child = getChildAt(i); 549 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 550 if (lp.alwaysShow && child.getVisibility() != GONE) { 551 measureChildWithMargins(child, widthSpec, widthPadding, heightSpec, heightUsed); 552 heightUsed += lp.topMargin + child.getMeasuredHeight() + lp.bottomMargin; 553 } 554 } 555 556 final int alwaysShowHeight = heightUsed; 557 558 // And now the rest. 559 for (int i = 0; i < childCount; i++) { 560 final View child = getChildAt(i); 561 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 562 if (!lp.alwaysShow && child.getVisibility() != GONE) { 563 measureChildWithMargins(child, widthSpec, widthPadding, heightSpec, heightUsed); 564 heightUsed += lp.topMargin + child.getMeasuredHeight() + lp.bottomMargin; 565 } 566 } 567 568 mCollapsibleHeight = Math.max(0, 569 heightUsed - alwaysShowHeight - getMaxCollapsedHeight()); 570 571 if (isLaidOut()) { 572 mCollapseOffset = Math.min(mCollapseOffset, mCollapsibleHeight); 573 } else { 574 // Start out collapsed at first 575 mCollapseOffset = mCollapsibleHeight; 576 } 577 578 mTopOffset = Math.max(0, heightSize - heightUsed) + (int) mCollapseOffset; 579 580 setMeasuredDimension(sourceWidth, heightSize); 581 } 582 583 @Override 584 protected void onLayout(boolean changed, int l, int t, int r, int b) { 585 final int width = getWidth(); 586 587 int ypos = mTopOffset; 588 int leftEdge = getPaddingLeft(); 589 int rightEdge = width - getPaddingRight(); 590 591 final int childCount = getChildCount(); 592 for (int i = 0; i < childCount; i++) { 593 final View child = getChildAt(i); 594 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 595 596 if (child.getVisibility() == GONE) { 597 continue; 598 } 599 600 int top = ypos + lp.topMargin; 601 if (lp.ignoreOffset) { 602 top -= mCollapseOffset; 603 } 604 final int bottom = top + child.getMeasuredHeight(); 605 606 final int childWidth = child.getMeasuredWidth(); 607 final int widthAvailable = rightEdge - leftEdge; 608 final int left = leftEdge + (widthAvailable - childWidth) / 2; 609 final int right = left + childWidth; 610 611 child.layout(left, top, right, bottom); 612 613 ypos = bottom + lp.bottomMargin; 614 } 615 } 616 617 @Override 618 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 619 return new LayoutParams(getContext(), attrs); 620 } 621 622 @Override 623 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 624 if (p instanceof LayoutParams) { 625 return new LayoutParams((LayoutParams) p); 626 } else if (p instanceof MarginLayoutParams) { 627 return new LayoutParams((MarginLayoutParams) p); 628 } 629 return new LayoutParams(p); 630 } 631 632 @Override 633 protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 634 return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); 635 } 636 637 public static class LayoutParams extends MarginLayoutParams { 638 public boolean alwaysShow; 639 public boolean ignoreOffset; 640 641 public LayoutParams(Context c, AttributeSet attrs) { 642 super(c, attrs); 643 644 final TypedArray a = c.obtainStyledAttributes(attrs, 645 R.styleable.ResolverDrawerLayout_LayoutParams); 646 alwaysShow = a.getBoolean( 647 R.styleable.ResolverDrawerLayout_LayoutParams_layout_alwaysShow, 648 false); 649 ignoreOffset = a.getBoolean( 650 R.styleable.ResolverDrawerLayout_LayoutParams_layout_ignoreOffset, 651 false); 652 a.recycle(); 653 } 654 655 public LayoutParams(int width, int height) { 656 super(width, height); 657 } 658 659 public LayoutParams(LayoutParams source) { 660 super(source); 661 this.alwaysShow = source.alwaysShow; 662 this.ignoreOffset = source.ignoreOffset; 663 } 664 665 public LayoutParams(MarginLayoutParams source) { 666 super(source); 667 } 668 669 public LayoutParams(ViewGroup.LayoutParams source) { 670 super(source); 671 } 672 } 673} 674