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