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