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