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