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