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