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