SlidingChallengeLayout.java revision 256ae67b9a629178b458dfc01102c9c0b9963d03
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); 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 mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null); 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 mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null); 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 mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null); 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 mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null); 1069 1070 if (show) { 1071 moveChallengeTo(getMinChallengeBottom()); 1072 } 1073 1074 setScrollState(SCROLL_STATE_FADING); 1075 } 1076 1077 private void onFadeEnd(boolean show) { 1078 mChallengeInteractiveInternal = true; 1079 setChallengeShowing(show); 1080 1081 if (!show) { 1082 moveChallengeTo(getMaxChallengeBottom()); 1083 } 1084 1085 mChallengeView.setLayerType(LAYER_TYPE_NONE, null); 1086 mFader = null; 1087 setScrollState(SCROLL_STATE_IDLE); 1088 } 1089 1090 public int getMaxChallengeTop() { 1091 if (mChallengeView == null) return 0; 1092 1093 final int layoutBottom = getLayoutBottom(); 1094 final int challengeHeight = mChallengeView.getMeasuredHeight(); 1095 return layoutBottom - challengeHeight - mInsets.top; 1096 } 1097 1098 /** 1099 * Move the bottom edge of mChallengeView to a new position and notify the listener 1100 * if it represents a change in position. Changes made through this method will 1101 * be stable across layout passes. If this method is called before first layout of 1102 * this SlidingChallengeLayout it will have no effect. 1103 * 1104 * @param bottom New bottom edge in px in this SlidingChallengeLayout's coordinate system. 1105 * @return true if the challenge view was moved 1106 */ 1107 private boolean moveChallengeTo(int bottom) { 1108 if (mChallengeView == null || !mHasLayout) { 1109 return false; 1110 } 1111 1112 final int layoutBottom = getLayoutBottom(); 1113 final int challengeHeight = mChallengeView.getHeight(); 1114 1115 bottom = Math.max(getMinChallengeBottom(), 1116 Math.min(bottom, getMaxChallengeBottom())); 1117 1118 float offset = 1.f - (float) (bottom - layoutBottom) / 1119 (challengeHeight - mChallengeBottomBound); 1120 mChallengeOffset = offset; 1121 if (offset > 0 && !mChallengeShowing) { 1122 setChallengeShowing(true); 1123 } 1124 1125 mChallengeView.layout(mChallengeView.getLeft(), 1126 bottom - mChallengeView.getHeight(), mChallengeView.getRight(), bottom); 1127 1128 mChallengeView.setAlpha(getChallengeAlpha()); 1129 if (mScrollListener != null) { 1130 mScrollListener.onScrollPositionChanged(offset, mChallengeView.getTop()); 1131 } 1132 postInvalidateOnAnimation(); 1133 return true; 1134 } 1135 1136 /** 1137 * The bottom edge of this SlidingChallengeLayout's coordinate system; will coincide with 1138 * the bottom edge of mChallengeView when the challenge is fully opened. 1139 */ 1140 private int getLayoutBottom() { 1141 final int bottomMargin = (mChallengeView == null) 1142 ? 0 1143 : ((LayoutParams) mChallengeView.getLayoutParams()).bottomMargin; 1144 final int layoutBottom = getMeasuredHeight() - getPaddingBottom() - bottomMargin 1145 - mInsets.bottom; 1146 return layoutBottom; 1147 } 1148 1149 /** 1150 * The bottom edge of mChallengeView; essentially, where the sliding challenge 'is'. 1151 */ 1152 private int getChallengeBottom() { 1153 if (mChallengeView == null) return 0; 1154 1155 return mChallengeView.getBottom(); 1156 } 1157 1158 /** 1159 * Show or hide the challenge view, animating it if necessary. 1160 * @param show true to show, false to hide 1161 */ 1162 public void showChallenge(boolean show) { 1163 showChallenge(show, 0); 1164 if (!show) { 1165 // Block any drags in progress so that callers can use this to disable dragging 1166 // for other touch interactions. 1167 mBlockDrag = true; 1168 } 1169 } 1170 1171 private void showChallenge(int velocity) { 1172 boolean show = false; 1173 if (Math.abs(velocity) > mMinVelocity) { 1174 show = velocity < 0; 1175 } else { 1176 show = mChallengeOffset >= 0.5f; 1177 } 1178 showChallenge(show, velocity); 1179 } 1180 1181 private void showChallenge(boolean show, int velocity) { 1182 if (mChallengeView == null) { 1183 setChallengeShowing(false); 1184 return; 1185 } 1186 1187 if (mHasLayout) { 1188 mChallengeShowingTargetState = show; 1189 final int layoutBottom = getLayoutBottom(); 1190 animateChallengeTo(show ? layoutBottom : 1191 layoutBottom + mChallengeView.getHeight() - mChallengeBottomBound, velocity); 1192 } 1193 } 1194 1195 @Override 1196 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 1197 return new LayoutParams(getContext(), attrs); 1198 } 1199 1200 @Override 1201 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 1202 return p instanceof LayoutParams ? new LayoutParams((LayoutParams) p) : 1203 p instanceof MarginLayoutParams ? new LayoutParams((MarginLayoutParams) p) : 1204 new LayoutParams(p); 1205 } 1206 1207 @Override 1208 protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 1209 return new LayoutParams(); 1210 } 1211 1212 @Override 1213 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 1214 return p instanceof LayoutParams; 1215 } 1216 1217 public static class LayoutParams extends MarginLayoutParams { 1218 public int childType = CHILD_TYPE_NONE; 1219 public static final int CHILD_TYPE_NONE = 0; 1220 public static final int CHILD_TYPE_CHALLENGE = 2; 1221 public static final int CHILD_TYPE_SCRIM = 4; 1222 public static final int CHILD_TYPE_WIDGETS = 5; 1223 public static final int CHILD_TYPE_EXPAND_CHALLENGE_HANDLE = 6; 1224 1225 public int maxHeight; 1226 1227 public LayoutParams() { 1228 this(MATCH_PARENT, WRAP_CONTENT); 1229 } 1230 1231 public LayoutParams(int width, int height) { 1232 super(width, height); 1233 } 1234 1235 public LayoutParams(android.view.ViewGroup.LayoutParams source) { 1236 super(source); 1237 } 1238 1239 public LayoutParams(MarginLayoutParams source) { 1240 super(source); 1241 } 1242 1243 public LayoutParams(LayoutParams source) { 1244 super(source); 1245 1246 childType = source.childType; 1247 } 1248 1249 public LayoutParams(Context c, AttributeSet attrs) { 1250 super(c, attrs); 1251 1252 final TypedArray a = c.obtainStyledAttributes(attrs, 1253 R.styleable.SlidingChallengeLayout_Layout); 1254 childType = a.getInt(R.styleable.SlidingChallengeLayout_Layout_layout_childType, 1255 CHILD_TYPE_NONE); 1256 maxHeight = a.getDimensionPixelSize( 1257 R.styleable.SlidingChallengeLayout_Layout_layout_maxHeight, 0); 1258 a.recycle(); 1259 } 1260 } 1261} 1262