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