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