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