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