SlidingChallengeLayout.java revision 257f283b0e4cc1c1fa4e7f2472d3ee4ab3b451d0
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 if (mScrimView != null) { 371 mScrimView.setVisibility(mIsBouncing ? VISIBLE : GONE); 372 mScrimView.setFocusable(true); 373 mScrimView.setOnClickListener(mScrimClickListener); 374 } 375 } 376 377 /** 378 * Animate the bottom edge of the challenge view to the given position. 379 * 380 * @param y desired final position for the bottom edge of the challenge view in px 381 * @param velocity velocity in 382 */ 383 void animateChallengeTo(int y, int velocity) { 384 if (mChallengeView == null) { 385 // Nothing to do. 386 return; 387 } 388 389 cancelTransitionsInProgress(); 390 391 mChallengeInteractiveInternal = false; 392 mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null); 393 final int sy = mChallengeView.getBottom(); 394 final int dy = y - sy; 395 if (dy == 0) { 396 completeChallengeScroll(); 397 return; 398 } 399 400 setScrollState(SCROLL_STATE_SETTLING); 401 402 final int childHeight = mChallengeView.getHeight(); 403 final int halfHeight = childHeight / 2; 404 final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / childHeight); 405 final float distance = halfHeight + halfHeight * 406 distanceInfluenceForSnapDuration(distanceRatio); 407 408 int duration = 0; 409 velocity = Math.abs(velocity); 410 if (velocity > 0) { 411 duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 412 } else { 413 final float childDelta = (float) Math.abs(dy) / childHeight; 414 duration = (int) ((childDelta + 1) * 100); 415 } 416 duration = Math.min(duration, MAX_SETTLE_DURATION); 417 418 mScroller.startScroll(0, sy, 0, dy, duration); 419 postInvalidateOnAnimation(); 420 } 421 422 private void setChallengeShowing(boolean showChallenge) { 423 if (mChallengeShowing == showChallenge) { 424 return; 425 } 426 mChallengeShowing = showChallenge; 427 428 if (mExpandChallengeView == null || mChallengeView == null) { 429 // These might not be here yet if we haven't been through layout. 430 // If we haven't, the first layout pass will set everything up correctly 431 // based on mChallengeShowing as set above. 432 return; 433 } 434 435 if (mChallengeShowing) { 436 mExpandChallengeView.setVisibility(View.INVISIBLE); 437 mChallengeView.setVisibility(View.VISIBLE); 438 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 439 mChallengeView.requestAccessibilityFocus(); 440 mChallengeView.announceForAccessibility(mContext.getString( 441 R.string.keyguard_accessibility_unlock_area_expanded)); 442 } 443 } else { 444 mExpandChallengeView.setVisibility(View.VISIBLE); 445 mChallengeView.setVisibility(View.INVISIBLE); 446 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 447 mExpandChallengeView.requestAccessibilityFocus(); 448 mChallengeView.announceForAccessibility(mContext.getString( 449 R.string.keyguard_accessibility_unlock_area_collapsed)); 450 } 451 } 452 } 453 454 /** 455 * @return true if the challenge is at all visible. 456 */ 457 public boolean isChallengeShowing() { 458 return mChallengeShowing; 459 } 460 461 @Override 462 public boolean isChallengeOverlapping() { 463 return mChallengeShowing; 464 } 465 466 @Override 467 public boolean isBouncing() { 468 return mIsBouncing; 469 } 470 471 @Override 472 public int getBouncerAnimationDuration() { 473 return HANDLE_ANIMATE_DURATION; 474 } 475 476 @Override 477 public void showBouncer() { 478 if (mIsBouncing) return; 479 mWasChallengeShowing = mChallengeShowing; 480 mIsBouncing = true; 481 showChallenge(true); 482 if (mScrimView != null) { 483 Animator anim = ObjectAnimator.ofFloat(mScrimView, "alpha", 1f); 484 anim.setDuration(HANDLE_ANIMATE_DURATION); 485 anim.addListener(new AnimatorListenerAdapter() { 486 @Override 487 public void onAnimationStart(Animator animation) { 488 mScrimView.setVisibility(VISIBLE); 489 } 490 }); 491 anim.start(); 492 } 493 if (mChallengeView != null) { 494 mChallengeView.showBouncer(HANDLE_ANIMATE_DURATION); 495 } 496 497 if (mBouncerListener != null) { 498 mBouncerListener.onBouncerStateChanged(true); 499 } 500 } 501 502 @Override 503 public void hideBouncer() { 504 if (!mIsBouncing) return; 505 if (!mWasChallengeShowing) showChallenge(false); 506 mIsBouncing = false; 507 508 if (mScrimView != null) { 509 Animator anim = ObjectAnimator.ofFloat(mScrimView, "alpha", 0f); 510 anim.setDuration(HANDLE_ANIMATE_DURATION); 511 anim.addListener(new AnimatorListenerAdapter() { 512 @Override 513 public void onAnimationEnd(Animator animation) { 514 mScrimView.setVisibility(GONE); 515 } 516 }); 517 anim.start(); 518 } 519 if (mChallengeView != null) { 520 mChallengeView.hideBouncer(HANDLE_ANIMATE_DURATION); 521 } 522 if (mBouncerListener != null) { 523 mBouncerListener.onBouncerStateChanged(false); 524 } 525 } 526 527 private int getChallengeMargin(boolean expanded) { 528 return expanded && mHasGlowpad ? 0 : mDragHandleEdgeSlop; 529 } 530 531 private float getChallengeAlpha() { 532 float x = mChallengeOffset - 1; 533 return x * x * x + 1.f; 534 } 535 536 @Override 537 public void requestDisallowInterceptTouchEvent(boolean allowIntercept) { 538 // We'll intercept whoever we feel like! ...as long as it isn't a challenge view. 539 // If there are one or more pointers in the challenge view before we take over 540 // touch events, onInterceptTouchEvent will set mBlockDrag. 541 } 542 543 @Override 544 public boolean onInterceptTouchEvent(MotionEvent ev) { 545 if (mVelocityTracker == null) { 546 mVelocityTracker = VelocityTracker.obtain(); 547 } 548 mVelocityTracker.addMovement(ev); 549 550 final int action = ev.getActionMasked(); 551 switch (action) { 552 case MotionEvent.ACTION_DOWN: 553 mGestureStartX = ev.getX(); 554 mGestureStartY = ev.getY(); 555 mBlockDrag = false; 556 break; 557 558 case MotionEvent.ACTION_CANCEL: 559 case MotionEvent.ACTION_UP: 560 resetTouch(); 561 break; 562 563 case MotionEvent.ACTION_MOVE: 564 final int count = ev.getPointerCount(); 565 for (int i = 0; i < count; i++) { 566 final float x = ev.getX(i); 567 final float y = ev.getY(i); 568 if (!mIsBouncing && mActivePointerId == INVALID_POINTER 569 && (crossedDragHandle(x, y, mGestureStartY) 570 || (isInChallengeView(x, y) && 571 mScrollState == SCROLL_STATE_SETTLING))) { 572 mActivePointerId = ev.getPointerId(i); 573 mGestureStartX = x; 574 mGestureStartY = y; 575 mGestureStartChallengeBottom = getChallengeBottom(); 576 mDragging = true; 577 mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null); 578 } else if (mChallengeShowing && isInChallengeView(x, y)) { 579 mBlockDrag = true; 580 } 581 } 582 break; 583 } 584 585 if (mBlockDrag || isChallengeInteractionBlocked()) { 586 mActivePointerId = INVALID_POINTER; 587 mDragging = false; 588 } 589 590 return mDragging; 591 } 592 593 private boolean isChallengeInteractionBlocked() { 594 return !mChallengeInteractiveExternal || !mChallengeInteractiveInternal; 595 } 596 597 private void resetTouch() { 598 mVelocityTracker.recycle(); 599 mVelocityTracker = null; 600 mActivePointerId = INVALID_POINTER; 601 mDragging = mBlockDrag = false; 602 } 603 604 @Override 605 public boolean onTouchEvent(MotionEvent ev) { 606 if (mVelocityTracker == null) { 607 mVelocityTracker = VelocityTracker.obtain(); 608 } 609 mVelocityTracker.addMovement(ev); 610 611 final int action = ev.getActionMasked(); 612 switch (action) { 613 case MotionEvent.ACTION_DOWN: 614 mBlockDrag = false; 615 mGestureStartX = ev.getX(); 616 mGestureStartY = ev.getY(); 617 break; 618 619 case MotionEvent.ACTION_CANCEL: 620 if (mDragging && !isChallengeInteractionBlocked()) { 621 showChallenge(0); 622 } 623 resetTouch(); 624 break; 625 626 case MotionEvent.ACTION_POINTER_UP: 627 if (mActivePointerId != ev.getPointerId(ev.getActionIndex())) { 628 break; 629 } 630 case MotionEvent.ACTION_UP: 631 if (mDragging && !isChallengeInteractionBlocked()) { 632 mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); 633 showChallenge((int) mVelocityTracker.getYVelocity(mActivePointerId)); 634 } 635 resetTouch(); 636 break; 637 638 case MotionEvent.ACTION_MOVE: 639 if (!mDragging && !mBlockDrag && !mIsBouncing) { 640 final int count = ev.getPointerCount(); 641 for (int i = 0; i < count; i++) { 642 final float x = ev.getX(i); 643 final float y = ev.getY(i); 644 645 if ((isInDragHandle(x, y) || crossedDragHandle(x, y, mGestureStartY) || 646 (isInChallengeView(x, y) && mScrollState == SCROLL_STATE_SETTLING)) 647 && mActivePointerId == INVALID_POINTER 648 && !isChallengeInteractionBlocked()) { 649 mGestureStartX = x; 650 mGestureStartY = y; 651 mActivePointerId = ev.getPointerId(i); 652 mGestureStartChallengeBottom = getChallengeBottom(); 653 mDragging = true; 654 mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null); 655 break; 656 } 657 } 658 } 659 // Not an else; this can be set above. 660 if (mDragging) { 661 // No-op if already in this state, but set it here in case we arrived 662 // at this point from either intercept or the above. 663 setScrollState(SCROLL_STATE_DRAGGING); 664 665 final int index = ev.findPointerIndex(mActivePointerId); 666 if (index < 0) { 667 // Oops, bogus state. We lost some touch events somewhere. 668 // Just drop it with no velocity and let things settle. 669 resetTouch(); 670 showChallenge(0); 671 return true; 672 } 673 final float y = ev.getY(index); 674 final float pos = Math.min(y - mGestureStartY, 675 getLayoutBottom() - mChallengeBottomBound); 676 677 moveChallengeTo(mGestureStartChallengeBottom + (int) pos); 678 } 679 break; 680 } 681 return true; 682 } 683 684 /** 685 * The lifecycle of touch events is subtle and it's very easy to do something 686 * that will cause bugs that will be nasty to track when overriding this method. 687 * Normally one should always override onInterceptTouchEvent instead. 688 * 689 * To put it another way, don't try this at home. 690 */ 691 @Override 692 public boolean dispatchTouchEvent(MotionEvent ev) { 693 final int action = ev.getActionMasked(); 694 boolean handled = false; 695 if (action == MotionEvent.ACTION_DOWN) { 696 // Defensive programming: if we didn't get the UP or CANCEL, reset anyway. 697 mEdgeCaptured = false; 698 } 699 if (mWidgetsView != null && !mIsBouncing && (mEdgeCaptured || isEdgeSwipeBeginEvent(ev))) { 700 // Normally we would need to do a lot of extra stuff here. 701 // We can only get away with this because we haven't padded in 702 // the widget pager or otherwise transformed it during layout. 703 // We also don't support things like splitting MotionEvents. 704 705 // We set handled to captured even if dispatch is returning false here so that 706 // we don't send a different view a busted or incomplete event stream. 707 handled = mEdgeCaptured |= mWidgetsView.dispatchTouchEvent(ev); 708 } 709 710 if (!handled && !mEdgeCaptured) { 711 handled = super.dispatchTouchEvent(ev); 712 } 713 714 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { 715 mEdgeCaptured = false; 716 } 717 718 return handled; 719 } 720 721 private boolean isEdgeSwipeBeginEvent(MotionEvent ev) { 722 if (ev.getActionMasked() != MotionEvent.ACTION_DOWN) { 723 return false; 724 } 725 726 final float x = ev.getX(); 727 return x < mDragHandleEdgeSlop || x >= getWidth() - mDragHandleEdgeSlop; 728 } 729 730 /** 731 * We only want to add additional vertical space to the drag handle when the panel is fully 732 * closed. 733 */ 734 private int getDragHandleSizeAbove() { 735 return isChallengeShowing() ? mDragHandleOpenAbove : mDragHandleClosedAbove; 736 } 737 private int getDragHandleSizeBelow() { 738 return isChallengeShowing() ? mDragHandleOpenBelow : mDragHandleClosedBelow; 739 } 740 741 private boolean isInChallengeView(float x, float y) { 742 return isPointInView(x, y, mChallengeView); 743 } 744 745 private boolean isInDragHandle(float x, float y) { 746 return isPointInView(x, y, mExpandChallengeView); 747 } 748 749 private boolean isPointInView(float x, float y, View view) { 750 if (view == null) { 751 return false; 752 } 753 return x >= view.getLeft() && y >= view.getTop() 754 && x < view.getRight() && y < view.getBottom(); 755 } 756 757 private boolean crossedDragHandle(float x, float y, float initialY) { 758 759 final int challengeTop = mChallengeView.getTop(); 760 final boolean horizOk = x >= 0 && x < getWidth(); 761 762 final boolean vertOk; 763 if (mChallengeShowing) { 764 vertOk = initialY < (challengeTop - getDragHandleSizeAbove()) && 765 y > challengeTop + getDragHandleSizeBelow(); 766 } else { 767 vertOk = initialY > challengeTop + getDragHandleSizeBelow() && 768 y < challengeTop - getDragHandleSizeAbove(); 769 } 770 return horizOk && vertOk; 771 } 772 773 private int makeChildMeasureSpec(int maxSize, int childDimen) { 774 final int mode; 775 final int size; 776 switch (childDimen) { 777 case LayoutParams.WRAP_CONTENT: 778 mode = MeasureSpec.AT_MOST; 779 size = maxSize; 780 break; 781 case LayoutParams.MATCH_PARENT: 782 mode = MeasureSpec.EXACTLY; 783 size = maxSize; 784 break; 785 default: 786 mode = MeasureSpec.EXACTLY; 787 size = Math.min(maxSize, childDimen); 788 break; 789 } 790 return MeasureSpec.makeMeasureSpec(size, mode); 791 } 792 793 @Override 794 protected void onMeasure(int widthSpec, int heightSpec) { 795 if (MeasureSpec.getMode(widthSpec) != MeasureSpec.EXACTLY || 796 MeasureSpec.getMode(heightSpec) != MeasureSpec.EXACTLY) { 797 throw new IllegalArgumentException( 798 "SlidingChallengeLayout must be measured with an exact size"); 799 } 800 801 final int width = MeasureSpec.getSize(widthSpec); 802 final int height = MeasureSpec.getSize(heightSpec); 803 setMeasuredDimension(width, height); 804 805 // Find one and only one challenge view. 806 final View oldChallengeView = mChallengeView; 807 final View oldExpandChallengeView = mChallengeView; 808 mChallengeView = null; 809 mExpandChallengeView = null; 810 final int count = getChildCount(); 811 812 // First iteration through the children finds special children and sets any associated 813 // state. 814 for (int i = 0; i < count; i++) { 815 final View child = getChildAt(i); 816 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 817 if (lp.childType == LayoutParams.CHILD_TYPE_CHALLENGE) { 818 if (mChallengeView != null) { 819 throw new IllegalStateException( 820 "There may only be one child with layout_isChallenge=\"true\""); 821 } 822 if (!(child instanceof KeyguardSecurityContainer)) { 823 throw new IllegalArgumentException( 824 "Challenge must be a KeyguardSecurityContainer"); 825 } 826 mChallengeView = (KeyguardSecurityContainer) child; 827 if (mChallengeView != oldChallengeView) { 828 mChallengeView.setVisibility(mChallengeShowing ? VISIBLE : INVISIBLE); 829 } 830 // We're going to play silly games with the frame's background drawable later. 831 if (!mHasLayout) { 832 // Set up the margin correctly based on our content for the first run. 833 mHasGlowpad = child.findViewById(R.id.keyguard_selector_view) != null; 834 lp.leftMargin = lp.rightMargin = getChallengeMargin(true); 835 } 836 } else if (lp.childType == LayoutParams.CHILD_TYPE_EXPAND_CHALLENGE_HANDLE) { 837 if (mExpandChallengeView != null) { 838 throw new IllegalStateException( 839 "There may only be one child with layout_childType" 840 + "=\"expandChallengeHandle\""); 841 } 842 mExpandChallengeView = child; 843 if (mExpandChallengeView != oldExpandChallengeView) { 844 mExpandChallengeView.setVisibility(mChallengeShowing ? INVISIBLE : VISIBLE); 845 mExpandChallengeView.setOnClickListener(mExpandChallengeClickListener); 846 } 847 } else if (lp.childType == LayoutParams.CHILD_TYPE_SCRIM) { 848 setScrimView(child); 849 } else if (lp.childType == LayoutParams.CHILD_TYPE_WIDGETS) { 850 mWidgetsView = child; 851 } 852 } 853 854 // We want to measure the challenge view first, since the KeyguardWidgetPager 855 // needs to do things its measure pass that are dependent on the challenge view 856 // having been measured. 857 if (mChallengeView != null && mChallengeView.getVisibility() != View.GONE) { 858 // This one's a little funny. If the IME is present - reported in the form 859 // of insets on the root view - we only give the challenge the space it would 860 // have had if the IME wasn't there in order to keep the rest of the layout stable. 861 // We base this on the layout_maxHeight on the challenge view. If it comes out 862 // negative or zero, either we didn't have a maxHeight or we're totally out of space, 863 // so give up and measure as if this rule weren't there. 864 int challengeHeightSpec = heightSpec; 865 final View root = getRootView(); 866 if (root != null) { 867 final LayoutParams lp = (LayoutParams) mChallengeView.getLayoutParams(); 868 final int specSize = MeasureSpec.getSize(heightSpec); 869 final int windowHeight = mDisplayMetrics.heightPixels - root.getPaddingTop(); 870 final int diff = windowHeight - specSize; 871 final int maxChallengeHeight = lp.maxHeight - diff; 872 if (maxChallengeHeight > 0) { 873 challengeHeightSpec = makeChildMeasureSpec(maxChallengeHeight, lp.height); 874 } 875 } 876 measureChildWithMargins(mChallengeView, widthSpec, 0, challengeHeightSpec, 0); 877 } 878 879 // Measure the rest of the children 880 for (int i = 0; i < count; i++) { 881 final View child = getChildAt(i); 882 if (child.getVisibility() == GONE) { 883 continue; 884 } 885 // Don't measure the challenge view twice! 886 if (child == mChallengeView) continue; 887 888 // Measure children. Widget frame measures special, so that we can ignore 889 // insets for the IME. 890 int parentWidthSpec = widthSpec, parentHeightSpec = heightSpec; 891 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 892 if (lp.childType == LayoutParams.CHILD_TYPE_WIDGETS) { 893 final View root = getRootView(); 894 if (root != null) { 895 // This calculation is super dodgy and relies on several assumptions. 896 // Specifically that the root of the window will be padded in for insets 897 // and that the window is LAYOUT_IN_SCREEN. 898 final int windowWidth = mDisplayMetrics.widthPixels; 899 final int windowHeight = mDisplayMetrics.heightPixels - root.getPaddingTop(); 900 parentWidthSpec = MeasureSpec.makeMeasureSpec( 901 windowWidth, MeasureSpec.EXACTLY); 902 parentHeightSpec = MeasureSpec.makeMeasureSpec( 903 windowHeight, MeasureSpec.EXACTLY); 904 } 905 } 906 measureChildWithMargins(child, parentWidthSpec, 0, parentHeightSpec, 0); 907 } 908 } 909 910 @Override 911 protected void onLayout(boolean changed, int l, int t, int r, int b) { 912 final int paddingLeft = getPaddingLeft(); 913 final int paddingTop = getPaddingTop(); 914 final int paddingRight = getPaddingRight(); 915 final int paddingBottom = getPaddingBottom(); 916 final int width = r - l; 917 final int height = b - t; 918 919 final int count = getChildCount(); 920 for (int i = 0; i < count; i++) { 921 final View child = getChildAt(i); 922 923 if (child.getVisibility() == GONE) continue; 924 925 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 926 927 if (lp.childType == LayoutParams.CHILD_TYPE_CHALLENGE) { 928 // Challenge views pin to the bottom, offset by a portion of their height, 929 // and center horizontally. 930 final int center = (paddingLeft + width - paddingRight) / 2; 931 final int childWidth = child.getMeasuredWidth(); 932 final int childHeight = child.getMeasuredHeight(); 933 final int left = center - childWidth / 2; 934 final int layoutBottom = height - paddingBottom - lp.bottomMargin; 935 // We use the top of the challenge view to position the handle, so 936 // we never want less than the handle size showing at the bottom. 937 final int bottom = layoutBottom + (int) ((childHeight - mChallengeBottomBound) 938 * (1 - mChallengeOffset)); 939 child.setAlpha(getChallengeAlpha()); 940 child.layout(left, bottom - childHeight, left + childWidth, bottom); 941 } else if (lp.childType == LayoutParams.CHILD_TYPE_EXPAND_CHALLENGE_HANDLE) { 942 final int center = (paddingLeft + width - paddingRight) / 2; 943 final int left = center - child.getMeasuredWidth() / 2; 944 final int right = left + child.getMeasuredWidth(); 945 final int bottom = height - paddingBottom - lp.bottomMargin; 946 final int top = bottom - child.getMeasuredHeight(); 947 child.layout(left, top, right, bottom); 948 } else { 949 // Non-challenge views lay out from the upper left, layered. 950 child.layout(paddingLeft + lp.leftMargin, 951 paddingTop + lp.topMargin, 952 paddingLeft + child.getMeasuredWidth(), 953 paddingTop + child.getMeasuredHeight()); 954 } 955 } 956 957 if (!mHasLayout) { 958 mHasLayout = true; 959 } 960 } 961 962 @Override 963 public void draw(Canvas c) { 964 super.draw(c); 965 if (DEBUG) { 966 final Paint debugPaint = new Paint(); 967 debugPaint.setColor(0x40FF00CC); 968 // show the isInDragHandle() rect 969 c.drawRect(mDragHandleEdgeSlop, 970 mChallengeView.getTop() - getDragHandleSizeAbove(), 971 getWidth() - mDragHandleEdgeSlop, 972 mChallengeView.getTop() + getDragHandleSizeBelow(), 973 debugPaint); 974 } 975 } 976 977 public void computeScroll() { 978 super.computeScroll(); 979 980 if (!mScroller.isFinished()) { 981 if (mChallengeView == null) { 982 // Can't scroll if the view is missing. 983 Log.e(TAG, "Challenge view missing in computeScroll"); 984 mScroller.abortAnimation(); 985 return; 986 } 987 988 mScroller.computeScrollOffset(); 989 moveChallengeTo(mScroller.getCurrY()); 990 991 if (mScroller.isFinished()) { 992 post(mEndScrollRunnable); 993 } 994 } 995 } 996 997 private void cancelTransitionsInProgress() { 998 if (!mScroller.isFinished()) { 999 mScroller.abortAnimation(); 1000 completeChallengeScroll(); 1001 } 1002 if (mFader != null) { 1003 mFader.cancel(); 1004 } 1005 } 1006 1007 public void fadeInChallenge() { 1008 fadeChallenge(true); 1009 } 1010 1011 public void fadeOutChallenge() { 1012 fadeChallenge(false); 1013 } 1014 1015 public void fadeChallenge(final boolean show) { 1016 if (mChallengeView != null) { 1017 1018 cancelTransitionsInProgress(); 1019 float alpha = show ? 1f : 0f; 1020 int duration = show ? CHALLENGE_FADE_IN_DURATION : CHALLENGE_FADE_OUT_DURATION; 1021 mFader = ObjectAnimator.ofFloat(mChallengeView, "alpha", alpha); 1022 mFader.addListener(new AnimatorListenerAdapter() { 1023 @Override 1024 public void onAnimationStart(Animator animation) { 1025 onFadeStart(show); 1026 } 1027 @Override 1028 public void onAnimationEnd(Animator animation) { 1029 onFadeEnd(show); 1030 } 1031 }); 1032 mFader.setDuration(duration); 1033 mFader.start(); 1034 } 1035 } 1036 1037 private int getMaxChallengeBottom() { 1038 if (mChallengeView == null) return 0; 1039 final int layoutBottom = getLayoutBottom(); 1040 final int challengeHeight = mChallengeView.getMeasuredHeight(); 1041 1042 return (layoutBottom + challengeHeight - mChallengeBottomBound); 1043 } 1044 1045 private int getMinChallengeBottom() { 1046 return getLayoutBottom(); 1047 } 1048 1049 1050 private void onFadeStart(boolean show) { 1051 mChallengeInteractiveInternal = false; 1052 mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null); 1053 1054 if (show) { 1055 moveChallengeTo(getMinChallengeBottom()); 1056 } 1057 1058 setScrollState(SCROLL_STATE_FADING); 1059 } 1060 1061 private void onFadeEnd(boolean show) { 1062 mChallengeInteractiveInternal = true; 1063 setChallengeShowing(show); 1064 1065 if (!show) { 1066 moveChallengeTo(getMaxChallengeBottom()); 1067 } 1068 1069 mChallengeView.setLayerType(LAYER_TYPE_NONE, null); 1070 mFader = null; 1071 setScrollState(SCROLL_STATE_IDLE); 1072 } 1073 1074 public int getMaxChallengeTop() { 1075 if (mChallengeView == null) return 0; 1076 1077 final int layoutBottom = getLayoutBottom(); 1078 final int challengeHeight = mChallengeView.getMeasuredHeight(); 1079 return layoutBottom - challengeHeight; 1080 } 1081 1082 /** 1083 * Move the bottom edge of mChallengeView to a new position and notify the listener 1084 * if it represents a change in position. Changes made through this method will 1085 * be stable across layout passes. If this method is called before first layout of 1086 * this SlidingChallengeLayout it will have no effect. 1087 * 1088 * @param bottom New bottom edge in px in this SlidingChallengeLayout's coordinate system. 1089 * @return true if the challenge view was moved 1090 */ 1091 private boolean moveChallengeTo(int bottom) { 1092 if (mChallengeView == null || !mHasLayout) { 1093 return false; 1094 } 1095 1096 final int layoutBottom = getLayoutBottom(); 1097 final int challengeHeight = mChallengeView.getHeight(); 1098 1099 bottom = Math.max(getMinChallengeBottom(), 1100 Math.min(bottom, getMaxChallengeBottom())); 1101 1102 float offset = 1.f - (float) (bottom - layoutBottom) / 1103 (challengeHeight - mChallengeBottomBound); 1104 mChallengeOffset = offset; 1105 if (offset > 0 && !mChallengeShowing) { 1106 setChallengeShowing(true); 1107 } 1108 1109 mChallengeView.layout(mChallengeView.getLeft(), 1110 bottom - mChallengeView.getHeight(), mChallengeView.getRight(), bottom); 1111 1112 mChallengeView.setAlpha(getChallengeAlpha()); 1113 if (mScrollListener != null) { 1114 mScrollListener.onScrollPositionChanged(offset, mChallengeView.getTop()); 1115 } 1116 postInvalidateOnAnimation(); 1117 return true; 1118 } 1119 1120 /** 1121 * The bottom edge of this SlidingChallengeLayout's coordinate system; will coincide with 1122 * the bottom edge of mChallengeView when the challenge is fully opened. 1123 */ 1124 private int getLayoutBottom() { 1125 final int bottomMargin = (mChallengeView == null) 1126 ? 0 1127 : ((LayoutParams) mChallengeView.getLayoutParams()).bottomMargin; 1128 final int layoutBottom = getMeasuredHeight() - getPaddingBottom() - bottomMargin; 1129 return layoutBottom; 1130 } 1131 1132 /** 1133 * The bottom edge of mChallengeView; essentially, where the sliding challenge 'is'. 1134 */ 1135 private int getChallengeBottom() { 1136 if (mChallengeView == null) return 0; 1137 1138 return mChallengeView.getBottom(); 1139 } 1140 1141 /** 1142 * Show or hide the challenge view, animating it if necessary. 1143 * @param show true to show, false to hide 1144 */ 1145 public void showChallenge(boolean show) { 1146 showChallenge(show, 0); 1147 if (!show) { 1148 // Block any drags in progress so that callers can use this to disable dragging 1149 // for other touch interactions. 1150 mBlockDrag = true; 1151 } 1152 } 1153 1154 private void showChallenge(int velocity) { 1155 boolean show = false; 1156 if (Math.abs(velocity) > mMinVelocity) { 1157 show = velocity < 0; 1158 } else { 1159 show = mChallengeOffset >= 0.5f; 1160 } 1161 showChallenge(show, velocity); 1162 } 1163 1164 private void showChallenge(boolean show, int velocity) { 1165 if (mChallengeView == null) { 1166 setChallengeShowing(false); 1167 return; 1168 } 1169 1170 if (mHasLayout) { 1171 mChallengeShowingTargetState = show; 1172 final int layoutBottom = getLayoutBottom(); 1173 animateChallengeTo(show ? layoutBottom : 1174 layoutBottom + mChallengeView.getHeight() - mChallengeBottomBound, velocity); 1175 } 1176 } 1177 1178 @Override 1179 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 1180 return new LayoutParams(getContext(), attrs); 1181 } 1182 1183 @Override 1184 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 1185 return p instanceof LayoutParams ? new LayoutParams((LayoutParams) p) : 1186 p instanceof MarginLayoutParams ? new LayoutParams((MarginLayoutParams) p) : 1187 new LayoutParams(p); 1188 } 1189 1190 @Override 1191 protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 1192 return new LayoutParams(); 1193 } 1194 1195 @Override 1196 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 1197 return p instanceof LayoutParams; 1198 } 1199 1200 public static class LayoutParams extends MarginLayoutParams { 1201 public int childType = CHILD_TYPE_NONE; 1202 public static final int CHILD_TYPE_NONE = 0; 1203 public static final int CHILD_TYPE_CHALLENGE = 2; 1204 public static final int CHILD_TYPE_SCRIM = 4; 1205 public static final int CHILD_TYPE_WIDGETS = 5; 1206 public static final int CHILD_TYPE_EXPAND_CHALLENGE_HANDLE = 6; 1207 1208 public int maxHeight; 1209 1210 public LayoutParams() { 1211 this(MATCH_PARENT, WRAP_CONTENT); 1212 } 1213 1214 public LayoutParams(int width, int height) { 1215 super(width, height); 1216 } 1217 1218 public LayoutParams(android.view.ViewGroup.LayoutParams source) { 1219 super(source); 1220 } 1221 1222 public LayoutParams(MarginLayoutParams source) { 1223 super(source); 1224 } 1225 1226 public LayoutParams(LayoutParams source) { 1227 super(source); 1228 1229 childType = source.childType; 1230 } 1231 1232 public LayoutParams(Context c, AttributeSet attrs) { 1233 super(c, attrs); 1234 1235 final TypedArray a = c.obtainStyledAttributes(attrs, 1236 R.styleable.SlidingChallengeLayout_Layout); 1237 childType = a.getInt(R.styleable.SlidingChallengeLayout_Layout_layout_childType, 1238 CHILD_TYPE_NONE); 1239 maxHeight = a.getDimensionPixelSize( 1240 R.styleable.SlidingChallengeLayout_Layout_layout_maxHeight, 0); 1241 a.recycle(); 1242 } 1243 } 1244} 1245