| /* |
| * Copyright (C) 2012 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.internal.policy.impl.keyguard; |
| |
| import com.android.internal.R; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.ObjectAnimator; |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.content.res.TypedArray; |
| import android.graphics.Canvas; |
| import android.graphics.Paint; |
| import android.util.AttributeSet; |
| import android.util.DisplayMetrics; |
| import android.util.FloatProperty; |
| import android.util.Log; |
| import android.util.Property; |
| import android.view.MotionEvent; |
| import android.view.VelocityTracker; |
| import android.view.View; |
| import android.view.ViewConfiguration; |
| import android.view.ViewGroup; |
| import android.view.accessibility.AccessibilityManager; |
| import android.view.animation.Interpolator; |
| import android.widget.Scroller; |
| |
| /** |
| * This layout handles interaction with the sliding security challenge views |
| * that overlay/resize other keyguard contents. |
| */ |
| public class SlidingChallengeLayout extends ViewGroup implements ChallengeLayout { |
| private static final String TAG = "SlidingChallengeLayout"; |
| private static final boolean DEBUG = false; |
| |
| // The drag handle is measured in dp above & below the top edge of the |
| // challenge view; these parameters change based on whether the challenge |
| // is open or closed. |
| private static final int DRAG_HANDLE_CLOSED_ABOVE = 8; // dp |
| private static final int DRAG_HANDLE_CLOSED_BELOW = 0; // dp |
| private static final int DRAG_HANDLE_OPEN_ABOVE = 8; // dp |
| private static final int DRAG_HANDLE_OPEN_BELOW = 0; // dp |
| |
| private static final int HANDLE_ANIMATE_DURATION = 250; // ms |
| |
| // Drawn to show the drag handle in closed state; crossfades to the challenge view |
| // when challenge is fully visible |
| private boolean mEdgeCaptured; |
| |
| private DisplayMetrics mDisplayMetrics; |
| |
| // Initialized during measurement from child layoutparams |
| private View mExpandChallengeView; |
| private KeyguardSecurityContainer mChallengeView; |
| private View mScrimView; |
| private View mWidgetsView; |
| |
| // Range: 0 (fully hidden) to 1 (fully visible) |
| private float mChallengeOffset = 1.f; |
| private boolean mChallengeShowing = true; |
| private boolean mChallengeShowingTargetState = true; |
| private boolean mWasChallengeShowing = true; |
| private boolean mIsBouncing = false; |
| |
| private final Scroller mScroller; |
| private ObjectAnimator mFader; |
| private int mScrollState; |
| private OnChallengeScrolledListener mScrollListener; |
| private OnBouncerStateChangedListener mBouncerListener; |
| |
| public static final int SCROLL_STATE_IDLE = 0; |
| public static final int SCROLL_STATE_DRAGGING = 1; |
| public static final int SCROLL_STATE_SETTLING = 2; |
| public static final int SCROLL_STATE_FADING = 3; |
| |
| private static final int CHALLENGE_FADE_OUT_DURATION = 100; |
| private static final int CHALLENGE_FADE_IN_DURATION = 160; |
| |
| private static final int MAX_SETTLE_DURATION = 600; // ms |
| |
| // ID of the pointer in charge of a current drag |
| private int mActivePointerId = INVALID_POINTER; |
| private static final int INVALID_POINTER = -1; |
| |
| // True if the user is currently dragging the slider |
| private boolean mDragging; |
| // True if the user may not drag until a new gesture begins |
| private boolean mBlockDrag; |
| |
| private VelocityTracker mVelocityTracker; |
| private int mMinVelocity; |
| private int mMaxVelocity; |
| private float mGestureStartX, mGestureStartY; // where did you first touch the screen? |
| private int mGestureStartChallengeBottom; // where was the challenge at that time? |
| |
| private int mDragHandleClosedBelow; // handle hitrect extension into the challenge view |
| private int mDragHandleClosedAbove; // extend the handle's hitrect this far above the line |
| private int mDragHandleOpenBelow; // handle hitrect extension into the challenge view |
| private int mDragHandleOpenAbove; // extend the handle's hitrect this far above the line |
| |
| private int mDragHandleEdgeSlop; |
| private int mChallengeBottomBound; // Number of pixels from the top of the challenge view |
| // that should remain on-screen |
| |
| private int mTouchSlop; |
| private int mTouchSlopSquare; |
| |
| float mHandleAlpha; |
| float mFrameAlpha; |
| float mFrameAnimationTarget = Float.MIN_VALUE; |
| private ObjectAnimator mHandleAnimation; |
| private ObjectAnimator mFrameAnimation; |
| |
| private boolean mHasGlowpad; |
| |
| // We have an internal and external version, and we and them together. |
| private boolean mChallengeInteractiveExternal = true; |
| private boolean mChallengeInteractiveInternal = true; |
| |
| static final Property<SlidingChallengeLayout, Float> HANDLE_ALPHA = |
| new FloatProperty<SlidingChallengeLayout>("handleAlpha") { |
| @Override |
| public void setValue(SlidingChallengeLayout view, float value) { |
| view.mHandleAlpha = value; |
| view.invalidate(); |
| } |
| |
| @Override |
| public Float get(SlidingChallengeLayout view) { |
| return view.mHandleAlpha; |
| } |
| }; |
| |
| // True if at least one layout pass has happened since the view was attached. |
| private boolean mHasLayout; |
| |
| private static final Interpolator sMotionInterpolator = new Interpolator() { |
| public float getInterpolation(float t) { |
| t -= 1.0f; |
| return t * t * t * t * t + 1.0f; |
| } |
| }; |
| |
| private static final Interpolator sHandleFadeInterpolator = new Interpolator() { |
| public float getInterpolation(float t) { |
| return t * t; |
| } |
| }; |
| |
| private final Runnable mEndScrollRunnable = new Runnable () { |
| public void run() { |
| completeChallengeScroll(); |
| } |
| }; |
| |
| private final OnClickListener mScrimClickListener = new OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| hideBouncer(); |
| } |
| }; |
| |
| private final OnClickListener mExpandChallengeClickListener = new OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| if (!isChallengeShowing()) { |
| showChallenge(true); |
| } |
| } |
| }; |
| |
| /** |
| * Listener interface that reports changes in scroll state of the challenge area. |
| */ |
| public interface OnChallengeScrolledListener { |
| /** |
| * The scroll state itself changed. |
| * |
| * <p>scrollState will be one of the following:</p> |
| * |
| * <ul> |
| * <li><code>SCROLL_STATE_IDLE</code> - The challenge area is stationary.</li> |
| * <li><code>SCROLL_STATE_DRAGGING</code> - The user is actively dragging |
| * the challenge area.</li> |
| * <li><code>SCROLL_STATE_SETTLING</code> - The challenge area is animating |
| * into place.</li> |
| * </ul> |
| * |
| * <p>Do not perform expensive operations (e.g. layout) |
| * while the scroll state is not <code>SCROLL_STATE_IDLE</code>.</p> |
| * |
| * @param scrollState The new scroll state of the challenge area. |
| */ |
| public void onScrollStateChanged(int scrollState); |
| |
| /** |
| * The precise position of the challenge area has changed. |
| * |
| * <p>NOTE: It is NOT safe to modify layout or call any View methods that may |
| * result in a requestLayout anywhere in your view hierarchy as a result of this call. |
| * It may be called during drawing.</p> |
| * |
| * @param scrollPosition New relative position of the challenge area. |
| * 1.f = fully visible/ready to be interacted with. |
| * 0.f = fully invisible/inaccessible to the user. |
| * @param challengeTop Position of the top edge of the challenge view in px in the |
| * SlidingChallengeLayout's coordinate system. |
| */ |
| public void onScrollPositionChanged(float scrollPosition, int challengeTop); |
| } |
| |
| public SlidingChallengeLayout(Context context) { |
| this(context, null); |
| } |
| |
| public SlidingChallengeLayout(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public SlidingChallengeLayout(Context context, AttributeSet attrs, int defStyle) { |
| super(context, attrs, defStyle); |
| |
| mScroller = new Scroller(context, sMotionInterpolator); |
| |
| final ViewConfiguration vc = ViewConfiguration.get(context); |
| mMinVelocity = vc.getScaledMinimumFlingVelocity(); |
| mMaxVelocity = vc.getScaledMaximumFlingVelocity(); |
| |
| final Resources res = getResources(); |
| mDragHandleEdgeSlop = res.getDimensionPixelSize(R.dimen.kg_edge_swipe_region_size); |
| |
| mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); |
| mTouchSlopSquare = mTouchSlop * mTouchSlop; |
| |
| mDisplayMetrics = res.getDisplayMetrics(); |
| final float density = mDisplayMetrics.density; |
| |
| // top half of the lock icon, plus another 25% to be sure |
| mDragHandleClosedAbove = (int) (DRAG_HANDLE_CLOSED_ABOVE * density + 0.5f); |
| mDragHandleClosedBelow = (int) (DRAG_HANDLE_CLOSED_BELOW * density + 0.5f); |
| mDragHandleOpenAbove = (int) (DRAG_HANDLE_OPEN_ABOVE * density + 0.5f); |
| mDragHandleOpenBelow = (int) (DRAG_HANDLE_OPEN_BELOW * density + 0.5f); |
| |
| // how much space to account for in the handle when closed |
| mChallengeBottomBound = res.getDimensionPixelSize(R.dimen.kg_widget_pager_bottom_padding); |
| |
| setWillNotDraw(false); |
| setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_STABLE); |
| } |
| |
| public void setHandleAlpha(float alpha) { |
| if (mExpandChallengeView != null) { |
| mExpandChallengeView.setAlpha(alpha); |
| } |
| } |
| |
| public void setChallengeInteractive(boolean interactive) { |
| mChallengeInteractiveExternal = interactive; |
| if (mExpandChallengeView != null) { |
| mExpandChallengeView.setEnabled(interactive); |
| } |
| } |
| |
| void animateHandle(boolean visible) { |
| if (mHandleAnimation != null) { |
| mHandleAnimation.cancel(); |
| mHandleAnimation = null; |
| } |
| final float targetAlpha = visible ? 1.f : 0.f; |
| if (targetAlpha == mHandleAlpha) { |
| return; |
| } |
| mHandleAnimation = ObjectAnimator.ofFloat(this, HANDLE_ALPHA, targetAlpha); |
| mHandleAnimation.setInterpolator(sHandleFadeInterpolator); |
| mHandleAnimation.setDuration(HANDLE_ANIMATE_DURATION); |
| mHandleAnimation.start(); |
| } |
| |
| private void sendInitialListenerUpdates() { |
| if (mScrollListener != null) { |
| int challengeTop = mChallengeView != null ? mChallengeView.getTop() : 0; |
| mScrollListener.onScrollPositionChanged(mChallengeOffset, challengeTop); |
| mScrollListener.onScrollStateChanged(mScrollState); |
| } |
| } |
| |
| public void setOnChallengeScrolledListener(OnChallengeScrolledListener listener) { |
| mScrollListener = listener; |
| if (mHasLayout) { |
| sendInitialListenerUpdates(); |
| } |
| } |
| |
| public void setOnBouncerStateChangedListener(OnBouncerStateChangedListener listener) { |
| mBouncerListener = listener; |
| } |
| |
| @Override |
| public void onAttachedToWindow() { |
| super.onAttachedToWindow(); |
| |
| mHasLayout = false; |
| } |
| |
| @Override |
| public void onDetachedFromWindow() { |
| super.onDetachedFromWindow(); |
| |
| removeCallbacks(mEndScrollRunnable); |
| mHasLayout = false; |
| } |
| |
| @Override |
| public void requestChildFocus(View child, View focused) { |
| if (mIsBouncing && child != mChallengeView) { |
| // Clear out of the bouncer if the user tries to move focus outside of |
| // the security challenge view. |
| hideBouncer(); |
| } |
| super.requestChildFocus(child, focused); |
| } |
| |
| // We want the duration of the page snap animation to be influenced by the distance that |
| // the screen has to travel, however, we don't want this duration to be effected in a |
| // purely linear fashion. Instead, we use this method to moderate the effect that the distance |
| // of travel has on the overall snap duration. |
| float distanceInfluenceForSnapDuration(float f) { |
| f -= 0.5f; // center the values about 0. |
| f *= 0.3f * Math.PI / 2.0f; |
| return (float) Math.sin(f); |
| } |
| |
| void setScrollState(int state) { |
| if (mScrollState != state) { |
| mScrollState = state; |
| |
| animateHandle(state == SCROLL_STATE_IDLE && !mChallengeShowing); |
| if (mScrollListener != null) { |
| mScrollListener.onScrollStateChanged(state); |
| } |
| } |
| } |
| |
| void completeChallengeScroll() { |
| setChallengeShowing(mChallengeShowingTargetState); |
| mChallengeOffset = mChallengeShowing ? 1.f : 0.f; |
| setScrollState(SCROLL_STATE_IDLE); |
| mChallengeInteractiveInternal = true; |
| mChallengeView.setLayerType(LAYER_TYPE_NONE, null); |
| } |
| |
| void setScrimView(View scrim) { |
| if (mScrimView != null) { |
| mScrimView.setOnClickListener(null); |
| } |
| mScrimView = scrim; |
| mScrimView.setVisibility(mIsBouncing ? VISIBLE : GONE); |
| mScrimView.setFocusable(true); |
| mScrimView.setOnClickListener(mScrimClickListener); |
| } |
| |
| /** |
| * Animate the bottom edge of the challenge view to the given position. |
| * |
| * @param y desired final position for the bottom edge of the challenge view in px |
| * @param velocity velocity in |
| */ |
| void animateChallengeTo(int y, int velocity) { |
| if (mChallengeView == null) { |
| // Nothing to do. |
| return; |
| } |
| |
| cancelTransitionsInProgress(); |
| |
| mChallengeInteractiveInternal = false; |
| mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null); |
| final int sy = mChallengeView.getBottom(); |
| final int dy = y - sy; |
| if (dy == 0) { |
| completeChallengeScroll(); |
| return; |
| } |
| |
| setScrollState(SCROLL_STATE_SETTLING); |
| |
| final int childHeight = mChallengeView.getHeight(); |
| final int halfHeight = childHeight / 2; |
| final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / childHeight); |
| final float distance = halfHeight + halfHeight * |
| distanceInfluenceForSnapDuration(distanceRatio); |
| |
| int duration = 0; |
| velocity = Math.abs(velocity); |
| if (velocity > 0) { |
| duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); |
| } else { |
| final float childDelta = (float) Math.abs(dy) / childHeight; |
| duration = (int) ((childDelta + 1) * 100); |
| } |
| duration = Math.min(duration, MAX_SETTLE_DURATION); |
| |
| mScroller.startScroll(0, sy, 0, dy, duration); |
| postInvalidateOnAnimation(); |
| } |
| |
| private void setChallengeShowing(boolean showChallenge) { |
| if (mChallengeShowing == showChallenge) { |
| return; |
| } |
| mChallengeShowing = showChallenge; |
| |
| if (mExpandChallengeView == null || mChallengeView == null) { |
| // These might not be here yet if we haven't been through layout. |
| // If we haven't, the first layout pass will set everything up correctly |
| // based on mChallengeShowing as set above. |
| return; |
| } |
| |
| if (mChallengeShowing) { |
| mExpandChallengeView.setVisibility(View.INVISIBLE); |
| mChallengeView.setVisibility(View.VISIBLE); |
| if (AccessibilityManager.getInstance(mContext).isEnabled()) { |
| mChallengeView.requestAccessibilityFocus(); |
| mChallengeView.announceForAccessibility(mContext.getString( |
| R.string.keyguard_accessibility_unlock_area_expanded)); |
| } |
| } else { |
| mExpandChallengeView.setVisibility(View.VISIBLE); |
| mChallengeView.setVisibility(View.INVISIBLE); |
| if (AccessibilityManager.getInstance(mContext).isEnabled()) { |
| mExpandChallengeView.requestAccessibilityFocus(); |
| mChallengeView.announceForAccessibility(mContext.getString( |
| R.string.keyguard_accessibility_unlock_area_collapsed)); |
| } |
| } |
| } |
| |
| /** |
| * @return true if the challenge is at all visible. |
| */ |
| public boolean isChallengeShowing() { |
| return mChallengeShowing; |
| } |
| |
| @Override |
| public boolean isChallengeOverlapping() { |
| return mChallengeShowing; |
| } |
| |
| @Override |
| public boolean isBouncing() { |
| return mIsBouncing; |
| } |
| |
| @Override |
| public int getBouncerAnimationDuration() { |
| return HANDLE_ANIMATE_DURATION; |
| } |
| |
| @Override |
| public void showBouncer() { |
| if (mIsBouncing) return; |
| mWasChallengeShowing = mChallengeShowing; |
| mIsBouncing = true; |
| showChallenge(true); |
| if (mScrimView != null) { |
| Animator anim = ObjectAnimator.ofFloat(mScrimView, "alpha", 1f); |
| anim.setDuration(HANDLE_ANIMATE_DURATION); |
| anim.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationStart(Animator animation) { |
| mScrimView.setVisibility(VISIBLE); |
| } |
| }); |
| anim.start(); |
| } |
| if (mChallengeView != null) { |
| mChallengeView.showBouncer(HANDLE_ANIMATE_DURATION); |
| } |
| |
| if (mBouncerListener != null) { |
| mBouncerListener.onBouncerStateChanged(true); |
| } |
| } |
| |
| @Override |
| public void hideBouncer() { |
| if (!mIsBouncing) return; |
| if (!mWasChallengeShowing) showChallenge(false); |
| mIsBouncing = false; |
| |
| if (mScrimView != null) { |
| Animator anim = ObjectAnimator.ofFloat(mScrimView, "alpha", 0f); |
| anim.setDuration(HANDLE_ANIMATE_DURATION); |
| anim.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| mScrimView.setVisibility(GONE); |
| } |
| }); |
| anim.start(); |
| } |
| if (mChallengeView != null) { |
| mChallengeView.hideBouncer(HANDLE_ANIMATE_DURATION); |
| } |
| if (mBouncerListener != null) { |
| mBouncerListener.onBouncerStateChanged(false); |
| } |
| } |
| |
| private int getChallengeMargin(boolean expanded) { |
| return expanded && mHasGlowpad ? 0 : mDragHandleEdgeSlop; |
| } |
| |
| private float getChallengeAlpha() { |
| float x = mChallengeOffset - 1; |
| return x * x * x + 1.f; |
| } |
| |
| @Override |
| public void requestDisallowInterceptTouchEvent(boolean allowIntercept) { |
| // We'll intercept whoever we feel like! ...as long as it isn't a challenge view. |
| // If there are one or more pointers in the challenge view before we take over |
| // touch events, onInterceptTouchEvent will set mBlockDrag. |
| } |
| |
| @Override |
| public boolean onInterceptTouchEvent(MotionEvent ev) { |
| if (mVelocityTracker == null) { |
| mVelocityTracker = VelocityTracker.obtain(); |
| } |
| mVelocityTracker.addMovement(ev); |
| |
| final int action = ev.getActionMasked(); |
| switch (action) { |
| case MotionEvent.ACTION_DOWN: |
| mGestureStartX = ev.getX(); |
| mGestureStartY = ev.getY(); |
| mBlockDrag = false; |
| break; |
| |
| case MotionEvent.ACTION_CANCEL: |
| case MotionEvent.ACTION_UP: |
| resetTouch(); |
| break; |
| |
| case MotionEvent.ACTION_MOVE: |
| final int count = ev.getPointerCount(); |
| for (int i = 0; i < count; i++) { |
| final float x = ev.getX(i); |
| final float y = ev.getY(i); |
| if (!mIsBouncing && mActivePointerId == INVALID_POINTER |
| && (crossedDragHandle(x, y, mGestureStartY) |
| || (isInChallengeView(x, y) && |
| mScrollState == SCROLL_STATE_SETTLING))) { |
| mActivePointerId = ev.getPointerId(i); |
| mGestureStartX = x; |
| mGestureStartY = y; |
| mGestureStartChallengeBottom = getChallengeBottom(); |
| mDragging = true; |
| mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null); |
| } else if (mChallengeShowing && isInChallengeView(x, y)) { |
| mBlockDrag = true; |
| } |
| } |
| break; |
| } |
| |
| if (mBlockDrag || isChallengeInteractionBlocked()) { |
| mActivePointerId = INVALID_POINTER; |
| mDragging = false; |
| } |
| |
| return mDragging; |
| } |
| |
| private boolean isChallengeInteractionBlocked() { |
| return !mChallengeInteractiveExternal || !mChallengeInteractiveInternal; |
| } |
| |
| private void resetTouch() { |
| mVelocityTracker.recycle(); |
| mVelocityTracker = null; |
| mActivePointerId = INVALID_POINTER; |
| mDragging = mBlockDrag = false; |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent ev) { |
| if (mVelocityTracker == null) { |
| mVelocityTracker = VelocityTracker.obtain(); |
| } |
| mVelocityTracker.addMovement(ev); |
| |
| final int action = ev.getActionMasked(); |
| switch (action) { |
| case MotionEvent.ACTION_DOWN: |
| mBlockDrag = false; |
| mGestureStartX = ev.getX(); |
| mGestureStartY = ev.getY(); |
| break; |
| |
| case MotionEvent.ACTION_CANCEL: |
| if (mDragging && !isChallengeInteractionBlocked()) { |
| showChallenge(0); |
| } |
| resetTouch(); |
| break; |
| |
| case MotionEvent.ACTION_POINTER_UP: |
| if (mActivePointerId != ev.getPointerId(ev.getActionIndex())) { |
| break; |
| } |
| case MotionEvent.ACTION_UP: |
| if (mDragging && !isChallengeInteractionBlocked()) { |
| mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); |
| showChallenge((int) mVelocityTracker.getYVelocity(mActivePointerId)); |
| } |
| resetTouch(); |
| break; |
| |
| case MotionEvent.ACTION_MOVE: |
| if (!mDragging && !mBlockDrag && !mIsBouncing) { |
| final int count = ev.getPointerCount(); |
| for (int i = 0; i < count; i++) { |
| final float x = ev.getX(i); |
| final float y = ev.getY(i); |
| |
| if ((isInDragHandle(x, y) || crossedDragHandle(x, y, mGestureStartY) || |
| (isInChallengeView(x, y) && mScrollState == SCROLL_STATE_SETTLING)) |
| && mActivePointerId == INVALID_POINTER |
| && !isChallengeInteractionBlocked()) { |
| mGestureStartX = x; |
| mGestureStartY = y; |
| mActivePointerId = ev.getPointerId(i); |
| mGestureStartChallengeBottom = getChallengeBottom(); |
| mDragging = true; |
| mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null); |
| break; |
| } |
| } |
| } |
| // Not an else; this can be set above. |
| if (mDragging) { |
| // No-op if already in this state, but set it here in case we arrived |
| // at this point from either intercept or the above. |
| setScrollState(SCROLL_STATE_DRAGGING); |
| |
| final int index = ev.findPointerIndex(mActivePointerId); |
| if (index < 0) { |
| // Oops, bogus state. We lost some touch events somewhere. |
| // Just drop it with no velocity and let things settle. |
| resetTouch(); |
| showChallenge(0); |
| return true; |
| } |
| final float y = ev.getY(index); |
| final float pos = Math.min(y - mGestureStartY, |
| getLayoutBottom() - mChallengeBottomBound); |
| |
| moveChallengeTo(mGestureStartChallengeBottom + (int) pos); |
| } |
| break; |
| } |
| return true; |
| } |
| |
| /** |
| * The lifecycle of touch events is subtle and it's very easy to do something |
| * that will cause bugs that will be nasty to track when overriding this method. |
| * Normally one should always override onInterceptTouchEvent instead. |
| * |
| * To put it another way, don't try this at home. |
| */ |
| @Override |
| public boolean dispatchTouchEvent(MotionEvent ev) { |
| final int action = ev.getActionMasked(); |
| boolean handled = false; |
| if (action == MotionEvent.ACTION_DOWN) { |
| // Defensive programming: if we didn't get the UP or CANCEL, reset anyway. |
| mEdgeCaptured = false; |
| } |
| if (mWidgetsView != null && !mIsBouncing && (mEdgeCaptured || isEdgeSwipeBeginEvent(ev))) { |
| // Normally we would need to do a lot of extra stuff here. |
| // We can only get away with this because we haven't padded in |
| // the widget pager or otherwise transformed it during layout. |
| // We also don't support things like splitting MotionEvents. |
| |
| // We set handled to captured even if dispatch is returning false here so that |
| // we don't send a different view a busted or incomplete event stream. |
| handled = mEdgeCaptured |= mWidgetsView.dispatchTouchEvent(ev); |
| } |
| |
| if (!handled && !mEdgeCaptured) { |
| handled = super.dispatchTouchEvent(ev); |
| } |
| |
| if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { |
| mEdgeCaptured = false; |
| } |
| |
| return handled; |
| } |
| |
| private boolean isEdgeSwipeBeginEvent(MotionEvent ev) { |
| if (ev.getActionMasked() != MotionEvent.ACTION_DOWN) { |
| return false; |
| } |
| |
| final float x = ev.getX(); |
| return x < mDragHandleEdgeSlop || x >= getWidth() - mDragHandleEdgeSlop; |
| } |
| |
| /** |
| * We only want to add additional vertical space to the drag handle when the panel is fully |
| * closed. |
| */ |
| private int getDragHandleSizeAbove() { |
| return isChallengeShowing() ? mDragHandleOpenAbove : mDragHandleClosedAbove; |
| } |
| private int getDragHandleSizeBelow() { |
| return isChallengeShowing() ? mDragHandleOpenBelow : mDragHandleClosedBelow; |
| } |
| |
| private boolean isInChallengeView(float x, float y) { |
| return isPointInView(x, y, mChallengeView); |
| } |
| |
| private boolean isInDragHandle(float x, float y) { |
| return isPointInView(x, y, mExpandChallengeView); |
| } |
| |
| private boolean isPointInView(float x, float y, View view) { |
| if (view == null) { |
| return false; |
| } |
| return x >= view.getLeft() && y >= view.getTop() |
| && x < view.getRight() && y < view.getBottom(); |
| } |
| |
| private boolean crossedDragHandle(float x, float y, float initialY) { |
| |
| final int challengeTop = mChallengeView.getTop(); |
| final boolean horizOk = x >= 0 && x < getWidth(); |
| |
| final boolean vertOk; |
| if (mChallengeShowing) { |
| vertOk = initialY < (challengeTop - getDragHandleSizeAbove()) && |
| y > challengeTop + getDragHandleSizeBelow(); |
| } else { |
| vertOk = initialY > challengeTop + getDragHandleSizeBelow() && |
| y < challengeTop - getDragHandleSizeAbove(); |
| } |
| return horizOk && vertOk; |
| } |
| |
| private int makeChildMeasureSpec(int maxSize, int childDimen) { |
| final int mode; |
| final int size; |
| switch (childDimen) { |
| case LayoutParams.WRAP_CONTENT: |
| mode = MeasureSpec.AT_MOST; |
| size = maxSize; |
| break; |
| case LayoutParams.MATCH_PARENT: |
| mode = MeasureSpec.EXACTLY; |
| size = maxSize; |
| break; |
| default: |
| mode = MeasureSpec.EXACTLY; |
| size = Math.min(maxSize, childDimen); |
| break; |
| } |
| return MeasureSpec.makeMeasureSpec(size, mode); |
| } |
| |
| @Override |
| protected void onMeasure(int widthSpec, int heightSpec) { |
| if (MeasureSpec.getMode(widthSpec) != MeasureSpec.EXACTLY || |
| MeasureSpec.getMode(heightSpec) != MeasureSpec.EXACTLY) { |
| throw new IllegalArgumentException( |
| "SlidingChallengeLayout must be measured with an exact size"); |
| } |
| |
| final int width = MeasureSpec.getSize(widthSpec); |
| final int height = MeasureSpec.getSize(heightSpec); |
| setMeasuredDimension(width, height); |
| |
| // Find one and only one challenge view. |
| final View oldChallengeView = mChallengeView; |
| final View oldExpandChallengeView = mChallengeView; |
| mChallengeView = null; |
| mExpandChallengeView = null; |
| final int count = getChildCount(); |
| |
| // First iteration through the children finds special children and sets any associated |
| // state. |
| for (int i = 0; i < count; i++) { |
| final View child = getChildAt(i); |
| final LayoutParams lp = (LayoutParams) child.getLayoutParams(); |
| if (lp.childType == LayoutParams.CHILD_TYPE_CHALLENGE) { |
| if (mChallengeView != null) { |
| throw new IllegalStateException( |
| "There may only be one child with layout_isChallenge=\"true\""); |
| } |
| if (!(child instanceof KeyguardSecurityContainer)) { |
| throw new IllegalArgumentException( |
| "Challenge must be a KeyguardSecurityContainer"); |
| } |
| mChallengeView = (KeyguardSecurityContainer) child; |
| if (mChallengeView != oldChallengeView) { |
| mChallengeView.setVisibility(mChallengeShowing ? VISIBLE : INVISIBLE); |
| } |
| // We're going to play silly games with the frame's background drawable later. |
| if (!mHasLayout) { |
| // Set up the margin correctly based on our content for the first run. |
| mHasGlowpad = child.findViewById(R.id.keyguard_selector_view) != null; |
| lp.leftMargin = lp.rightMargin = getChallengeMargin(true); |
| } |
| } else if (lp.childType == LayoutParams.CHILD_TYPE_EXPAND_CHALLENGE_HANDLE) { |
| if (mExpandChallengeView != null) { |
| throw new IllegalStateException( |
| "There may only be one child with layout_childType" |
| + "=\"expandChallengeHandle\""); |
| } |
| mExpandChallengeView = child; |
| if (mExpandChallengeView != oldExpandChallengeView) { |
| mExpandChallengeView.setVisibility(mChallengeShowing ? INVISIBLE : VISIBLE); |
| mExpandChallengeView.setOnClickListener(mExpandChallengeClickListener); |
| } |
| } else if (lp.childType == LayoutParams.CHILD_TYPE_SCRIM) { |
| setScrimView(child); |
| } else if (lp.childType == LayoutParams.CHILD_TYPE_WIDGETS) { |
| mWidgetsView = child; |
| } |
| } |
| |
| // We want to measure the challenge view first, since the KeyguardWidgetPager |
| // needs to do things its measure pass that are dependent on the challenge view |
| // having been measured. |
| if (mChallengeView != null && mChallengeView.getVisibility() != View.GONE) { |
| // This one's a little funny. If the IME is present - reported in the form |
| // of insets on the root view - we only give the challenge the space it would |
| // have had if the IME wasn't there in order to keep the rest of the layout stable. |
| // We base this on the layout_maxHeight on the challenge view. If it comes out |
| // negative or zero, either we didn't have a maxHeight or we're totally out of space, |
| // so give up and measure as if this rule weren't there. |
| int challengeHeightSpec = heightSpec; |
| final View root = getRootView(); |
| if (root != null) { |
| final LayoutParams lp = (LayoutParams) mChallengeView.getLayoutParams(); |
| final int specSize = MeasureSpec.getSize(heightSpec); |
| final int windowHeight = mDisplayMetrics.heightPixels - root.getPaddingTop(); |
| final int diff = windowHeight - specSize; |
| final int maxChallengeHeight = lp.maxHeight - diff; |
| if (maxChallengeHeight > 0) { |
| challengeHeightSpec = makeChildMeasureSpec(maxChallengeHeight, lp.height); |
| } |
| } |
| measureChildWithMargins(mChallengeView, widthSpec, 0, challengeHeightSpec, 0); |
| } |
| |
| // Measure the rest of the children |
| for (int i = 0; i < count; i++) { |
| final View child = getChildAt(i); |
| if (child.getVisibility() == GONE) { |
| continue; |
| } |
| // Don't measure the challenge view twice! |
| if (child == mChallengeView) continue; |
| |
| // Measure children. Widget frame measures special, so that we can ignore |
| // insets for the IME. |
| int parentWidthSpec = widthSpec, parentHeightSpec = heightSpec; |
| final LayoutParams lp = (LayoutParams) child.getLayoutParams(); |
| if (lp.childType == LayoutParams.CHILD_TYPE_WIDGETS) { |
| final View root = getRootView(); |
| if (root != null) { |
| // This calculation is super dodgy and relies on several assumptions. |
| // Specifically that the root of the window will be padded in for insets |
| // and that the window is LAYOUT_IN_SCREEN. |
| final int windowWidth = mDisplayMetrics.widthPixels; |
| final int windowHeight = mDisplayMetrics.heightPixels - root.getPaddingTop(); |
| parentWidthSpec = MeasureSpec.makeMeasureSpec( |
| windowWidth, MeasureSpec.EXACTLY); |
| parentHeightSpec = MeasureSpec.makeMeasureSpec( |
| windowHeight, MeasureSpec.EXACTLY); |
| } |
| } |
| measureChildWithMargins(child, parentWidthSpec, 0, parentHeightSpec, 0); |
| } |
| } |
| |
| @Override |
| protected void onLayout(boolean changed, int l, int t, int r, int b) { |
| final int paddingLeft = getPaddingLeft(); |
| final int paddingTop = getPaddingTop(); |
| final int paddingRight = getPaddingRight(); |
| final int paddingBottom = getPaddingBottom(); |
| final int width = r - l; |
| final int height = b - t; |
| |
| final int count = getChildCount(); |
| for (int i = 0; i < count; i++) { |
| final View child = getChildAt(i); |
| |
| if (child.getVisibility() == GONE) continue; |
| |
| final LayoutParams lp = (LayoutParams) child.getLayoutParams(); |
| |
| if (lp.childType == LayoutParams.CHILD_TYPE_CHALLENGE) { |
| // Challenge views pin to the bottom, offset by a portion of their height, |
| // and center horizontally. |
| final int center = (paddingLeft + width - paddingRight) / 2; |
| final int childWidth = child.getMeasuredWidth(); |
| final int childHeight = child.getMeasuredHeight(); |
| final int left = center - childWidth / 2; |
| final int layoutBottom = height - paddingBottom - lp.bottomMargin; |
| // We use the top of the challenge view to position the handle, so |
| // we never want less than the handle size showing at the bottom. |
| final int bottom = layoutBottom + (int) ((childHeight - mChallengeBottomBound) |
| * (1 - mChallengeOffset)); |
| child.setAlpha(getChallengeAlpha()); |
| child.layout(left, bottom - childHeight, left + childWidth, bottom); |
| } else if (lp.childType == LayoutParams.CHILD_TYPE_EXPAND_CHALLENGE_HANDLE) { |
| final int center = (paddingLeft + width - paddingRight) / 2; |
| final int left = center - child.getMeasuredWidth() / 2; |
| final int right = left + child.getMeasuredWidth(); |
| final int bottom = height - paddingBottom - lp.bottomMargin; |
| final int top = bottom - child.getMeasuredHeight(); |
| child.layout(left, top, right, bottom); |
| } else { |
| // Non-challenge views lay out from the upper left, layered. |
| child.layout(paddingLeft + lp.leftMargin, |
| paddingTop + lp.topMargin, |
| paddingLeft + child.getMeasuredWidth(), |
| paddingTop + child.getMeasuredHeight()); |
| } |
| } |
| |
| if (!mHasLayout) { |
| mHasLayout = true; |
| } |
| } |
| |
| @Override |
| public void draw(Canvas c) { |
| super.draw(c); |
| if (DEBUG) { |
| final Paint debugPaint = new Paint(); |
| debugPaint.setColor(0x40FF00CC); |
| // show the isInDragHandle() rect |
| c.drawRect(mDragHandleEdgeSlop, |
| mChallengeView.getTop() - getDragHandleSizeAbove(), |
| getWidth() - mDragHandleEdgeSlop, |
| mChallengeView.getTop() + getDragHandleSizeBelow(), |
| debugPaint); |
| } |
| } |
| |
| public void computeScroll() { |
| super.computeScroll(); |
| |
| if (!mScroller.isFinished()) { |
| if (mChallengeView == null) { |
| // Can't scroll if the view is missing. |
| Log.e(TAG, "Challenge view missing in computeScroll"); |
| mScroller.abortAnimation(); |
| return; |
| } |
| |
| mScroller.computeScrollOffset(); |
| moveChallengeTo(mScroller.getCurrY()); |
| |
| if (mScroller.isFinished()) { |
| post(mEndScrollRunnable); |
| } |
| } |
| } |
| |
| private void cancelTransitionsInProgress() { |
| if (!mScroller.isFinished()) { |
| mScroller.abortAnimation(); |
| completeChallengeScroll(); |
| } |
| if (mFader != null) { |
| mFader.cancel(); |
| } |
| } |
| |
| public void fadeInChallenge() { |
| fadeChallenge(true); |
| } |
| |
| public void fadeOutChallenge() { |
| fadeChallenge(false); |
| } |
| |
| public void fadeChallenge(final boolean show) { |
| if (mChallengeView != null) { |
| |
| cancelTransitionsInProgress(); |
| float alpha = show ? 1f : 0f; |
| int duration = show ? CHALLENGE_FADE_IN_DURATION : CHALLENGE_FADE_OUT_DURATION; |
| mFader = ObjectAnimator.ofFloat(mChallengeView, "alpha", alpha); |
| mFader.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationStart(Animator animation) { |
| onFadeStart(show); |
| } |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| onFadeEnd(show); |
| } |
| }); |
| mFader.setDuration(duration); |
| mFader.start(); |
| } |
| } |
| |
| private int getMaxChallengeBottom() { |
| if (mChallengeView == null) return 0; |
| final int layoutBottom = getLayoutBottom(); |
| final int challengeHeight = mChallengeView.getMeasuredHeight(); |
| |
| return (layoutBottom + challengeHeight - mChallengeBottomBound); |
| } |
| |
| private int getMinChallengeBottom() { |
| return getLayoutBottom(); |
| } |
| |
| |
| private void onFadeStart(boolean show) { |
| mChallengeInteractiveInternal = false; |
| mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null); |
| |
| if (show) { |
| moveChallengeTo(getMinChallengeBottom()); |
| } |
| |
| setScrollState(SCROLL_STATE_FADING); |
| } |
| |
| private void onFadeEnd(boolean show) { |
| mChallengeInteractiveInternal = true; |
| setChallengeShowing(show); |
| |
| if (!show) { |
| moveChallengeTo(getMaxChallengeBottom()); |
| } |
| |
| mChallengeView.setLayerType(LAYER_TYPE_NONE, null); |
| mFader = null; |
| setScrollState(SCROLL_STATE_IDLE); |
| } |
| |
| public int getMaxChallengeTop() { |
| if (mChallengeView == null) return 0; |
| |
| final int layoutBottom = getLayoutBottom(); |
| final int challengeHeight = mChallengeView.getMeasuredHeight(); |
| return layoutBottom - challengeHeight; |
| } |
| |
| /** |
| * Move the bottom edge of mChallengeView to a new position and notify the listener |
| * if it represents a change in position. Changes made through this method will |
| * be stable across layout passes. If this method is called before first layout of |
| * this SlidingChallengeLayout it will have no effect. |
| * |
| * @param bottom New bottom edge in px in this SlidingChallengeLayout's coordinate system. |
| * @return true if the challenge view was moved |
| */ |
| private boolean moveChallengeTo(int bottom) { |
| if (mChallengeView == null || !mHasLayout) { |
| return false; |
| } |
| |
| final int layoutBottom = getLayoutBottom(); |
| final int challengeHeight = mChallengeView.getHeight(); |
| |
| bottom = Math.max(getMinChallengeBottom(), |
| Math.min(bottom, getMaxChallengeBottom())); |
| |
| float offset = 1.f - (float) (bottom - layoutBottom) / |
| (challengeHeight - mChallengeBottomBound); |
| mChallengeOffset = offset; |
| if (offset > 0 && !mChallengeShowing) { |
| setChallengeShowing(true); |
| } |
| |
| mChallengeView.layout(mChallengeView.getLeft(), |
| bottom - mChallengeView.getHeight(), mChallengeView.getRight(), bottom); |
| |
| mChallengeView.setAlpha(getChallengeAlpha()); |
| if (mScrollListener != null) { |
| mScrollListener.onScrollPositionChanged(offset, mChallengeView.getTop()); |
| } |
| postInvalidateOnAnimation(); |
| return true; |
| } |
| |
| /** |
| * The bottom edge of this SlidingChallengeLayout's coordinate system; will coincide with |
| * the bottom edge of mChallengeView when the challenge is fully opened. |
| */ |
| private int getLayoutBottom() { |
| final int bottomMargin = (mChallengeView == null) |
| ? 0 |
| : ((LayoutParams) mChallengeView.getLayoutParams()).bottomMargin; |
| final int layoutBottom = getMeasuredHeight() - getPaddingBottom() - bottomMargin; |
| return layoutBottom; |
| } |
| |
| /** |
| * The bottom edge of mChallengeView; essentially, where the sliding challenge 'is'. |
| */ |
| private int getChallengeBottom() { |
| if (mChallengeView == null) return 0; |
| |
| return mChallengeView.getBottom(); |
| } |
| |
| /** |
| * Show or hide the challenge view, animating it if necessary. |
| * @param show true to show, false to hide |
| */ |
| public void showChallenge(boolean show) { |
| showChallenge(show, 0); |
| if (!show) { |
| // Block any drags in progress so that callers can use this to disable dragging |
| // for other touch interactions. |
| mBlockDrag = true; |
| } |
| } |
| |
| private void showChallenge(int velocity) { |
| boolean show = false; |
| if (Math.abs(velocity) > mMinVelocity) { |
| show = velocity < 0; |
| } else { |
| show = mChallengeOffset >= 0.5f; |
| } |
| showChallenge(show, velocity); |
| } |
| |
| private void showChallenge(boolean show, int velocity) { |
| if (mChallengeView == null) { |
| setChallengeShowing(false); |
| return; |
| } |
| |
| if (mHasLayout) { |
| mChallengeShowingTargetState = show; |
| final int layoutBottom = getLayoutBottom(); |
| animateChallengeTo(show ? layoutBottom : |
| layoutBottom + mChallengeView.getHeight() - mChallengeBottomBound, velocity); |
| } |
| } |
| |
| @Override |
| public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { |
| return new LayoutParams(getContext(), attrs); |
| } |
| |
| @Override |
| protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { |
| return p instanceof LayoutParams ? new LayoutParams((LayoutParams) p) : |
| p instanceof MarginLayoutParams ? new LayoutParams((MarginLayoutParams) p) : |
| new LayoutParams(p); |
| } |
| |
| @Override |
| protected ViewGroup.LayoutParams generateDefaultLayoutParams() { |
| return new LayoutParams(); |
| } |
| |
| @Override |
| protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { |
| return p instanceof LayoutParams; |
| } |
| |
| public static class LayoutParams extends MarginLayoutParams { |
| public int childType = CHILD_TYPE_NONE; |
| public static final int CHILD_TYPE_NONE = 0; |
| public static final int CHILD_TYPE_CHALLENGE = 2; |
| public static final int CHILD_TYPE_SCRIM = 4; |
| public static final int CHILD_TYPE_WIDGETS = 5; |
| public static final int CHILD_TYPE_EXPAND_CHALLENGE_HANDLE = 6; |
| |
| public int maxHeight; |
| |
| public LayoutParams() { |
| this(MATCH_PARENT, WRAP_CONTENT); |
| } |
| |
| public LayoutParams(int width, int height) { |
| super(width, height); |
| } |
| |
| public LayoutParams(android.view.ViewGroup.LayoutParams source) { |
| super(source); |
| } |
| |
| public LayoutParams(MarginLayoutParams source) { |
| super(source); |
| } |
| |
| public LayoutParams(LayoutParams source) { |
| super(source); |
| |
| childType = source.childType; |
| } |
| |
| public LayoutParams(Context c, AttributeSet attrs) { |
| super(c, attrs); |
| |
| final TypedArray a = c.obtainStyledAttributes(attrs, |
| R.styleable.SlidingChallengeLayout_Layout); |
| childType = a.getInt(R.styleable.SlidingChallengeLayout_Layout_layout_childType, |
| CHILD_TYPE_NONE); |
| maxHeight = a.getDimensionPixelSize( |
| R.styleable.SlidingChallengeLayout_Layout_layout_maxHeight, 0); |
| a.recycle(); |
| } |
| } |
| } |