SlidingChallengeLayout.java revision eee209313d564be37b8fc068f785b3844cd6597e
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.content.Context;
20import android.content.res.TypedArray;
21import android.graphics.Canvas;
22import android.graphics.drawable.Drawable;
23import android.util.AttributeSet;
24import android.util.Log;
25import android.view.MotionEvent;
26import android.view.VelocityTracker;
27import android.view.View;
28import android.view.ViewConfiguration;
29import android.view.ViewGroup;
30import android.view.animation.Interpolator;
31import android.widget.Scroller;
32
33import com.android.internal.R;
34
35/**
36 * This layout handles interaction with the sliding security challenge views
37 * that overlay/resize other keyguard contents.
38 */
39public class SlidingChallengeLayout extends ViewGroup implements ChallengeLayout {
40    private static final String TAG = "SlidingChallengeLayout";
41
42    // Drawn to show the drag handle in closed state; crossfades to the challenge view
43    // when challenge is fully visible
44    private Drawable mHandleDrawable;
45    private boolean mShowHandle = true;
46
47    // Initialized during measurement from child layoutparams
48    private View mChallengeView;
49    private View mScrimView;
50
51    // Range: 0 (fully hidden) to 1 (fully visible)
52    private float mChallengeOffset = 1.f;
53    private boolean mChallengeShowing = true;
54    private boolean mIsBouncing = false;
55
56    private final Scroller mScroller;
57    private int mScrollState;
58    private OnChallengeScrolledListener mListener;
59
60    public static final int SCROLL_STATE_IDLE = 0;
61    public static final int SCROLL_STATE_DRAGGING = 1;
62    public static final int SCROLL_STATE_SETTLING = 2;
63
64    private static final int MAX_SETTLE_DURATION = 600; // ms
65
66    // ID of the pointer in charge of a current drag
67    private int mActivePointerId = INVALID_POINTER;
68    private static final int INVALID_POINTER = -1;
69
70    // True if the user is currently dragging the slider
71    private boolean mDragging;
72    // True if the user may not drag until a new gesture begins
73    private boolean mBlockDrag;
74
75    private VelocityTracker mVelocityTracker;
76    private int mMinVelocity;
77    private int mMaxVelocity;
78    private float mLastTouchY;
79    private int mDragHandleSize;
80
81    private static final int DRAG_HANDLE_DEFAULT_SIZE = 32; // dp
82
83    // True if at least one layout pass has happened since the view was attached.
84    private boolean mHasLayout;
85
86    private static final Interpolator sMotionInterpolator = new Interpolator() {
87        public float getInterpolation(float t) {
88            t -= 1.0f;
89            return t * t * t * t * t + 1.0f;
90        }
91    };
92
93    private static final Interpolator sHandleFadeInterpolator = new Interpolator() {
94        public float getInterpolation(float t) {
95            return t * t;
96        }
97    };
98
99    private final Runnable mEndScrollRunnable = new Runnable () {
100        public void run() {
101            completeChallengeScroll();
102        }
103    };
104
105    private final OnClickListener mScrimClickListener = new OnClickListener() {
106        @Override
107        public void onClick(View v) {
108            hideBouncer();
109        }
110    };
111
112    /**
113     * Listener interface that reports changes in scroll state of the challenge area.
114     */
115    public interface OnChallengeScrolledListener {
116        /**
117         * The scroll state itself changed.
118         *
119         * <p>scrollState will be one of the following:</p>
120         *
121         * <ul>
122         * <li><code>SCROLL_STATE_IDLE</code> - The challenge area is stationary.</li>
123         * <li><code>SCROLL_STATE_DRAGGING</code> - The user is actively dragging
124         * the challenge area.</li>
125         * <li><code>SCROLL_STATE_SETTLING</code> - The challenge area is animating
126         * into place.</li>
127         * </ul>
128         *
129         * <p>Do not perform expensive operations (e.g. layout)
130         * while the scroll state is not <code>SCROLL_STATE_IDLE</code>.</p>
131         *
132         * @param scrollState The new scroll state of the challenge area.
133         */
134        public void onScrollStateChanged(int scrollState);
135
136        /**
137         * The precise position of the challenge area has changed.
138         *
139         * <p>NOTE: It is NOT safe to modify layout or call any View methods that may
140         * result in a requestLayout anywhere in your view hierarchy as a result of this call.
141         * It may be called during drawing.</p>
142         *
143         * @param scrollPosition New relative position of the challenge area.
144         *                       1.f = fully visible/ready to be interacted with.
145         *                       0.f = fully invisible/inaccessible to the user.
146         * @param challengeTop Position of the top edge of the challenge view in px in the
147         *                     SlidingChallengeLayout's coordinate system.
148         */
149        public void onScrollPositionChanged(float scrollPosition, int challengeTop);
150    }
151
152    public SlidingChallengeLayout(Context context) {
153        this(context, null);
154    }
155
156    public SlidingChallengeLayout(Context context, AttributeSet attrs) {
157        this(context, attrs, 0);
158    }
159
160    public SlidingChallengeLayout(Context context, AttributeSet attrs, int defStyle) {
161        super(context, attrs, defStyle);
162
163        final TypedArray a = context.obtainStyledAttributes(attrs,
164                R.styleable.SlidingChallengeLayout, defStyle, 0);
165        setDragHandleDrawable(a.getDrawable(R.styleable.SlidingChallengeLayout_dragHandle));
166
167        a.recycle();
168
169        mScroller = new Scroller(context, sMotionInterpolator);
170
171        final ViewConfiguration vc = ViewConfiguration.get(context);
172        mMinVelocity = vc.getScaledMinimumFlingVelocity();
173        mMaxVelocity = vc.getScaledMaximumFlingVelocity();
174
175        setWillNotDraw(false);
176    }
177
178    public void setDragHandleDrawable(Drawable d) {
179        if (d != null) {
180            mDragHandleSize = d.getIntrinsicHeight();
181        }
182        if (mDragHandleSize == 0 || d == null) {
183            final float density = getResources().getDisplayMetrics().density;
184            mDragHandleSize = (int) (DRAG_HANDLE_DEFAULT_SIZE * density + 0.5f);
185        }
186        mHandleDrawable = d;
187    }
188
189    private void sendInitialListenerUpdates() {
190        if (mListener != null) {
191            int challengeTop = mChallengeView != null ? mChallengeView.getTop() : 0;
192            mListener.onScrollPositionChanged(mChallengeOffset, challengeTop);
193            mListener.onScrollStateChanged(mScrollState);
194        }
195    }
196
197    public void setOnChallengeScrolledListener(OnChallengeScrolledListener listener) {
198        mListener = listener;
199        if (mHasLayout) {
200            sendInitialListenerUpdates();
201        }
202    }
203
204    @Override
205    public void onAttachedToWindow() {
206        super.onAttachedToWindow();
207
208        mHasLayout = false;
209    }
210
211    @Override
212    public void onDetachedFromWindow() {
213        super.onDetachedFromWindow();
214
215        removeCallbacks(mEndScrollRunnable);
216        mHasLayout = false;
217    }
218
219    // We want the duration of the page snap animation to be influenced by the distance that
220    // the screen has to travel, however, we don't want this duration to be effected in a
221    // purely linear fashion. Instead, we use this method to moderate the effect that the distance
222    // of travel has on the overall snap duration.
223    float distanceInfluenceForSnapDuration(float f) {
224        f -= 0.5f; // center the values about 0.
225        f *= 0.3f * Math.PI / 2.0f;
226        return (float) Math.sin(f);
227    }
228
229    void setScrollState(int state) {
230        if (mScrollState != state) {
231            mScrollState = state;
232
233            if (mListener != null) {
234                mListener.onScrollStateChanged(state);
235            }
236        }
237    }
238
239    void completeChallengeScroll() {
240        setChallengeShowing(mChallengeOffset != 0);
241        setScrollState(SCROLL_STATE_IDLE);
242    }
243
244    void setScrimView(View scrim) {
245        if (mScrimView != null) {
246            mScrimView.setOnClickListener(null);
247        }
248        mScrimView = scrim;
249        mScrimView.setVisibility(mIsBouncing ? VISIBLE : GONE);
250        mScrimView.setFocusable(true);
251        mScrimView.setOnClickListener(mScrimClickListener);
252    }
253
254    /**
255     * Animate the bottom edge of the challenge view to the given position.
256     *
257     * @param y desired final position for the bottom edge of the challenge view in px
258     * @param velocity velocity in
259     */
260    void animateChallengeTo(int y, int velocity) {
261        if (mChallengeView == null) {
262            // Nothing to do.
263            return;
264        }
265        int sy = mChallengeView.getBottom();
266        int dy = y - sy;
267        if (dy == 0) {
268            completeChallengeScroll();
269            return;
270        }
271
272        setScrollState(SCROLL_STATE_SETTLING);
273
274        final int childHeight = mChallengeView.getHeight();
275        final int halfHeight = childHeight / 2;
276        final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / childHeight);
277        final float distance = halfHeight + halfHeight *
278                distanceInfluenceForSnapDuration(distanceRatio);
279
280        int duration = 0;
281        velocity = Math.abs(velocity);
282        if (velocity > 0) {
283            duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
284        } else {
285            final float childDelta = (float) Math.abs(dy) / childHeight;
286            duration = (int) ((childDelta + 1) * 100);
287        }
288        duration = Math.min(duration, MAX_SETTLE_DURATION);
289
290        mScroller.startScroll(0, sy, 0, dy, duration);
291        postInvalidateOnAnimation();
292    }
293
294    private void setChallengeShowing(boolean showChallenge) {
295        if (mChallengeShowing != showChallenge) {
296            mChallengeShowing = showChallenge;
297            if (mChallengeView != null) {
298                mChallengeView.setVisibility(showChallenge ? VISIBLE : INVISIBLE);
299            }
300        }
301    }
302
303    /**
304     * @return true if the challenge is at all visible.
305     */
306    public boolean isChallengeShowing() {
307        return mChallengeShowing;
308    }
309
310    @Override
311    public boolean isChallengeOverlapping() {
312        return mChallengeShowing;
313    }
314
315    @Override
316    public boolean isBouncing() {
317        return mIsBouncing;
318    }
319
320    @Override
321    public void showBouncer() {
322        if (mIsBouncing) return;
323        showChallenge(true);
324        mIsBouncing = true;
325        if (mScrimView != null) {
326            mScrimView.setVisibility(GONE);
327        }
328    }
329
330    @Override
331    public void hideBouncer() {
332        if (!mIsBouncing) return;
333        setChallengeShowing(false);
334        mIsBouncing = false;
335        if (mScrimView != null) {
336            mScrimView.setVisibility(GONE);
337        }
338    }
339
340    @Override
341    public void requestDisallowInterceptTouchEvent(boolean allowIntercept) {
342        // We'll intercept whoever we feel like! ...as long as it isn't a challenge view.
343        // If there are one or more pointers in the challenge view before we take over
344        // touch events, onInterceptTouchEvent will set mBlockDrag.
345    }
346
347    @Override
348    public boolean onInterceptTouchEvent(MotionEvent ev) {
349        if (mVelocityTracker == null) {
350            mVelocityTracker = VelocityTracker.obtain();
351        }
352        mVelocityTracker.addMovement(ev);
353
354        final int action = ev.getActionMasked();
355        switch (action) {
356            case MotionEvent.ACTION_DOWN:
357                mLastTouchY = ev.getY();
358                break;
359
360            case MotionEvent.ACTION_CANCEL:
361            case MotionEvent.ACTION_UP:
362                resetTouch();
363                break;
364
365            case MotionEvent.ACTION_MOVE:
366                final int count = ev.getPointerCount();
367                for (int i = 0; i < count; i++) {
368                    final float x = ev.getX(i);
369                    final float y = ev.getY(i);
370
371                    if ((isInDragHandle(x, y) || crossedDragHandle(x, y, mLastTouchY) ||
372                            (isInChallengeView(x, y) && mScrollState == SCROLL_STATE_SETTLING)) &&
373                            mActivePointerId == INVALID_POINTER) {
374                        mActivePointerId = ev.getPointerId(i);
375                        mLastTouchY = y;
376                        mDragging = true;
377                    } else if (isInChallengeView(x, y)) {
378                        mBlockDrag = true;
379                    }
380                }
381                break;
382        }
383
384        if (mBlockDrag) {
385            mActivePointerId = INVALID_POINTER;
386            mDragging = false;
387        }
388
389        return mDragging;
390    }
391
392    private void resetTouch() {
393        mVelocityTracker.recycle();
394        mVelocityTracker = null;
395        mActivePointerId = INVALID_POINTER;
396        mDragging = mBlockDrag = false;
397    }
398
399    @Override
400    public boolean onTouchEvent(MotionEvent ev) {
401        if (mVelocityTracker == null) {
402            mVelocityTracker = VelocityTracker.obtain();
403        }
404        mVelocityTracker.addMovement(ev);
405
406        final int action = ev.getActionMasked();
407        switch (action) {
408            case MotionEvent.ACTION_CANCEL:
409                if (mDragging) {
410                    showChallenge(0);
411                }
412                resetTouch();
413                break;
414
415            case MotionEvent.ACTION_POINTER_UP:
416                if (mActivePointerId != ev.getPointerId(ev.getActionIndex())) {
417                    break;
418                }
419            case MotionEvent.ACTION_UP:
420                if (mDragging) {
421                    mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
422                    showChallenge((int) mVelocityTracker.getYVelocity(mActivePointerId));
423                }
424                resetTouch();
425                break;
426
427            case MotionEvent.ACTION_MOVE:
428                if (!mDragging && !mBlockDrag) {
429                    final int count = ev.getPointerCount();
430                    for (int i = 0; i < count; i++) {
431                        final float x = ev.getX(i);
432                        final float y = ev.getY(i);
433
434                        if ((isInDragHandle(x, y) || crossedDragHandle(x, y, mLastTouchY) ||
435                                (isInChallengeView(x, y) && mScrollState == SCROLL_STATE_SETTLING))
436                                && mActivePointerId == INVALID_POINTER) {
437                            mActivePointerId = ev.getPointerId(i);
438                            mDragging = true;
439                            break;
440                        }
441                    }
442                }
443                // Not an else; this can be set above.
444                if (mDragging) {
445                    // No-op if already in this state, but set it here in case we arrived
446                    // at this point from either intercept or the above.
447                    setScrollState(SCROLL_STATE_DRAGGING);
448
449                    final int index = ev.findPointerIndex(mActivePointerId);
450                    if (index < 0) {
451                        // Oops, bogus state. We lost some touch events somewhere.
452                        // Just drop it with no velocity and let things settle.
453                        resetTouch();
454                        showChallenge(0);
455                        return true;
456                    }
457                    final float y = Math.max(ev.getY(index), getChallengeOpenedTop());
458                    final float delta = y - mLastTouchY;
459                    final int idelta = (int) delta;
460                    // Don't lose the rounded component
461                    mLastTouchY = y + delta - idelta;
462
463                    moveChallengeBy(idelta);
464                }
465                break;
466        }
467        return true;
468    }
469
470    private boolean isInChallengeView(float x, float y) {
471        if (mChallengeView == null) return false;
472
473        return x >= mChallengeView.getLeft() && y >= mChallengeView.getTop() &&
474                x < mChallengeView.getRight() && y < mChallengeView.getBottom();
475    }
476
477    private boolean isInDragHandle(float x, float y) {
478        if (mChallengeView == null) return false;
479
480        return x >= 0 && y >= mChallengeView.getTop() && x < getWidth() &&
481                y < mChallengeView.getTop() + mDragHandleSize;
482    }
483
484    private boolean crossedDragHandle(float x, float y, float lastY) {
485        final int challengeTop = mChallengeView.getTop();
486        return x >= 0 && x < getWidth() && lastY < challengeTop &&
487                y > challengeTop + mDragHandleSize;
488    }
489
490    @Override
491    protected void onMeasure(int widthSpec, int heightSpec) {
492        if (MeasureSpec.getMode(widthSpec) != MeasureSpec.EXACTLY ||
493                MeasureSpec.getMode(heightSpec) != MeasureSpec.EXACTLY) {
494            throw new IllegalArgumentException(
495                    "SlidingChallengeLayout must be measured with an exact size");
496        }
497
498        final int width = MeasureSpec.getSize(widthSpec);
499        final int height = MeasureSpec.getSize(heightSpec);
500        setMeasuredDimension(width, height);
501
502        // Find one and only one challenge view.
503        final View oldChallengeView = mChallengeView;
504        mChallengeView = null;
505        final int count = getChildCount();
506        for (int i = 0; i < count; i++) {
507            final View child = getChildAt(i);
508            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
509
510            if (lp.childType == LayoutParams.CHILD_TYPE_CHALLENGE) {
511                if (mChallengeView != null) {
512                    throw new IllegalStateException(
513                            "There may only be one child with layout_isChallenge=\"true\"");
514                }
515                mChallengeView = child;
516                if (mChallengeView != oldChallengeView) {
517                    mChallengeView.setVisibility(mChallengeShowing ? VISIBLE : INVISIBLE);
518                }
519            } else if (lp.childType == LayoutParams.CHILD_TYPE_SCRIM) {
520                setScrimView(child);
521            }
522
523            if (child.getVisibility() == GONE) continue;
524
525            measureChildWithMargins(child, widthSpec, 0, heightSpec, 0);
526        }
527    }
528
529    @Override
530    protected void onLayout(boolean changed, int l, int t, int r, int b) {
531        final int paddingLeft = getPaddingLeft();
532        final int paddingTop = getPaddingTop();
533        final int paddingRight = getPaddingRight();
534        final int paddingBottom = getPaddingBottom();
535        final int width = r - l;
536        final int height = b - t;
537
538        final int count = getChildCount();
539        for (int i = 0; i < count; i++) {
540            final View child = getChildAt(i);
541
542            if (child.getVisibility() == GONE) continue;
543
544            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
545
546            if (lp.childType == LayoutParams.CHILD_TYPE_CHALLENGE) {
547                // Challenge views pin to the bottom, offset by a portion of their height,
548                // and center horizontally.
549                final int center = (paddingLeft + width - paddingRight) / 2;
550                final int childWidth = child.getMeasuredWidth();
551                final int childHeight = child.getMeasuredHeight();
552                final int left = center - childWidth / 2;
553                final int layoutBottom = height - paddingBottom - lp.bottomMargin;
554                // We use the top of the challenge view to position the handle, so
555                // we never want less than the handle size showing at the bottom.
556                final int bottom = layoutBottom + (int) ((childHeight - mDragHandleSize)
557                        * (1 - mChallengeOffset));
558                float offset = 1.f - (bottom - layoutBottom) / childHeight;
559                child.setAlpha(offset);
560                child.layout(left, bottom - childHeight, left + childWidth, bottom);
561            } else {
562                // Non-challenge views lay out from the upper left, layered.
563                child.layout(paddingLeft + lp.leftMargin,
564                        paddingTop + lp.topMargin,
565                        paddingLeft + child.getMeasuredWidth(),
566                        paddingTop + child.getMeasuredHeight());
567            }
568        }
569
570        if (!mHasLayout) {
571            // We want to trigger the initial listener updates outside of layout pass,
572            // in case the listeners trigger requestLayout().
573            post(new Runnable() {
574                @Override
575                public void run() {
576                    sendInitialListenerUpdates();
577                }
578            });
579        }
580        mHasLayout = true;
581    }
582
583    public void computeScroll() {
584        super.computeScroll();
585
586        if (!mScroller.isFinished()) {
587            if (mChallengeView == null) {
588                // Can't scroll if the view is missing.
589                Log.e(TAG, "Challenge view missing in computeScroll");
590                mScroller.abortAnimation();
591                return;
592            }
593
594            mScroller.computeScrollOffset();
595            moveChallengeTo(mScroller.getCurrY());
596
597            if (mScroller.isFinished()) {
598                post(mEndScrollRunnable);
599            }
600        }
601    }
602
603    @Override
604    public void draw(Canvas c) {
605        super.draw(c);
606        if (mChallengeOffset < 1.f
607                && mChallengeView != null && mHandleDrawable != null && mShowHandle) {
608            final int top = mChallengeView.getTop();
609            mHandleDrawable.setBounds(0, top, getWidth(), top + mDragHandleSize);
610            final float alpha = sHandleFadeInterpolator.getInterpolation(1 - mChallengeOffset);
611            mHandleDrawable.setAlpha((int) (alpha * 0xFF));
612            mHandleDrawable.draw(c);
613        }
614    }
615
616    /**
617     * Move the bottom edge of mChallengeView to a new position and notify the listener
618     * if it represents a change in position. Changes made through this method will
619     * be stable across layout passes. If this method is called before first layout of
620     * this SlidingChallengeLayout it will have no effect.
621     *
622     * @param bottom New bottom edge in px in this SlidingChallengeLayout's coordinate system.
623     * @return true if the challenge view was moved
624     */
625    private boolean moveChallengeTo(int bottom) {
626        if (mChallengeView == null || !mHasLayout) {
627            return false;
628        }
629
630        final int bottomMargin = ((LayoutParams) mChallengeView.getLayoutParams()).bottomMargin;
631        final int layoutBottom = getHeight() - getPaddingBottom() - bottomMargin;
632        final int challengeHeight = mChallengeView.getHeight();
633
634        bottom = Math.max(layoutBottom,
635                Math.min(bottom, layoutBottom + challengeHeight - mDragHandleSize));
636
637        float offset = 1.f - (float) (bottom - layoutBottom) / (challengeHeight - mDragHandleSize);
638        mChallengeOffset = offset;
639        if (offset > 0 && !mChallengeShowing) {
640            setChallengeShowing(true);
641        }
642
643        mChallengeView.layout(mChallengeView.getLeft(),
644                bottom - mChallengeView.getHeight(), mChallengeView.getRight(), bottom);
645
646        mChallengeView.setAlpha(offset);
647        if (mListener != null) {
648            mListener.onScrollPositionChanged(offset, mChallengeView.getTop());
649        }
650        postInvalidateOnAnimation();
651        return true;
652    }
653
654    private int getChallengeBottom() {
655        if (mChallengeView == null) return 0;
656
657        return mChallengeView.getBottom();
658    }
659
660    private int getChallengeOpenedTop() {
661        final int paddedBottom = getHeight() - getPaddingBottom();
662        if (mChallengeView == null) return paddedBottom;
663
664        final int bottomMargin = ((LayoutParams) mChallengeView.getLayoutParams()).bottomMargin;
665        final int layoutBottom = paddedBottom - bottomMargin;
666
667        return layoutBottom - mChallengeView.getHeight();
668    }
669
670    private void moveChallengeBy(int delta) {
671        moveChallengeTo(getChallengeBottom() + delta);
672    }
673
674    /**
675     * Show or hide the challenge view, animating it if necessary.
676     * @param show true to show, false to hide
677     */
678    public void showChallenge(boolean show) {
679        showChallenge(show, 0);
680    }
681
682    private void showChallenge(int velocity) {
683        boolean show = false;
684        if (Math.abs(velocity) > mMinVelocity) {
685            show = velocity < 0;
686        } else {
687            show = mChallengeOffset >= 0.5f;
688        }
689        showChallenge(show, velocity);
690    }
691
692    private void showChallenge(boolean show, int velocity) {
693        if (mChallengeView == null) {
694            setChallengeShowing(false);
695            return;
696        }
697
698        if (mHasLayout) {
699            final int bottomMargin = ((LayoutParams) mChallengeView.getLayoutParams()).bottomMargin;
700            final int layoutBottom = getHeight() - getPaddingBottom() - bottomMargin;
701            animateChallengeTo(show ? layoutBottom :
702                layoutBottom + mChallengeView.getHeight() - mDragHandleSize, velocity);
703        }
704    }
705
706    public void showHandle(boolean show) {
707        mShowHandle = show;
708        invalidate();
709    }
710
711    @Override
712    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
713        return new LayoutParams(getContext(), attrs);
714    }
715
716    @Override
717    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
718        return p instanceof LayoutParams ? new LayoutParams((LayoutParams) p) :
719                p instanceof MarginLayoutParams ? new LayoutParams((MarginLayoutParams) p) :
720                new LayoutParams(p);
721    }
722
723    @Override
724    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
725        return new LayoutParams();
726    }
727
728    @Override
729    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
730        return p instanceof LayoutParams;
731    }
732
733    public static class LayoutParams extends MarginLayoutParams {
734        public int childType = CHILD_TYPE_NONE;
735        public static final int CHILD_TYPE_NONE = 0;
736        public static final int CHILD_TYPE_CHALLENGE = 2;
737        public static final int CHILD_TYPE_SCRIM = 4;
738
739        public LayoutParams() {
740            this(MATCH_PARENT, WRAP_CONTENT);
741        }
742
743        public LayoutParams(int width, int height) {
744            super(width, height);
745        }
746
747        public LayoutParams(android.view.ViewGroup.LayoutParams source) {
748            super(source);
749        }
750
751        public LayoutParams(MarginLayoutParams source) {
752            super(source);
753        }
754
755        public LayoutParams(LayoutParams source) {
756            super(source);
757
758            childType = source.childType;
759        }
760
761        public LayoutParams(Context c, AttributeSet attrs) {
762            super(c, attrs);
763
764            final TypedArray a = c.obtainStyledAttributes(attrs,
765                    R.styleable.SlidingChallengeLayout_Layout);
766            childType = a.getInt(R.styleable.SlidingChallengeLayout_Layout_layout_childType,
767                    CHILD_TYPE_NONE);
768            a.recycle();
769        }
770    }
771}
772