SlidingChallengeLayout.java revision c606bf1af953bb8a682589a29809e714c648c291
1/* 2 * Copyright (C) 2012 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 17package com.android.internal.policy.impl.keyguard; 18 19import android.animation.ObjectAnimator; 20import android.content.Context; 21import android.content.res.TypedArray; 22import android.graphics.Canvas; 23import android.graphics.Paint; 24import android.graphics.RectF; 25import android.graphics.drawable.Drawable; 26import android.util.AttributeSet; 27import android.util.FloatProperty; 28import android.util.Log; 29import android.util.Property; 30import android.view.MotionEvent; 31import android.view.VelocityTracker; 32import android.view.View; 33import android.view.ViewConfiguration; 34import android.view.ViewGroup; 35import android.view.animation.Interpolator; 36import android.widget.Scroller; 37 38import com.android.internal.R; 39 40/** 41 * This layout handles interaction with the sliding security challenge views 42 * that overlay/resize other keyguard contents. 43 */ 44public class SlidingChallengeLayout extends ViewGroup implements ChallengeLayout { 45 private static final String TAG = "SlidingChallengeLayout"; 46 private static final boolean DEBUG = false; 47 48 // The drag handle is measured in dp above & below the top edge of the 49 // challenge view; these parameters change based on whether the challenge 50 // is open or closed. 51 private static final int DRAG_HANDLE_CLOSED_ABOVE = 32; // dp 52 private static final int DRAG_HANDLE_CLOSED_BELOW = 32; // dp 53 private static final int DRAG_HANDLE_OPEN_ABOVE = 8; // dp 54 private static final int DRAG_HANDLE_OPEN_BELOW = 0; // dp 55 56 private static final boolean OPEN_ON_CLICK = true; 57 58 private static final int HANDLE_ANIMATE_DURATION = 200; // ms 59 60 // Drawn to show the drag handle in closed state; crossfades to the challenge view 61 // when challenge is fully visible 62 private Drawable mHandleDrawable; 63 private Drawable mFrameDrawable; 64 private Drawable mDragIconDrawable; 65 66 // Initialized during measurement from child layoutparams 67 private View mChallengeView; 68 private View mScrimView; 69 70 // Range: 0 (fully hidden) to 1 (fully visible) 71 private float mChallengeOffset = 1.f; 72 private boolean mChallengeShowing = true; 73 private boolean mIsBouncing = false; 74 75 private final Scroller mScroller; 76 private int mScrollState; 77 private OnChallengeScrolledListener mScrollListener; 78 private OnBouncerStateChangedListener mBouncerListener; 79 80 public static final int SCROLL_STATE_IDLE = 0; 81 public static final int SCROLL_STATE_DRAGGING = 1; 82 public static final int SCROLL_STATE_SETTLING = 2; 83 84 private static final int MAX_SETTLE_DURATION = 600; // ms 85 86 // ID of the pointer in charge of a current drag 87 private int mActivePointerId = INVALID_POINTER; 88 private static final int INVALID_POINTER = -1; 89 90 // True if the user is currently dragging the slider 91 private boolean mDragging; 92 // True if the user may not drag until a new gesture begins 93 private boolean mBlockDrag; 94 95 private VelocityTracker mVelocityTracker; 96 private int mMinVelocity; 97 private int mMaxVelocity; 98 private float mGestureStartX, mGestureStartY; // where did you first touch the screen? 99 private int mGestureStartChallengeBottom; // where was the challenge at that time? 100 101 private int mDragHandleClosedBelow; // handle hitrect extension into the challenge view 102 private int mDragHandleClosedAbove; // extend the handle's hitrect this far above the line 103 private int mDragHandleOpenBelow; // handle hitrect extension into the challenge view 104 private int mDragHandleOpenAbove; // extend the handle's hitrect this far above the line 105 106 private int mDragHandleEdgeSlop; 107 private int mChallengeBottomBound; // Number of pixels from the top of the challenge view 108 // that should remain on-screen 109 110 private int mTouchSlop; 111 112 float mHandleAlpha; 113 float mFrameAlpha; 114 private ObjectAnimator mHandleAnimation; 115 private ObjectAnimator mFrameAnimation; 116 117 static final Property<SlidingChallengeLayout, Float> HANDLE_ALPHA = 118 new FloatProperty<SlidingChallengeLayout>("handleAlpha") { 119 @Override 120 public void setValue(SlidingChallengeLayout view, float value) { 121 view.mHandleAlpha = value; 122 view.invalidate(); 123 } 124 125 @Override 126 public Float get(SlidingChallengeLayout view) { 127 return view.mHandleAlpha; 128 } 129 }; 130 131 static final Property<SlidingChallengeLayout, Float> FRAME_ALPHA = 132 new FloatProperty<SlidingChallengeLayout>("frameAlpha") { 133 @Override 134 public void setValue(SlidingChallengeLayout view, float value) { 135 if (view.mFrameDrawable != null) { 136 view.mFrameAlpha = value; 137 view.mFrameDrawable.setAlpha((int) (value * 0xFF)); 138 view.mFrameDrawable.invalidateSelf(); 139 } 140 } 141 142 @Override 143 public Float get(SlidingChallengeLayout view) { 144 return view.mFrameAlpha; 145 } 146 }; 147 148 // True if at least one layout pass has happened since the view was attached. 149 private boolean mHasLayout; 150 151 private static final Interpolator sMotionInterpolator = new Interpolator() { 152 public float getInterpolation(float t) { 153 t -= 1.0f; 154 return t * t * t * t * t + 1.0f; 155 } 156 }; 157 158 private static final Interpolator sHandleFadeInterpolator = new Interpolator() { 159 public float getInterpolation(float t) { 160 return t * t; 161 } 162 }; 163 164 private final Runnable mEndScrollRunnable = new Runnable () { 165 public void run() { 166 completeChallengeScroll(); 167 } 168 }; 169 170 private final OnClickListener mScrimClickListener = new OnClickListener() { 171 @Override 172 public void onClick(View v) { 173 hideBouncer(); 174 } 175 }; 176 177 /** 178 * Listener interface that reports changes in scroll state of the challenge area. 179 */ 180 public interface OnChallengeScrolledListener { 181 /** 182 * The scroll state itself changed. 183 * 184 * <p>scrollState will be one of the following:</p> 185 * 186 * <ul> 187 * <li><code>SCROLL_STATE_IDLE</code> - The challenge area is stationary.</li> 188 * <li><code>SCROLL_STATE_DRAGGING</code> - The user is actively dragging 189 * the challenge area.</li> 190 * <li><code>SCROLL_STATE_SETTLING</code> - The challenge area is animating 191 * into place.</li> 192 * </ul> 193 * 194 * <p>Do not perform expensive operations (e.g. layout) 195 * while the scroll state is not <code>SCROLL_STATE_IDLE</code>.</p> 196 * 197 * @param scrollState The new scroll state of the challenge area. 198 */ 199 public void onScrollStateChanged(int scrollState); 200 201 /** 202 * The precise position of the challenge area has changed. 203 * 204 * <p>NOTE: It is NOT safe to modify layout or call any View methods that may 205 * result in a requestLayout anywhere in your view hierarchy as a result of this call. 206 * It may be called during drawing.</p> 207 * 208 * @param scrollPosition New relative position of the challenge area. 209 * 1.f = fully visible/ready to be interacted with. 210 * 0.f = fully invisible/inaccessible to the user. 211 * @param challengeTop Position of the top edge of the challenge view in px in the 212 * SlidingChallengeLayout's coordinate system. 213 */ 214 public void onScrollPositionChanged(float scrollPosition, int challengeTop); 215 } 216 217 public SlidingChallengeLayout(Context context) { 218 this(context, null); 219 } 220 221 public SlidingChallengeLayout(Context context, AttributeSet attrs) { 222 this(context, attrs, 0); 223 } 224 225 public SlidingChallengeLayout(Context context, AttributeSet attrs, int defStyle) { 226 super(context, attrs, defStyle); 227 228 final TypedArray a = context.obtainStyledAttributes(attrs, 229 R.styleable.SlidingChallengeLayout, defStyle, 0); 230 setDragDrawables(a.getDrawable(R.styleable.SlidingChallengeLayout_dragHandle), 231 a.getDrawable(R.styleable.SlidingChallengeLayout_dragIcon)); 232 233 a.recycle(); 234 235 mScroller = new Scroller(context, sMotionInterpolator); 236 237 final ViewConfiguration vc = ViewConfiguration.get(context); 238 mMinVelocity = vc.getScaledMinimumFlingVelocity(); 239 mMaxVelocity = vc.getScaledMaximumFlingVelocity(); 240 241 mDragHandleEdgeSlop = getResources().getDimensionPixelSize( 242 R.dimen.kg_edge_swipe_region_size); 243 244 mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 245 246 setWillNotDraw(false); 247 } 248 249 public void setDragDrawables(Drawable handle, Drawable icon) { 250 final float density = getResources().getDisplayMetrics().density; 251 final int handleHeight = handle != null ? handle.getIntrinsicHeight() : 0; 252 final int iconHeight = icon != null ? icon.getIntrinsicHeight() : 0; 253 254 // top half of the lock icon, plus another 25% to be sure 255 mDragHandleClosedAbove = (int) (DRAG_HANDLE_CLOSED_ABOVE * density + 0.5f); 256 mDragHandleClosedBelow = (int) (DRAG_HANDLE_CLOSED_BELOW * density + 0.5f); 257 mDragHandleOpenAbove = (int) (DRAG_HANDLE_OPEN_ABOVE * density + 0.5f); 258 mDragHandleOpenBelow = (int) (DRAG_HANDLE_OPEN_BELOW * density + 0.5f); 259 260 // how much space to account for in the handle when closed 261 mChallengeBottomBound = 262 (mDragHandleClosedBelow + mDragHandleClosedAbove + handleHeight) / 2; 263 264 mHandleDrawable = handle; 265 mDragIconDrawable = icon; 266 } 267 268 public void setDragIconDrawable(Drawable d) { 269 mDragIconDrawable = d; 270 } 271 272 public void showHandle(boolean visible) { 273 if (visible) { 274 if (mHandleAnimation != null) { 275 mHandleAnimation.cancel(); 276 mHandleAnimation = null; 277 } 278 mHandleAlpha = 1.f; 279 invalidate(); 280 } else { 281 animateHandle(false); 282 } 283 } 284 285 void animateHandle(boolean visible) { 286 if (mHandleAnimation != null) { 287 mHandleAnimation.cancel(); 288 mHandleAnimation = null; 289 } 290 final float targetAlpha = visible ? 1.f : 0.f; 291 if (targetAlpha == mHandleAlpha) { 292 return; 293 } 294 mHandleAnimation = ObjectAnimator.ofFloat(this, HANDLE_ALPHA, targetAlpha); 295 mHandleAnimation.setInterpolator(sHandleFadeInterpolator); 296 mHandleAnimation.setDuration(HANDLE_ANIMATE_DURATION); 297 mHandleAnimation.start(); 298 } 299 300 void animateFrame(boolean visible, boolean full) { 301 if (mFrameDrawable == null) return; 302 303 if (mFrameAnimation != null) { 304 mFrameAnimation.cancel(); 305 mFrameAnimation = null; 306 } 307 final float targetAlpha = visible ? (full ? 1.f : 0.5f) : 0.f; 308 if (targetAlpha == mFrameAlpha) { 309 return; 310 } 311 312 mFrameAnimation = ObjectAnimator.ofFloat(this, FRAME_ALPHA, targetAlpha); 313 mFrameAnimation.setInterpolator(sHandleFadeInterpolator); 314 mFrameAnimation.setDuration(HANDLE_ANIMATE_DURATION); 315 mFrameAnimation.start(); 316 } 317 318 private void sendInitialListenerUpdates() { 319 if (mScrollListener != null) { 320 int challengeTop = mChallengeView != null ? mChallengeView.getTop() : 0; 321 mScrollListener.onScrollPositionChanged(mChallengeOffset, challengeTop); 322 mScrollListener.onScrollStateChanged(mScrollState); 323 } 324 } 325 326 public void setOnChallengeScrolledListener(OnChallengeScrolledListener listener) { 327 mScrollListener = listener; 328 if (mHasLayout) { 329 sendInitialListenerUpdates(); 330 } 331 } 332 333 public void setOnBouncerStateChangedListener(OnBouncerStateChangedListener listener) { 334 mBouncerListener = listener; 335 } 336 337 @Override 338 public void onAttachedToWindow() { 339 super.onAttachedToWindow(); 340 341 mHasLayout = false; 342 } 343 344 @Override 345 public void onDetachedFromWindow() { 346 super.onDetachedFromWindow(); 347 348 removeCallbacks(mEndScrollRunnable); 349 mHasLayout = false; 350 } 351 352 @Override 353 public void requestChildFocus(View child, View focused) { 354 if (mIsBouncing && child != mChallengeView) { 355 // Clear out of the bouncer if the user tries to move focus outside of 356 // the security challenge view. 357 hideBouncer(); 358 } 359 super.requestChildFocus(child, focused); 360 } 361 362 // We want the duration of the page snap animation to be influenced by the distance that 363 // the screen has to travel, however, we don't want this duration to be effected in a 364 // purely linear fashion. Instead, we use this method to moderate the effect that the distance 365 // of travel has on the overall snap duration. 366 float distanceInfluenceForSnapDuration(float f) { 367 f -= 0.5f; // center the values about 0. 368 f *= 0.3f * Math.PI / 2.0f; 369 return (float) Math.sin(f); 370 } 371 372 void setScrollState(int state) { 373 if (mScrollState != state) { 374 mScrollState = state; 375 376 animateHandle(state == SCROLL_STATE_IDLE && !mChallengeShowing); 377 animateFrame(state != SCROLL_STATE_IDLE, false); 378 if (mScrollListener != null) { 379 mScrollListener.onScrollStateChanged(state); 380 } 381 } 382 } 383 384 void completeChallengeScroll() { 385 setChallengeShowing(mChallengeOffset != 0); 386 setScrollState(SCROLL_STATE_IDLE); 387 } 388 389 void setScrimView(View scrim) { 390 if (mScrimView != null) { 391 mScrimView.setOnClickListener(null); 392 } 393 mScrimView = scrim; 394 mScrimView.setVisibility(mIsBouncing ? VISIBLE : GONE); 395 mScrimView.setFocusable(true); 396 mScrimView.setOnClickListener(mScrimClickListener); 397 } 398 399 /** 400 * Animate the bottom edge of the challenge view to the given position. 401 * 402 * @param y desired final position for the bottom edge of the challenge view in px 403 * @param velocity velocity in 404 */ 405 void animateChallengeTo(int y, int velocity) { 406 if (mChallengeView == null) { 407 // Nothing to do. 408 return; 409 } 410 final int sy = mChallengeView.getBottom(); 411 final int dy = y - sy; 412 if (dy == 0) { 413 completeChallengeScroll(); 414 return; 415 } 416 417 setScrollState(SCROLL_STATE_SETTLING); 418 419 final int childHeight = mChallengeView.getHeight(); 420 final int halfHeight = childHeight / 2; 421 final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / childHeight); 422 final float distance = halfHeight + halfHeight * 423 distanceInfluenceForSnapDuration(distanceRatio); 424 425 int duration = 0; 426 velocity = Math.abs(velocity); 427 if (velocity > 0) { 428 duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 429 } else { 430 final float childDelta = (float) Math.abs(dy) / childHeight; 431 duration = (int) ((childDelta + 1) * 100); 432 } 433 duration = Math.min(duration, MAX_SETTLE_DURATION); 434 435 mScroller.startScroll(0, sy, 0, dy, duration); 436 postInvalidateOnAnimation(); 437 } 438 439 private void setChallengeShowing(boolean showChallenge) { 440 mChallengeShowing = showChallenge; 441 } 442 443 /** 444 * @return true if the challenge is at all visible. 445 */ 446 public boolean isChallengeShowing() { 447 return mChallengeShowing; 448 } 449 450 @Override 451 public boolean isChallengeOverlapping() { 452 return mChallengeShowing; 453 } 454 455 @Override 456 public boolean isBouncing() { 457 return mIsBouncing; 458 } 459 460 @Override 461 public void showBouncer() { 462 if (mIsBouncing) return; 463 showChallenge(true); 464 mIsBouncing = true; 465 if (mScrimView != null) { 466 mScrimView.setVisibility(VISIBLE); 467 } 468 animateFrame(true, true); 469 if (mBouncerListener != null) { 470 mBouncerListener.onBouncerStateChanged(true); 471 } 472 } 473 474 @Override 475 public void hideBouncer() { 476 if (!mIsBouncing) return; 477 setChallengeShowing(false); 478 mIsBouncing = false; 479 if (mScrimView != null) { 480 mScrimView.setVisibility(GONE); 481 } 482 animateFrame(false, false); 483 if (mBouncerListener != null) { 484 mBouncerListener.onBouncerStateChanged(false); 485 } 486 } 487 488 @Override 489 public void requestDisallowInterceptTouchEvent(boolean allowIntercept) { 490 // We'll intercept whoever we feel like! ...as long as it isn't a challenge view. 491 // If there are one or more pointers in the challenge view before we take over 492 // touch events, onInterceptTouchEvent will set mBlockDrag. 493 } 494 495 @Override 496 public boolean onInterceptTouchEvent(MotionEvent ev) { 497 if (mVelocityTracker == null) { 498 mVelocityTracker = VelocityTracker.obtain(); 499 } 500 mVelocityTracker.addMovement(ev); 501 502 //Log.v(TAG, "onIntercept: " + ev); 503 504 final int action = ev.getActionMasked(); 505 switch (action) { 506 case MotionEvent.ACTION_DOWN: 507 mGestureStartX = ev.getX(); 508 mGestureStartY = ev.getY(); 509 mBlockDrag = false; 510 break; 511 512 case MotionEvent.ACTION_CANCEL: 513 case MotionEvent.ACTION_UP: 514 resetTouch(); 515 break; 516 517 case MotionEvent.ACTION_MOVE: 518 final int count = ev.getPointerCount(); 519 for (int i = 0; i < count; i++) { 520 final float x = ev.getX(i); 521 final float y = ev.getY(i); 522 523 if (!mIsBouncing && 524 (isInDragHandle(x, y) || crossedDragHandle(x, y, mGestureStartY) || 525 (isInChallengeView(x, y) && mScrollState == SCROLL_STATE_SETTLING)) && 526 mActivePointerId == INVALID_POINTER) { 527 mActivePointerId = ev.getPointerId(i); 528 mGestureStartX = x; 529 mGestureStartY = y; 530 mGestureStartChallengeBottom = getChallengeBottom(); 531 mDragging = true; 532 } else if (isInChallengeView(x, y)) { 533 mBlockDrag = true; 534 } 535 } 536 break; 537 } 538 539 if (mBlockDrag) { 540 mActivePointerId = INVALID_POINTER; 541 mDragging = false; 542 } 543 544 return mDragging; 545 } 546 547 private void resetTouch() { 548 mVelocityTracker.recycle(); 549 mVelocityTracker = null; 550 mActivePointerId = INVALID_POINTER; 551 mDragging = mBlockDrag = false; 552 } 553 554 @Override 555 public boolean onTouchEvent(MotionEvent ev) { 556 if (mVelocityTracker == null) { 557 mVelocityTracker = VelocityTracker.obtain(); 558 } 559 mVelocityTracker.addMovement(ev); 560 561 //Log.v(TAG, "onTouch: " + ev); 562 563 final int action = ev.getActionMasked(); 564 switch (action) { 565 case MotionEvent.ACTION_DOWN: 566 mBlockDrag = false; 567 mGestureStartX = ev.getX(); 568 mGestureStartY = ev.getY(); 569 break; 570 571 case MotionEvent.ACTION_CANCEL: 572 if (mDragging) { 573 showChallenge(0); 574 } 575 resetTouch(); 576 break; 577 578 case MotionEvent.ACTION_POINTER_UP: 579 if (mActivePointerId != ev.getPointerId(ev.getActionIndex())) { 580 break; 581 } 582 case MotionEvent.ACTION_UP: 583 if (OPEN_ON_CLICK 584 && isInDragHandle(mGestureStartX, mGestureStartY) 585 && Math.abs(ev.getX() - mGestureStartX) <= mTouchSlop 586 && Math.abs(ev.getY() - mGestureStartY) <= mTouchSlop) { 587 showChallenge(true); 588 } else if (mDragging) { 589 mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); 590 showChallenge((int) mVelocityTracker.getYVelocity(mActivePointerId)); 591 } 592 resetTouch(); 593 break; 594 595 case MotionEvent.ACTION_MOVE: 596 if (!mDragging && !mBlockDrag && !mIsBouncing) { 597 final int count = ev.getPointerCount(); 598 for (int i = 0; i < count; i++) { 599 final float x = ev.getX(i); 600 final float y = ev.getY(i); 601 602 if ((isInDragHandle(x, y) || crossedDragHandle(x, y, mGestureStartY) || 603 (isInChallengeView(x, y) && mScrollState == SCROLL_STATE_SETTLING)) 604 && mActivePointerId == INVALID_POINTER) { 605 mGestureStartX = x; 606 mGestureStartY = y; 607 mActivePointerId = ev.getPointerId(i); 608 mGestureStartChallengeBottom = getChallengeBottom(); 609 mDragging = true; 610 break; 611 } 612 } 613 } 614 // Not an else; this can be set above. 615 if (mDragging) { 616 // No-op if already in this state, but set it here in case we arrived 617 // at this point from either intercept or the above. 618 setScrollState(SCROLL_STATE_DRAGGING); 619 620 final int index = ev.findPointerIndex(mActivePointerId); 621 if (index < 0) { 622 // Oops, bogus state. We lost some touch events somewhere. 623 // Just drop it with no velocity and let things settle. 624 resetTouch(); 625 showChallenge(0); 626 return true; 627 } 628 final float y = ev.getY(index); 629 final float pos = Math.min(y - mGestureStartY, 630 getLayoutBottom() - mChallengeBottomBound); 631 632 moveChallengeTo(mGestureStartChallengeBottom + (int) pos); 633 } 634 break; 635 } 636 return true; 637 } 638 639 /** 640 * We only want to add additional vertical space to the drag handle when the panel is fully 641 * closed. 642 */ 643 private int getDragHandleSizeAbove() { 644 return isChallengeShowing() ? mDragHandleOpenAbove : mDragHandleClosedAbove; 645 } 646 private int getDragHandleSizeBelow() { 647 return isChallengeShowing() ? mDragHandleOpenBelow : mDragHandleClosedBelow; 648 } 649 650 private boolean isInChallengeView(float x, float y) { 651 if (mChallengeView == null) return false; 652 653 return x >= mChallengeView.getLeft() && y >= mChallengeView.getTop() && 654 x < mChallengeView.getRight() && y < mChallengeView.getBottom(); 655 } 656 657 private boolean isInDragHandle(float x, float y) { 658 if (mChallengeView == null) return false; 659 660 return x >= mDragHandleEdgeSlop && 661 y >= mChallengeView.getTop() - getDragHandleSizeAbove() && 662 x < getWidth() - mDragHandleEdgeSlop && 663 y < mChallengeView.getTop() + getDragHandleSizeBelow(); 664 } 665 666 private boolean crossedDragHandle(float x, float y, float initialY) { 667 final int challengeTop = mChallengeView.getTop(); 668 return x >= 0 && 669 x < getWidth() && 670 initialY < (challengeTop - getDragHandleSizeAbove()) && 671 y > challengeTop + getDragHandleSizeBelow(); 672 } 673 674 @Override 675 protected void onMeasure(int widthSpec, int heightSpec) { 676 if (MeasureSpec.getMode(widthSpec) != MeasureSpec.EXACTLY || 677 MeasureSpec.getMode(heightSpec) != MeasureSpec.EXACTLY) { 678 throw new IllegalArgumentException( 679 "SlidingChallengeLayout must be measured with an exact size"); 680 } 681 682 final int width = MeasureSpec.getSize(widthSpec); 683 final int height = MeasureSpec.getSize(heightSpec); 684 setMeasuredDimension(width, height); 685 686 // Find one and only one challenge view. 687 final View oldChallengeView = mChallengeView; 688 mChallengeView = null; 689 final int count = getChildCount(); 690 for (int i = 0; i < count; i++) { 691 final View child = getChildAt(i); 692 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 693 694 if (lp.childType == LayoutParams.CHILD_TYPE_CHALLENGE) { 695 if (mChallengeView != null) { 696 throw new IllegalStateException( 697 "There may only be one child with layout_isChallenge=\"true\""); 698 } 699 mChallengeView = child; 700 if (mChallengeView != oldChallengeView) { 701 mChallengeView.setVisibility(mChallengeShowing ? VISIBLE : INVISIBLE); 702 } 703 // We're going to play silly games with the frame's background drawable later. 704 mFrameDrawable = mChallengeView.getBackground(); 705 } else if (lp.childType == LayoutParams.CHILD_TYPE_SCRIM) { 706 setScrimView(child); 707 } 708 709 if (child.getVisibility() == GONE) continue; 710 711 measureChildWithMargins(child, widthSpec, 0, heightSpec, 0); 712 } 713 } 714 715 @Override 716 protected void onLayout(boolean changed, int l, int t, int r, int b) { 717 final int paddingLeft = getPaddingLeft(); 718 final int paddingTop = getPaddingTop(); 719 final int paddingRight = getPaddingRight(); 720 final int paddingBottom = getPaddingBottom(); 721 final int width = r - l; 722 final int height = b - t; 723 724 final int count = getChildCount(); 725 for (int i = 0; i < count; i++) { 726 final View child = getChildAt(i); 727 728 if (child.getVisibility() == GONE) continue; 729 730 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 731 732 if (lp.childType == LayoutParams.CHILD_TYPE_CHALLENGE) { 733 // Challenge views pin to the bottom, offset by a portion of their height, 734 // and center horizontally. 735 final int center = (paddingLeft + width - paddingRight) / 2; 736 final int childWidth = child.getMeasuredWidth(); 737 final int childHeight = child.getMeasuredHeight(); 738 final int left = center - childWidth / 2; 739 final int layoutBottom = height - paddingBottom - lp.bottomMargin; 740 // We use the top of the challenge view to position the handle, so 741 // we never want less than the handle size showing at the bottom. 742 final int bottom = layoutBottom + (int) ((childHeight - mChallengeBottomBound) 743 * (1 - mChallengeOffset)); 744 child.setAlpha(mChallengeOffset / 2 + 0.5f); 745 child.layout(left, bottom - childHeight, left + childWidth, bottom); 746 } else { 747 // Non-challenge views lay out from the upper left, layered. 748 child.layout(paddingLeft + lp.leftMargin, 749 paddingTop + lp.topMargin, 750 paddingLeft + child.getMeasuredWidth(), 751 paddingTop + child.getMeasuredHeight()); 752 } 753 } 754 755 if (!mHasLayout) { 756 // We want to trigger the initial listener updates outside of layout pass, 757 // in case the listeners trigger requestLayout(). 758 post(new Runnable() { 759 @Override 760 public void run() { 761 sendInitialListenerUpdates(); 762 } 763 }); 764 if (mFrameDrawable != null) { 765 mFrameDrawable.setAlpha(0); 766 } 767 mHasLayout = true; 768 } 769 } 770 771 public void computeScroll() { 772 super.computeScroll(); 773 774 if (!mScroller.isFinished()) { 775 if (mChallengeView == null) { 776 // Can't scroll if the view is missing. 777 Log.e(TAG, "Challenge view missing in computeScroll"); 778 mScroller.abortAnimation(); 779 return; 780 } 781 782 mScroller.computeScrollOffset(); 783 moveChallengeTo(mScroller.getCurrY()); 784 785 if (mScroller.isFinished()) { 786 post(mEndScrollRunnable); 787 } 788 } 789 } 790 791 @Override 792 public void draw(Canvas c) { 793 super.draw(c); 794 795 final Paint debugPaint; 796 if (DEBUG) { 797 debugPaint = new Paint(); 798 debugPaint.setColor(0x40FF00CC); 799 // show the isInDragHandle() rect 800 c.drawRect(mDragHandleEdgeSlop, 801 mChallengeView.getTop() - getDragHandleSizeAbove(), 802 getWidth() - mDragHandleEdgeSlop, 803 mChallengeView.getTop() + getDragHandleSizeBelow(), 804 debugPaint); 805 } 806 807 if (mChallengeView != null && mHandleAlpha > 0) { 808 final int top = mChallengeView.getTop(); 809 final int handleHeight; 810 final int challengeLeft = mChallengeView.getLeft(); 811 final int challengeRight = mChallengeView.getRight(); 812 if (mHandleDrawable != null) { 813 handleHeight = mHandleDrawable.getIntrinsicHeight(); 814 mHandleDrawable.setBounds(challengeLeft, top, challengeRight, top + handleHeight); 815 mHandleDrawable.setAlpha((int) (mHandleAlpha * 0xFF)); 816 mHandleDrawable.draw(c); 817 } else { 818 handleHeight = 0; 819 } 820 821 if (DEBUG) { 822 // now show the actual drag handle 823 debugPaint.setStyle(Paint.Style.STROKE); 824 debugPaint.setStrokeWidth(1); 825 debugPaint.setColor(0xFF80FF00); 826 c.drawRect(challengeLeft, top, challengeRight, top + handleHeight, debugPaint); 827 } 828 829 if (mDragIconDrawable != null) { 830 final int iconWidth = mDragIconDrawable.getIntrinsicWidth(); 831 final int iconHeight = mDragIconDrawable.getIntrinsicHeight(); 832 final int iconLeft = (challengeLeft + challengeRight - iconWidth) / 2; 833 final int iconTop = top + (handleHeight - iconHeight) / 2; 834 mDragIconDrawable.setBounds(iconLeft, iconTop, iconLeft + iconWidth, 835 iconTop + iconHeight); 836 mDragIconDrawable.setAlpha((int) (mHandleAlpha * 0xFF)); 837 mDragIconDrawable.draw(c); 838 839 if (DEBUG) { 840 debugPaint.setColor(0xFF00FF00); 841 c.drawRect(iconLeft, iconTop, iconLeft + iconWidth, 842 iconTop + iconHeight, debugPaint); 843 } 844 } 845 } 846 } 847 848 /** 849 * Move the bottom edge of mChallengeView to a new position and notify the listener 850 * if it represents a change in position. Changes made through this method will 851 * be stable across layout passes. If this method is called before first layout of 852 * this SlidingChallengeLayout it will have no effect. 853 * 854 * @param bottom New bottom edge in px in this SlidingChallengeLayout's coordinate system. 855 * @return true if the challenge view was moved 856 */ 857 private boolean moveChallengeTo(int bottom) { 858 if (mChallengeView == null || !mHasLayout) { 859 return false; 860 } 861 862 final int layoutBottom = getLayoutBottom(); 863 final int challengeHeight = mChallengeView.getHeight(); 864 865 bottom = Math.max(layoutBottom, 866 Math.min(bottom, layoutBottom + challengeHeight - mChallengeBottomBound)); 867 868 float offset = 1.f - (float) (bottom - layoutBottom) / 869 (challengeHeight - mChallengeBottomBound); 870 mChallengeOffset = offset; 871 if (offset > 0 && !mChallengeShowing) { 872 setChallengeShowing(true); 873 } 874 875 mChallengeView.layout(mChallengeView.getLeft(), 876 bottom - mChallengeView.getHeight(), mChallengeView.getRight(), bottom); 877 878 mChallengeView.setAlpha(offset / 2 + 0.5f); 879 if (mScrollListener != null) { 880 mScrollListener.onScrollPositionChanged(offset, mChallengeView.getTop()); 881 } 882 postInvalidateOnAnimation(); 883 return true; 884 } 885 886 /** 887 * The bottom edge of this SlidingChallengeLayout's coordinate system; will coincide with 888 * the bottom edge of mChallengeView when the challenge is fully opened. 889 */ 890 private int getLayoutBottom() { 891 final int bottomMargin = (mChallengeView == null) 892 ? 0 893 : ((LayoutParams) mChallengeView.getLayoutParams()).bottomMargin; 894 final int layoutBottom = getHeight() - getPaddingBottom() - bottomMargin; 895 return layoutBottom; 896 } 897 898 /** 899 * The bottom edge of mChallengeView; essentially, where the sliding challenge 'is'. 900 */ 901 private int getChallengeBottom() { 902 if (mChallengeView == null) return 0; 903 904 return mChallengeView.getBottom(); 905 } 906 907 /** 908 * Show or hide the challenge view, animating it if necessary. 909 * @param show true to show, false to hide 910 */ 911 public void showChallenge(boolean show) { 912 showChallenge(show, 0); 913 if (!show) { 914 // Block any drags in progress so that callers can use this to disable dragging 915 // for other touch interactions. 916 mBlockDrag = true; 917 } 918 } 919 920 private void showChallenge(int velocity) { 921 boolean show = false; 922 if (Math.abs(velocity) > mMinVelocity) { 923 show = velocity < 0; 924 } else { 925 show = mChallengeOffset >= 0.5f; 926 } 927 showChallenge(show, velocity); 928 } 929 930 private void showChallenge(boolean show, int velocity) { 931 if (mChallengeView == null) { 932 setChallengeShowing(false); 933 return; 934 } 935 936 if (mHasLayout) { 937 final int layoutBottom = getLayoutBottom(); 938 animateChallengeTo(show ? layoutBottom : 939 layoutBottom + mChallengeView.getHeight() - mChallengeBottomBound, velocity); 940 } 941 } 942 943 @Override 944 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 945 return new LayoutParams(getContext(), attrs); 946 } 947 948 @Override 949 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 950 return p instanceof LayoutParams ? new LayoutParams((LayoutParams) p) : 951 p instanceof MarginLayoutParams ? new LayoutParams((MarginLayoutParams) p) : 952 new LayoutParams(p); 953 } 954 955 @Override 956 protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 957 return new LayoutParams(); 958 } 959 960 @Override 961 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 962 return p instanceof LayoutParams; 963 } 964 965 public static class LayoutParams extends MarginLayoutParams { 966 public int childType = CHILD_TYPE_NONE; 967 public static final int CHILD_TYPE_NONE = 0; 968 public static final int CHILD_TYPE_CHALLENGE = 2; 969 public static final int CHILD_TYPE_SCRIM = 4; 970 971 public LayoutParams() { 972 this(MATCH_PARENT, WRAP_CONTENT); 973 } 974 975 public LayoutParams(int width, int height) { 976 super(width, height); 977 } 978 979 public LayoutParams(android.view.ViewGroup.LayoutParams source) { 980 super(source); 981 } 982 983 public LayoutParams(MarginLayoutParams source) { 984 super(source); 985 } 986 987 public LayoutParams(LayoutParams source) { 988 super(source); 989 990 childType = source.childType; 991 } 992 993 public LayoutParams(Context c, AttributeSet attrs) { 994 super(c, attrs); 995 996 final TypedArray a = c.obtainStyledAttributes(attrs, 997 R.styleable.SlidingChallengeLayout_Layout); 998 childType = a.getInt(R.styleable.SlidingChallengeLayout_Layout_layout_childType, 999 CHILD_TYPE_NONE); 1000 a.recycle(); 1001 } 1002 } 1003} 1004