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