SlidingChallengeLayout.java revision 5ecd81154fa039961f65bb4e36d18ac555b0d1d6
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.keyguard; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.ObjectAnimator; 22import android.content.Context; 23import android.content.res.Resources; 24import android.content.res.TypedArray; 25import android.graphics.Canvas; 26import android.graphics.Paint; 27import android.util.AttributeSet; 28import android.util.DisplayMetrics; 29import android.util.FloatProperty; 30import android.util.Log; 31import android.util.Property; 32import android.view.MotionEvent; 33import android.view.VelocityTracker; 34import android.view.View; 35import android.view.ViewConfiguration; 36import android.view.ViewGroup; 37import android.view.accessibility.AccessibilityManager; 38import android.view.animation.Interpolator; 39import android.widget.Scroller; 40 41/** 42 * This layout handles interaction with the sliding security challenge views 43 * that overlay/resize other keyguard contents. 44 */ 45public class SlidingChallengeLayout extends ViewGroup implements ChallengeLayout { 46 private static final String TAG = "SlidingChallengeLayout"; 47 private static final boolean DEBUG = false; 48 49 // The drag handle is measured in dp above & below the top edge of the 50 // challenge view; these parameters change based on whether the challenge 51 // is open or closed. 52 private static final int DRAG_HANDLE_CLOSED_ABOVE = 8; // dp 53 private static final int DRAG_HANDLE_CLOSED_BELOW = 0; // dp 54 private static final int DRAG_HANDLE_OPEN_ABOVE = 8; // dp 55 private static final int DRAG_HANDLE_OPEN_BELOW = 0; // dp 56 57 private static final int HANDLE_ANIMATE_DURATION = 250; // 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 boolean mEdgeCaptured; 62 63 private DisplayMetrics mDisplayMetrics; 64 65 // Initialized during measurement from child layoutparams 66 private View mExpandChallengeView; 67 private KeyguardSecurityContainer mChallengeView; 68 private View mScrimView; 69 private View mWidgetsView; 70 71 // Range: 0 (fully hidden) to 1 (fully visible) 72 private float mChallengeOffset = 1.f; 73 private boolean mChallengeShowing = true; 74 private boolean mChallengeShowingTargetState = true; 75 private boolean mWasChallengeShowing = true; 76 private boolean mIsBouncing = false; 77 78 private final Scroller mScroller; 79 private ObjectAnimator mFader; 80 private int mScrollState; 81 private OnChallengeScrolledListener mScrollListener; 82 private OnBouncerStateChangedListener mBouncerListener; 83 84 public static final int SCROLL_STATE_IDLE = 0; 85 public static final int SCROLL_STATE_DRAGGING = 1; 86 public static final int SCROLL_STATE_SETTLING = 2; 87 public static final int SCROLL_STATE_FADING = 3; 88 89 private static final int CHALLENGE_FADE_OUT_DURATION = 100; 90 private static final int CHALLENGE_FADE_IN_DURATION = 160; 91 92 private static final int MAX_SETTLE_DURATION = 600; // ms 93 94 // ID of the pointer in charge of a current drag 95 private int mActivePointerId = INVALID_POINTER; 96 private static final int INVALID_POINTER = -1; 97 98 // True if the user is currently dragging the slider 99 private boolean mDragging; 100 // True if the user may not drag until a new gesture begins 101 private boolean mBlockDrag; 102 103 private VelocityTracker mVelocityTracker; 104 private int mMinVelocity; 105 private int mMaxVelocity; 106 private float mGestureStartX, mGestureStartY; // where did you first touch the screen? 107 private int mGestureStartChallengeBottom; // where was the challenge at that time? 108 109 private int mDragHandleClosedBelow; // handle hitrect extension into the challenge view 110 private int mDragHandleClosedAbove; // extend the handle's hitrect this far above the line 111 private int mDragHandleOpenBelow; // handle hitrect extension into the challenge view 112 private int mDragHandleOpenAbove; // extend the handle's hitrect this far above the line 113 114 private int mDragHandleEdgeSlop; 115 private int mChallengeBottomBound; // Number of pixels from the top of the challenge view 116 // that should remain on-screen 117 118 private int mTouchSlop; 119 private int mTouchSlopSquare; 120 121 float mHandleAlpha; 122 float mFrameAlpha; 123 float mFrameAnimationTarget = Float.MIN_VALUE; 124 private ObjectAnimator mHandleAnimation; 125 private ObjectAnimator mFrameAnimation; 126 127 private boolean mHasGlowpad; 128 129 // We have an internal and external version, and we and them together. 130 private boolean mChallengeInteractiveExternal = true; 131 private boolean mChallengeInteractiveInternal = true; 132 133 static final Property<SlidingChallengeLayout, Float> HANDLE_ALPHA = 134 new FloatProperty<SlidingChallengeLayout>("handleAlpha") { 135 @Override 136 public void setValue(SlidingChallengeLayout view, float value) { 137 view.mHandleAlpha = value; 138 view.invalidate(); 139 } 140 141 @Override 142 public Float get(SlidingChallengeLayout view) { 143 return view.mHandleAlpha; 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 private final OnClickListener mExpandChallengeClickListener = new OnClickListener() { 177 @Override 178 public void onClick(View v) { 179 if (!isChallengeShowing()) { 180 showChallenge(true); 181 } 182 } 183 }; 184 185 /** 186 * Listener interface that reports changes in scroll state of the challenge area. 187 */ 188 public interface OnChallengeScrolledListener { 189 /** 190 * The scroll state itself changed. 191 * 192 * <p>scrollState will be one of the following:</p> 193 * 194 * <ul> 195 * <li><code>SCROLL_STATE_IDLE</code> - The challenge area is stationary.</li> 196 * <li><code>SCROLL_STATE_DRAGGING</code> - The user is actively dragging 197 * the challenge area.</li> 198 * <li><code>SCROLL_STATE_SETTLING</code> - The challenge area is animating 199 * into place.</li> 200 * </ul> 201 * 202 * <p>Do not perform expensive operations (e.g. layout) 203 * while the scroll state is not <code>SCROLL_STATE_IDLE</code>.</p> 204 * 205 * @param scrollState The new scroll state of the challenge area. 206 */ 207 public void onScrollStateChanged(int scrollState); 208 209 /** 210 * The precise position of the challenge area has changed. 211 * 212 * <p>NOTE: It is NOT safe to modify layout or call any View methods that may 213 * result in a requestLayout anywhere in your view hierarchy as a result of this call. 214 * It may be called during drawing.</p> 215 * 216 * @param scrollPosition New relative position of the challenge area. 217 * 1.f = fully visible/ready to be interacted with. 218 * 0.f = fully invisible/inaccessible to the user. 219 * @param challengeTop Position of the top edge of the challenge view in px in the 220 * SlidingChallengeLayout's coordinate system. 221 */ 222 public void onScrollPositionChanged(float scrollPosition, int challengeTop); 223 } 224 225 public SlidingChallengeLayout(Context context) { 226 this(context, null); 227 } 228 229 public SlidingChallengeLayout(Context context, AttributeSet attrs) { 230 this(context, attrs, 0); 231 } 232 233 public SlidingChallengeLayout(Context context, AttributeSet attrs, int defStyle) { 234 super(context, attrs, defStyle); 235 236 mScroller = new Scroller(context, sMotionInterpolator); 237 238 final ViewConfiguration vc = ViewConfiguration.get(context); 239 mMinVelocity = vc.getScaledMinimumFlingVelocity(); 240 mMaxVelocity = vc.getScaledMaximumFlingVelocity(); 241 242 final Resources res = getResources(); 243 mDragHandleEdgeSlop = res.getDimensionPixelSize(R.dimen.kg_edge_swipe_region_size); 244 245 mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 246 mTouchSlopSquare = mTouchSlop * mTouchSlop; 247 248 mDisplayMetrics = res.getDisplayMetrics(); 249 final float density = mDisplayMetrics.density; 250 251 // top half of the lock icon, plus another 25% to be sure 252 mDragHandleClosedAbove = (int) (DRAG_HANDLE_CLOSED_ABOVE * density + 0.5f); 253 mDragHandleClosedBelow = (int) (DRAG_HANDLE_CLOSED_BELOW * density + 0.5f); 254 mDragHandleOpenAbove = (int) (DRAG_HANDLE_OPEN_ABOVE * density + 0.5f); 255 mDragHandleOpenBelow = (int) (DRAG_HANDLE_OPEN_BELOW * density + 0.5f); 256 257 // how much space to account for in the handle when closed 258 mChallengeBottomBound = res.getDimensionPixelSize(R.dimen.kg_widget_pager_bottom_padding); 259 260 setWillNotDraw(false); 261 setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_STABLE); 262 } 263 264 public void setHandleAlpha(float alpha) { 265 if (mExpandChallengeView != null) { 266 mExpandChallengeView.setAlpha(alpha); 267 } 268 } 269 270 public void setChallengeInteractive(boolean interactive) { 271 mChallengeInteractiveExternal = interactive; 272 if (mExpandChallengeView != null) { 273 mExpandChallengeView.setEnabled(interactive); 274 } 275 } 276 277 void animateHandle(boolean visible) { 278 if (mHandleAnimation != null) { 279 mHandleAnimation.cancel(); 280 mHandleAnimation = null; 281 } 282 final float targetAlpha = visible ? 1.f : 0.f; 283 if (targetAlpha == mHandleAlpha) { 284 return; 285 } 286 mHandleAnimation = ObjectAnimator.ofFloat(this, HANDLE_ALPHA, targetAlpha); 287 mHandleAnimation.setInterpolator(sHandleFadeInterpolator); 288 mHandleAnimation.setDuration(HANDLE_ANIMATE_DURATION); 289 mHandleAnimation.start(); 290 } 291 292 private void sendInitialListenerUpdates() { 293 if (mScrollListener != null) { 294 int challengeTop = mChallengeView != null ? mChallengeView.getTop() : 0; 295 mScrollListener.onScrollPositionChanged(mChallengeOffset, challengeTop); 296 mScrollListener.onScrollStateChanged(mScrollState); 297 } 298 } 299 300 public void setOnChallengeScrolledListener(OnChallengeScrolledListener listener) { 301 mScrollListener = listener; 302 if (mHasLayout) { 303 sendInitialListenerUpdates(); 304 } 305 } 306 307 public void setOnBouncerStateChangedListener(OnBouncerStateChangedListener listener) { 308 mBouncerListener = listener; 309 } 310 311 @Override 312 public void onAttachedToWindow() { 313 super.onAttachedToWindow(); 314 315 mHasLayout = false; 316 } 317 318 @Override 319 public void onDetachedFromWindow() { 320 super.onDetachedFromWindow(); 321 322 removeCallbacks(mEndScrollRunnable); 323 mHasLayout = false; 324 } 325 326 @Override 327 public void requestChildFocus(View child, View focused) { 328 if (mIsBouncing && child != mChallengeView) { 329 // Clear out of the bouncer if the user tries to move focus outside of 330 // the security challenge view. 331 hideBouncer(); 332 } 333 super.requestChildFocus(child, focused); 334 } 335 336 // We want the duration of the page snap animation to be influenced by the distance that 337 // the screen has to travel, however, we don't want this duration to be effected in a 338 // purely linear fashion. Instead, we use this method to moderate the effect that the distance 339 // of travel has on the overall snap duration. 340 float distanceInfluenceForSnapDuration(float f) { 341 f -= 0.5f; // center the values about 0. 342 f *= 0.3f * Math.PI / 2.0f; 343 return (float) Math.sin(f); 344 } 345 346 void setScrollState(int state) { 347 if (mScrollState != state) { 348 mScrollState = state; 349 350 animateHandle(state == SCROLL_STATE_IDLE && !mChallengeShowing); 351 if (mScrollListener != null) { 352 mScrollListener.onScrollStateChanged(state); 353 } 354 } 355 } 356 357 void completeChallengeScroll() { 358 setChallengeShowing(mChallengeShowingTargetState); 359 mChallengeOffset = mChallengeShowing ? 1.f : 0.f; 360 setScrollState(SCROLL_STATE_IDLE); 361 mChallengeInteractiveInternal = true; 362 mChallengeView.setLayerType(LAYER_TYPE_NONE, null); 363 } 364 365 void setScrimView(View scrim) { 366 if (mScrimView != null) { 367 mScrimView.setOnClickListener(null); 368 } 369 mScrimView = scrim; 370 mScrimView.setVisibility(mIsBouncing ? VISIBLE : GONE); 371 mScrimView.setFocusable(true); 372 mScrimView.setOnClickListener(mScrimClickListener); 373 } 374 375 /** 376 * Animate the bottom edge of the challenge view to the given position. 377 * 378 * @param y desired final position for the bottom edge of the challenge view in px 379 * @param velocity velocity in 380 */ 381 void animateChallengeTo(int y, int velocity) { 382 if (mChallengeView == null) { 383 // Nothing to do. 384 return; 385 } 386 387 cancelTransitionsInProgress(); 388 389 mChallengeInteractiveInternal = false; 390 mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null); 391 final int sy = mChallengeView.getBottom(); 392 final int dy = y - sy; 393 if (dy == 0) { 394 completeChallengeScroll(); 395 return; 396 } 397 398 setScrollState(SCROLL_STATE_SETTLING); 399 400 final int childHeight = mChallengeView.getHeight(); 401 final int halfHeight = childHeight / 2; 402 final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / childHeight); 403 final float distance = halfHeight + halfHeight * 404 distanceInfluenceForSnapDuration(distanceRatio); 405 406 int duration = 0; 407 velocity = Math.abs(velocity); 408 if (velocity > 0) { 409 duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 410 } else { 411 final float childDelta = (float) Math.abs(dy) / childHeight; 412 duration = (int) ((childDelta + 1) * 100); 413 } 414 duration = Math.min(duration, MAX_SETTLE_DURATION); 415 416 mScroller.startScroll(0, sy, 0, dy, duration); 417 postInvalidateOnAnimation(); 418 } 419 420 private void setChallengeShowing(boolean showChallenge) { 421 if (mChallengeShowing == showChallenge) { 422 return; 423 } 424 mChallengeShowing = showChallenge; 425 426 if (mExpandChallengeView == null || mChallengeView == null) { 427 // These might not be here yet if we haven't been through layout. 428 // If we haven't, the first layout pass will set everything up correctly 429 // based on mChallengeShowing as set above. 430 return; 431 } 432 433 if (mChallengeShowing) { 434 mExpandChallengeView.setVisibility(View.INVISIBLE); 435 mChallengeView.setVisibility(View.VISIBLE); 436 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 437 mChallengeView.requestAccessibilityFocus(); 438 mChallengeView.announceForAccessibility(mContext.getString( 439 R.string.keyguard_accessibility_unlock_area_expanded)); 440 } 441 } else { 442 mExpandChallengeView.setVisibility(View.VISIBLE); 443 mChallengeView.setVisibility(View.INVISIBLE); 444 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 445 mExpandChallengeView.requestAccessibilityFocus(); 446 mChallengeView.announceForAccessibility(mContext.getString( 447 R.string.keyguard_accessibility_unlock_area_collapsed)); 448 } 449 } 450 } 451 452 /** 453 * @return true if the challenge is at all visible. 454 */ 455 public boolean isChallengeShowing() { 456 return mChallengeShowing; 457 } 458 459 @Override 460 public boolean isChallengeOverlapping() { 461 return mChallengeShowing; 462 } 463 464 @Override 465 public boolean isBouncing() { 466 return mIsBouncing; 467 } 468 469 @Override 470 public int getBouncerAnimationDuration() { 471 return HANDLE_ANIMATE_DURATION; 472 } 473 474 @Override 475 public void showBouncer() { 476 if (mIsBouncing) return; 477 mWasChallengeShowing = mChallengeShowing; 478 mIsBouncing = true; 479 showChallenge(true); 480 if (mScrimView != null) { 481 Animator anim = ObjectAnimator.ofFloat(mScrimView, "alpha", 1f); 482 anim.setDuration(HANDLE_ANIMATE_DURATION); 483 anim.addListener(new AnimatorListenerAdapter() { 484 @Override 485 public void onAnimationStart(Animator animation) { 486 mScrimView.setVisibility(VISIBLE); 487 } 488 }); 489 anim.start(); 490 } 491 if (mChallengeView != null) { 492 mChallengeView.showBouncer(HANDLE_ANIMATE_DURATION); 493 } 494 495 if (mBouncerListener != null) { 496 mBouncerListener.onBouncerStateChanged(true); 497 } 498 } 499 500 @Override 501 public void hideBouncer() { 502 if (!mIsBouncing) return; 503 if (!mWasChallengeShowing) showChallenge(false); 504 mIsBouncing = false; 505 506 if (mScrimView != null) { 507 Animator anim = ObjectAnimator.ofFloat(mScrimView, "alpha", 0f); 508 anim.setDuration(HANDLE_ANIMATE_DURATION); 509 anim.addListener(new AnimatorListenerAdapter() { 510 @Override 511 public void onAnimationEnd(Animator animation) { 512 mScrimView.setVisibility(GONE); 513 } 514 }); 515 anim.start(); 516 } 517 if (mChallengeView != null) { 518 mChallengeView.hideBouncer(HANDLE_ANIMATE_DURATION); 519 } 520 if (mBouncerListener != null) { 521 mBouncerListener.onBouncerStateChanged(false); 522 } 523 } 524 525 private int getChallengeMargin(boolean expanded) { 526 return expanded && mHasGlowpad ? 0 : mDragHandleEdgeSlop; 527 } 528 529 private float getChallengeAlpha() { 530 float x = mChallengeOffset - 1; 531 return x * x * x + 1.f; 532 } 533 534 @Override 535 public void requestDisallowInterceptTouchEvent(boolean allowIntercept) { 536 // We'll intercept whoever we feel like! ...as long as it isn't a challenge view. 537 // If there are one or more pointers in the challenge view before we take over 538 // touch events, onInterceptTouchEvent will set mBlockDrag. 539 } 540 541 @Override 542 public boolean onInterceptTouchEvent(MotionEvent ev) { 543 if (mVelocityTracker == null) { 544 mVelocityTracker = VelocityTracker.obtain(); 545 } 546 mVelocityTracker.addMovement(ev); 547 548 final int action = ev.getActionMasked(); 549 switch (action) { 550 case MotionEvent.ACTION_DOWN: 551 mGestureStartX = ev.getX(); 552 mGestureStartY = ev.getY(); 553 mBlockDrag = false; 554 break; 555 556 case MotionEvent.ACTION_CANCEL: 557 case MotionEvent.ACTION_UP: 558 resetTouch(); 559 break; 560 561 case MotionEvent.ACTION_MOVE: 562 final int count = ev.getPointerCount(); 563 for (int i = 0; i < count; i++) { 564 final float x = ev.getX(i); 565 final float y = ev.getY(i); 566 if (!mIsBouncing && mActivePointerId == INVALID_POINTER 567 && (crossedDragHandle(x, y, mGestureStartY) 568 || (isInChallengeView(x, y) && 569 mScrollState == SCROLL_STATE_SETTLING))) { 570 mActivePointerId = ev.getPointerId(i); 571 mGestureStartX = x; 572 mGestureStartY = y; 573 mGestureStartChallengeBottom = getChallengeBottom(); 574 mDragging = true; 575 mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null); 576 } else if (mChallengeShowing && isInChallengeView(x, y)) { 577 mBlockDrag = true; 578 } 579 } 580 break; 581 } 582 583 if (mBlockDrag || isChallengeInteractionBlocked()) { 584 mActivePointerId = INVALID_POINTER; 585 mDragging = false; 586 } 587 588 return mDragging; 589 } 590 591 private boolean isChallengeInteractionBlocked() { 592 return !mChallengeInteractiveExternal || !mChallengeInteractiveInternal; 593 } 594 595 private void resetTouch() { 596 mVelocityTracker.recycle(); 597 mVelocityTracker = null; 598 mActivePointerId = INVALID_POINTER; 599 mDragging = mBlockDrag = false; 600 } 601 602 @Override 603 public boolean onTouchEvent(MotionEvent ev) { 604 if (mVelocityTracker == null) { 605 mVelocityTracker = VelocityTracker.obtain(); 606 } 607 mVelocityTracker.addMovement(ev); 608 609 final int action = ev.getActionMasked(); 610 switch (action) { 611 case MotionEvent.ACTION_DOWN: 612 mBlockDrag = false; 613 mGestureStartX = ev.getX(); 614 mGestureStartY = ev.getY(); 615 break; 616 617 case MotionEvent.ACTION_CANCEL: 618 if (mDragging && !isChallengeInteractionBlocked()) { 619 showChallenge(0); 620 } 621 resetTouch(); 622 break; 623 624 case MotionEvent.ACTION_POINTER_UP: 625 if (mActivePointerId != ev.getPointerId(ev.getActionIndex())) { 626 break; 627 } 628 case MotionEvent.ACTION_UP: 629 if (mDragging && !isChallengeInteractionBlocked()) { 630 mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); 631 showChallenge((int) mVelocityTracker.getYVelocity(mActivePointerId)); 632 } 633 resetTouch(); 634 break; 635 636 case MotionEvent.ACTION_MOVE: 637 if (!mDragging && !mBlockDrag && !mIsBouncing) { 638 final int count = ev.getPointerCount(); 639 for (int i = 0; i < count; i++) { 640 final float x = ev.getX(i); 641 final float y = ev.getY(i); 642 643 if ((isInDragHandle(x, y) || crossedDragHandle(x, y, mGestureStartY) || 644 (isInChallengeView(x, y) && mScrollState == SCROLL_STATE_SETTLING)) 645 && mActivePointerId == INVALID_POINTER 646 && !isChallengeInteractionBlocked()) { 647 mGestureStartX = x; 648 mGestureStartY = y; 649 mActivePointerId = ev.getPointerId(i); 650 mGestureStartChallengeBottom = getChallengeBottom(); 651 mDragging = true; 652 mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null); 653 break; 654 } 655 } 656 } 657 // Not an else; this can be set above. 658 if (mDragging) { 659 // No-op if already in this state, but set it here in case we arrived 660 // at this point from either intercept or the above. 661 setScrollState(SCROLL_STATE_DRAGGING); 662 663 final int index = ev.findPointerIndex(mActivePointerId); 664 if (index < 0) { 665 // Oops, bogus state. We lost some touch events somewhere. 666 // Just drop it with no velocity and let things settle. 667 resetTouch(); 668 showChallenge(0); 669 return true; 670 } 671 final float y = ev.getY(index); 672 final float pos = Math.min(y - mGestureStartY, 673 getLayoutBottom() - mChallengeBottomBound); 674 675 moveChallengeTo(mGestureStartChallengeBottom + (int) pos); 676 } 677 break; 678 } 679 return true; 680 } 681 682 /** 683 * The lifecycle of touch events is subtle and it's very easy to do something 684 * that will cause bugs that will be nasty to track when overriding this method. 685 * Normally one should always override onInterceptTouchEvent instead. 686 * 687 * To put it another way, don't try this at home. 688 */ 689 @Override 690 public boolean dispatchTouchEvent(MotionEvent ev) { 691 final int action = ev.getActionMasked(); 692 boolean handled = false; 693 if (action == MotionEvent.ACTION_DOWN) { 694 // Defensive programming: if we didn't get the UP or CANCEL, reset anyway. 695 mEdgeCaptured = false; 696 } 697 if (mWidgetsView != null && !mIsBouncing && (mEdgeCaptured || isEdgeSwipeBeginEvent(ev))) { 698 // Normally we would need to do a lot of extra stuff here. 699 // We can only get away with this because we haven't padded in 700 // the widget pager or otherwise transformed it during layout. 701 // We also don't support things like splitting MotionEvents. 702 703 // We set handled to captured even if dispatch is returning false here so that 704 // we don't send a different view a busted or incomplete event stream. 705 handled = mEdgeCaptured |= mWidgetsView.dispatchTouchEvent(ev); 706 } 707 708 if (!handled && !mEdgeCaptured) { 709 handled = super.dispatchTouchEvent(ev); 710 } 711 712 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { 713 mEdgeCaptured = false; 714 } 715 716 return handled; 717 } 718 719 private boolean isEdgeSwipeBeginEvent(MotionEvent ev) { 720 if (ev.getActionMasked() != MotionEvent.ACTION_DOWN) { 721 return false; 722 } 723 724 final float x = ev.getX(); 725 return x < mDragHandleEdgeSlop || x >= getWidth() - mDragHandleEdgeSlop; 726 } 727 728 /** 729 * We only want to add additional vertical space to the drag handle when the panel is fully 730 * closed. 731 */ 732 private int getDragHandleSizeAbove() { 733 return isChallengeShowing() ? mDragHandleOpenAbove : mDragHandleClosedAbove; 734 } 735 private int getDragHandleSizeBelow() { 736 return isChallengeShowing() ? mDragHandleOpenBelow : mDragHandleClosedBelow; 737 } 738 739 private boolean isInChallengeView(float x, float y) { 740 return isPointInView(x, y, mChallengeView); 741 } 742 743 private boolean isInDragHandle(float x, float y) { 744 return isPointInView(x, y, mExpandChallengeView); 745 } 746 747 private boolean isPointInView(float x, float y, View view) { 748 if (view == null) { 749 return false; 750 } 751 return x >= view.getLeft() && y >= view.getTop() 752 && x < view.getRight() && y < view.getBottom(); 753 } 754 755 private boolean crossedDragHandle(float x, float y, float initialY) { 756 757 final int challengeTop = mChallengeView.getTop(); 758 final boolean horizOk = x >= 0 && x < getWidth(); 759 760 final boolean vertOk; 761 if (mChallengeShowing) { 762 vertOk = initialY < (challengeTop - getDragHandleSizeAbove()) && 763 y > challengeTop + getDragHandleSizeBelow(); 764 } else { 765 vertOk = initialY > challengeTop + getDragHandleSizeBelow() && 766 y < challengeTop - getDragHandleSizeAbove(); 767 } 768 return horizOk && vertOk; 769 } 770 771 private int makeChildMeasureSpec(int maxSize, int childDimen) { 772 final int mode; 773 final int size; 774 switch (childDimen) { 775 case LayoutParams.WRAP_CONTENT: 776 mode = MeasureSpec.AT_MOST; 777 size = maxSize; 778 break; 779 case LayoutParams.MATCH_PARENT: 780 mode = MeasureSpec.EXACTLY; 781 size = maxSize; 782 break; 783 default: 784 mode = MeasureSpec.EXACTLY; 785 size = Math.min(maxSize, childDimen); 786 break; 787 } 788 return MeasureSpec.makeMeasureSpec(size, mode); 789 } 790 791 @Override 792 protected void onMeasure(int widthSpec, int heightSpec) { 793 if (MeasureSpec.getMode(widthSpec) != MeasureSpec.EXACTLY || 794 MeasureSpec.getMode(heightSpec) != MeasureSpec.EXACTLY) { 795 throw new IllegalArgumentException( 796 "SlidingChallengeLayout must be measured with an exact size"); 797 } 798 799 final int width = MeasureSpec.getSize(widthSpec); 800 final int height = MeasureSpec.getSize(heightSpec); 801 setMeasuredDimension(width, height); 802 803 // Find one and only one challenge view. 804 final View oldChallengeView = mChallengeView; 805 final View oldExpandChallengeView = mChallengeView; 806 mChallengeView = null; 807 mExpandChallengeView = null; 808 final int count = getChildCount(); 809 810 // First iteration through the children finds special children and sets any associated 811 // state. 812 for (int i = 0; i < count; i++) { 813 final View child = getChildAt(i); 814 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 815 if (lp.childType == LayoutParams.CHILD_TYPE_CHALLENGE) { 816 if (mChallengeView != null) { 817 throw new IllegalStateException( 818 "There may only be one child with layout_isChallenge=\"true\""); 819 } 820 if (!(child instanceof KeyguardSecurityContainer)) { 821 throw new IllegalArgumentException( 822 "Challenge must be a KeyguardSecurityContainer"); 823 } 824 mChallengeView = (KeyguardSecurityContainer) child; 825 if (mChallengeView != oldChallengeView) { 826 mChallengeView.setVisibility(mChallengeShowing ? VISIBLE : INVISIBLE); 827 } 828 // We're going to play silly games with the frame's background drawable later. 829 if (!mHasLayout) { 830 // Set up the margin correctly based on our content for the first run. 831 mHasGlowpad = child.findViewById(R.id.keyguard_selector_view) != null; 832 lp.leftMargin = lp.rightMargin = getChallengeMargin(true); 833 } 834 } else if (lp.childType == LayoutParams.CHILD_TYPE_EXPAND_CHALLENGE_HANDLE) { 835 if (mExpandChallengeView != null) { 836 throw new IllegalStateException( 837 "There may only be one child with layout_childType" 838 + "=\"expandChallengeHandle\""); 839 } 840 mExpandChallengeView = child; 841 if (mExpandChallengeView != oldExpandChallengeView) { 842 mExpandChallengeView.setVisibility(mChallengeShowing ? INVISIBLE : VISIBLE); 843 mExpandChallengeView.setOnClickListener(mExpandChallengeClickListener); 844 } 845 } else if (lp.childType == LayoutParams.CHILD_TYPE_SCRIM) { 846 setScrimView(child); 847 } else if (lp.childType == LayoutParams.CHILD_TYPE_WIDGETS) { 848 mWidgetsView = child; 849 } 850 } 851 852 // We want to measure the challenge view first, since the KeyguardWidgetPager 853 // needs to do things its measure pass that are dependent on the challenge view 854 // having been measured. 855 if (mChallengeView != null && mChallengeView.getVisibility() != View.GONE) { 856 // This one's a little funny. If the IME is present - reported in the form 857 // of insets on the root view - we only give the challenge the space it would 858 // have had if the IME wasn't there in order to keep the rest of the layout stable. 859 // We base this on the layout_maxHeight on the challenge view. If it comes out 860 // negative or zero, either we didn't have a maxHeight or we're totally out of space, 861 // so give up and measure as if this rule weren't there. 862 int challengeHeightSpec = heightSpec; 863 final View root = getRootView(); 864 if (root != null) { 865 final LayoutParams lp = (LayoutParams) mChallengeView.getLayoutParams(); 866 final int specSize = MeasureSpec.getSize(heightSpec); 867 final int windowHeight = mDisplayMetrics.heightPixels - root.getPaddingTop(); 868 final int diff = windowHeight - specSize; 869 final int maxChallengeHeight = lp.maxHeight - diff; 870 if (maxChallengeHeight > 0) { 871 challengeHeightSpec = makeChildMeasureSpec(maxChallengeHeight, lp.height); 872 } 873 } 874 measureChildWithMargins(mChallengeView, widthSpec, 0, challengeHeightSpec, 0); 875 } 876 877 // Measure the rest of the children 878 for (int i = 0; i < count; i++) { 879 final View child = getChildAt(i); 880 if (child.getVisibility() == GONE) { 881 continue; 882 } 883 // Don't measure the challenge view twice! 884 if (child == mChallengeView) continue; 885 886 // Measure children. Widget frame measures special, so that we can ignore 887 // insets for the IME. 888 int parentWidthSpec = widthSpec, parentHeightSpec = heightSpec; 889 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 890 if (lp.childType == LayoutParams.CHILD_TYPE_WIDGETS) { 891 final View root = getRootView(); 892 if (root != null) { 893 // This calculation is super dodgy and relies on several assumptions. 894 // Specifically that the root of the window will be padded in for insets 895 // and that the window is LAYOUT_IN_SCREEN. 896 final int windowWidth = mDisplayMetrics.widthPixels; 897 final int windowHeight = mDisplayMetrics.heightPixels - root.getPaddingTop(); 898 parentWidthSpec = MeasureSpec.makeMeasureSpec( 899 windowWidth, MeasureSpec.EXACTLY); 900 parentHeightSpec = MeasureSpec.makeMeasureSpec( 901 windowHeight, MeasureSpec.EXACTLY); 902 } 903 } 904 measureChildWithMargins(child, parentWidthSpec, 0, parentHeightSpec, 0); 905 } 906 } 907 908 @Override 909 protected void onLayout(boolean changed, int l, int t, int r, int b) { 910 final int paddingLeft = getPaddingLeft(); 911 final int paddingTop = getPaddingTop(); 912 final int paddingRight = getPaddingRight(); 913 final int paddingBottom = getPaddingBottom(); 914 final int width = r - l; 915 final int height = b - t; 916 917 final int count = getChildCount(); 918 for (int i = 0; i < count; i++) { 919 final View child = getChildAt(i); 920 921 if (child.getVisibility() == GONE) continue; 922 923 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 924 925 if (lp.childType == LayoutParams.CHILD_TYPE_CHALLENGE) { 926 // Challenge views pin to the bottom, offset by a portion of their height, 927 // and center horizontally. 928 final int center = (paddingLeft + width - paddingRight) / 2; 929 final int childWidth = child.getMeasuredWidth(); 930 final int childHeight = child.getMeasuredHeight(); 931 final int left = center - childWidth / 2; 932 final int layoutBottom = height - paddingBottom - lp.bottomMargin; 933 // We use the top of the challenge view to position the handle, so 934 // we never want less than the handle size showing at the bottom. 935 final int bottom = layoutBottom + (int) ((childHeight - mChallengeBottomBound) 936 * (1 - mChallengeOffset)); 937 child.setAlpha(getChallengeAlpha()); 938 child.layout(left, bottom - childHeight, left + childWidth, bottom); 939 } else if (lp.childType == LayoutParams.CHILD_TYPE_EXPAND_CHALLENGE_HANDLE) { 940 final int center = (paddingLeft + width - paddingRight) / 2; 941 final int left = center - child.getMeasuredWidth() / 2; 942 final int right = left + child.getMeasuredWidth(); 943 final int bottom = height - paddingBottom - lp.bottomMargin; 944 final int top = bottom - child.getMeasuredHeight(); 945 child.layout(left, top, right, bottom); 946 } else { 947 // Non-challenge views lay out from the upper left, layered. 948 child.layout(paddingLeft + lp.leftMargin, 949 paddingTop + lp.topMargin, 950 paddingLeft + child.getMeasuredWidth(), 951 paddingTop + child.getMeasuredHeight()); 952 } 953 } 954 955 if (!mHasLayout) { 956 mHasLayout = true; 957 } 958 } 959 960 @Override 961 public void draw(Canvas c) { 962 super.draw(c); 963 if (DEBUG) { 964 final Paint debugPaint = new Paint(); 965 debugPaint.setColor(0x40FF00CC); 966 // show the isInDragHandle() rect 967 c.drawRect(mDragHandleEdgeSlop, 968 mChallengeView.getTop() - getDragHandleSizeAbove(), 969 getWidth() - mDragHandleEdgeSlop, 970 mChallengeView.getTop() + getDragHandleSizeBelow(), 971 debugPaint); 972 } 973 } 974 975 public void computeScroll() { 976 super.computeScroll(); 977 978 if (!mScroller.isFinished()) { 979 if (mChallengeView == null) { 980 // Can't scroll if the view is missing. 981 Log.e(TAG, "Challenge view missing in computeScroll"); 982 mScroller.abortAnimation(); 983 return; 984 } 985 986 mScroller.computeScrollOffset(); 987 moveChallengeTo(mScroller.getCurrY()); 988 989 if (mScroller.isFinished()) { 990 post(mEndScrollRunnable); 991 } 992 } 993 } 994 995 private void cancelTransitionsInProgress() { 996 if (!mScroller.isFinished()) { 997 mScroller.abortAnimation(); 998 completeChallengeScroll(); 999 } 1000 if (mFader != null) { 1001 mFader.cancel(); 1002 } 1003 } 1004 1005 public void fadeInChallenge() { 1006 fadeChallenge(true); 1007 } 1008 1009 public void fadeOutChallenge() { 1010 fadeChallenge(false); 1011 } 1012 1013 public void fadeChallenge(final boolean show) { 1014 if (mChallengeView != null) { 1015 1016 cancelTransitionsInProgress(); 1017 float alpha = show ? 1f : 0f; 1018 int duration = show ? CHALLENGE_FADE_IN_DURATION : CHALLENGE_FADE_OUT_DURATION; 1019 mFader = ObjectAnimator.ofFloat(mChallengeView, "alpha", alpha); 1020 mFader.addListener(new AnimatorListenerAdapter() { 1021 @Override 1022 public void onAnimationStart(Animator animation) { 1023 onFadeStart(show); 1024 } 1025 @Override 1026 public void onAnimationEnd(Animator animation) { 1027 onFadeEnd(show); 1028 } 1029 }); 1030 mFader.setDuration(duration); 1031 mFader.start(); 1032 } 1033 } 1034 1035 private int getMaxChallengeBottom() { 1036 if (mChallengeView == null) return 0; 1037 final int layoutBottom = getLayoutBottom(); 1038 final int challengeHeight = mChallengeView.getMeasuredHeight(); 1039 1040 return (layoutBottom + challengeHeight - mChallengeBottomBound); 1041 } 1042 1043 private int getMinChallengeBottom() { 1044 return getLayoutBottom(); 1045 } 1046 1047 1048 private void onFadeStart(boolean show) { 1049 mChallengeInteractiveInternal = false; 1050 mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null); 1051 1052 if (show) { 1053 moveChallengeTo(getMinChallengeBottom()); 1054 } 1055 1056 setScrollState(SCROLL_STATE_FADING); 1057 } 1058 1059 private void onFadeEnd(boolean show) { 1060 mChallengeInteractiveInternal = true; 1061 setChallengeShowing(show); 1062 1063 if (!show) { 1064 moveChallengeTo(getMaxChallengeBottom()); 1065 } 1066 1067 mChallengeView.setLayerType(LAYER_TYPE_NONE, null); 1068 mFader = null; 1069 setScrollState(SCROLL_STATE_IDLE); 1070 } 1071 1072 public int getMaxChallengeTop() { 1073 if (mChallengeView == null) return 0; 1074 1075 final int layoutBottom = getLayoutBottom(); 1076 final int challengeHeight = mChallengeView.getMeasuredHeight(); 1077 return layoutBottom - challengeHeight; 1078 } 1079 1080 /** 1081 * Move the bottom edge of mChallengeView to a new position and notify the listener 1082 * if it represents a change in position. Changes made through this method will 1083 * be stable across layout passes. If this method is called before first layout of 1084 * this SlidingChallengeLayout it will have no effect. 1085 * 1086 * @param bottom New bottom edge in px in this SlidingChallengeLayout's coordinate system. 1087 * @return true if the challenge view was moved 1088 */ 1089 private boolean moveChallengeTo(int bottom) { 1090 if (mChallengeView == null || !mHasLayout) { 1091 return false; 1092 } 1093 1094 final int layoutBottom = getLayoutBottom(); 1095 final int challengeHeight = mChallengeView.getHeight(); 1096 1097 bottom = Math.max(getMinChallengeBottom(), 1098 Math.min(bottom, getMaxChallengeBottom())); 1099 1100 float offset = 1.f - (float) (bottom - layoutBottom) / 1101 (challengeHeight - mChallengeBottomBound); 1102 mChallengeOffset = offset; 1103 if (offset > 0 && !mChallengeShowing) { 1104 setChallengeShowing(true); 1105 } 1106 1107 mChallengeView.layout(mChallengeView.getLeft(), 1108 bottom - mChallengeView.getHeight(), mChallengeView.getRight(), bottom); 1109 1110 mChallengeView.setAlpha(getChallengeAlpha()); 1111 if (mScrollListener != null) { 1112 mScrollListener.onScrollPositionChanged(offset, mChallengeView.getTop()); 1113 } 1114 postInvalidateOnAnimation(); 1115 return true; 1116 } 1117 1118 /** 1119 * The bottom edge of this SlidingChallengeLayout's coordinate system; will coincide with 1120 * the bottom edge of mChallengeView when the challenge is fully opened. 1121 */ 1122 private int getLayoutBottom() { 1123 final int bottomMargin = (mChallengeView == null) 1124 ? 0 1125 : ((LayoutParams) mChallengeView.getLayoutParams()).bottomMargin; 1126 final int layoutBottom = getMeasuredHeight() - getPaddingBottom() - bottomMargin; 1127 return layoutBottom; 1128 } 1129 1130 /** 1131 * The bottom edge of mChallengeView; essentially, where the sliding challenge 'is'. 1132 */ 1133 private int getChallengeBottom() { 1134 if (mChallengeView == null) return 0; 1135 1136 return mChallengeView.getBottom(); 1137 } 1138 1139 /** 1140 * Show or hide the challenge view, animating it if necessary. 1141 * @param show true to show, false to hide 1142 */ 1143 public void showChallenge(boolean show) { 1144 showChallenge(show, 0); 1145 if (!show) { 1146 // Block any drags in progress so that callers can use this to disable dragging 1147 // for other touch interactions. 1148 mBlockDrag = true; 1149 } 1150 } 1151 1152 private void showChallenge(int velocity) { 1153 boolean show = false; 1154 if (Math.abs(velocity) > mMinVelocity) { 1155 show = velocity < 0; 1156 } else { 1157 show = mChallengeOffset >= 0.5f; 1158 } 1159 showChallenge(show, velocity); 1160 } 1161 1162 private void showChallenge(boolean show, int velocity) { 1163 if (mChallengeView == null) { 1164 setChallengeShowing(false); 1165 return; 1166 } 1167 1168 if (mHasLayout) { 1169 mChallengeShowingTargetState = show; 1170 final int layoutBottom = getLayoutBottom(); 1171 animateChallengeTo(show ? layoutBottom : 1172 layoutBottom + mChallengeView.getHeight() - mChallengeBottomBound, velocity); 1173 } 1174 } 1175 1176 @Override 1177 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 1178 return new LayoutParams(getContext(), attrs); 1179 } 1180 1181 @Override 1182 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 1183 return p instanceof LayoutParams ? new LayoutParams((LayoutParams) p) : 1184 p instanceof MarginLayoutParams ? new LayoutParams((MarginLayoutParams) p) : 1185 new LayoutParams(p); 1186 } 1187 1188 @Override 1189 protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 1190 return new LayoutParams(); 1191 } 1192 1193 @Override 1194 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 1195 return p instanceof LayoutParams; 1196 } 1197 1198 public static class LayoutParams extends MarginLayoutParams { 1199 public int childType = CHILD_TYPE_NONE; 1200 public static final int CHILD_TYPE_NONE = 0; 1201 public static final int CHILD_TYPE_CHALLENGE = 2; 1202 public static final int CHILD_TYPE_SCRIM = 4; 1203 public static final int CHILD_TYPE_WIDGETS = 5; 1204 public static final int CHILD_TYPE_EXPAND_CHALLENGE_HANDLE = 6; 1205 1206 public int maxHeight; 1207 1208 public LayoutParams() { 1209 this(MATCH_PARENT, WRAP_CONTENT); 1210 } 1211 1212 public LayoutParams(int width, int height) { 1213 super(width, height); 1214 } 1215 1216 public LayoutParams(android.view.ViewGroup.LayoutParams source) { 1217 super(source); 1218 } 1219 1220 public LayoutParams(MarginLayoutParams source) { 1221 super(source); 1222 } 1223 1224 public LayoutParams(LayoutParams source) { 1225 super(source); 1226 1227 childType = source.childType; 1228 } 1229 1230 public LayoutParams(Context c, AttributeSet attrs) { 1231 super(c, attrs); 1232 1233 final TypedArray a = c.obtainStyledAttributes(attrs, 1234 R.styleable.SlidingChallengeLayout_Layout); 1235 childType = a.getInt(R.styleable.SlidingChallengeLayout_Layout_layout_childType, 1236 CHILD_TYPE_NONE); 1237 maxHeight = a.getDimensionPixelSize( 1238 R.styleable.SlidingChallengeLayout_Layout_layout_maxHeight, 0); 1239 a.recycle(); 1240 } 1241 } 1242} 1243