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