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