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          */
onScrollStateChanged(int scrollState)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          */
onScrollPositionChanged(float scrollPosition, int challengeTop)225         public void onScrollPositionChanged(float scrollPosition, int challengeTop);
226     }
227 
SlidingChallengeLayout(Context context)228     public SlidingChallengeLayout(Context context) {
229         this(context, null);
230     }
231 
SlidingChallengeLayout(Context context, AttributeSet attrs)232     public SlidingChallengeLayout(Context context, AttributeSet attrs) {
233         this(context, attrs, 0);
234     }
235 
SlidingChallengeLayout(Context context, AttributeSet attrs, int defStyle)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 
setEnableChallengeDragging(boolean enabled)267     public void setEnableChallengeDragging(boolean enabled) {
268         mEnableChallengeDragging = enabled;
269     }
270 
setInsets(Rect insets)271     public void setInsets(Rect insets) {
272         mInsets.set(insets);
273     }
274 
setHandleAlpha(float alpha)275     public void setHandleAlpha(float alpha) {
276         if (mExpandChallengeView != null) {
277             mExpandChallengeView.setAlpha(alpha);
278         }
279     }
280 
setChallengeInteractive(boolean interactive)281     public void setChallengeInteractive(boolean interactive) {
282         mChallengeInteractiveExternal = interactive;
283         if (mExpandChallengeView != null) {
284             mExpandChallengeView.setEnabled(interactive);
285         }
286     }
287 
animateHandle(boolean visible)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 
sendInitialListenerUpdates()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 
setOnChallengeScrolledListener(OnChallengeScrolledListener listener)311     public void setOnChallengeScrolledListener(OnChallengeScrolledListener listener) {
312         mScrollListener = listener;
313         if (mHasLayout) {
314             sendInitialListenerUpdates();
315         }
316     }
317 
setOnBouncerStateChangedListener(OnBouncerStateChangedListener listener)318     public void setOnBouncerStateChangedListener(OnBouncerStateChangedListener listener) {
319         mBouncerListener = listener;
320     }
321 
322     @Override
onAttachedToWindow()323     public void onAttachedToWindow() {
324         super.onAttachedToWindow();
325 
326         mHasLayout = false;
327     }
328 
329     @Override
onDetachedFromWindow()330     public void onDetachedFromWindow() {
331         super.onDetachedFromWindow();
332 
333         removeCallbacks(mEndScrollRunnable);
334         mHasLayout = false;
335     }
336 
337     @Override
requestChildFocus(View child, View focused)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.
distanceInfluenceForSnapDuration(float f)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 
setScrollState(int state)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 
completeChallengeScroll()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 
setScrimView(View scrim)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      */
animateChallengeTo(int y, int velocity)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 
setChallengeShowing(boolean showChallenge)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      */
isChallengeShowing()468     public boolean isChallengeShowing() {
469         return mChallengeShowing;
470     }
471 
472     @Override
isChallengeOverlapping()473     public boolean isChallengeOverlapping() {
474         return mChallengeShowing;
475     }
476 
477     @Override
isBouncing()478     public boolean isBouncing() {
479         return mIsBouncing;
480     }
481 
482     @Override
getBouncerAnimationDuration()483     public int getBouncerAnimationDuration() {
484         return HANDLE_ANIMATE_DURATION;
485     }
486 
487     @Override
showBouncer()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
hideBouncer()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 
getChallengeMargin(boolean expanded)540     private int getChallengeMargin(boolean expanded) {
541         return expanded && mHasGlowpad ? 0 : mDragHandleEdgeSlop;
542     }
543 
getChallengeAlpha()544     private float getChallengeAlpha() {
545         float x = mChallengeOffset - 1;
546         return x * x * x + 1.f;
547     }
548 
549     @Override
requestDisallowInterceptTouchEvent(boolean allowIntercept)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
onInterceptTouchEvent(MotionEvent ev)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 
shouldEnableChallengeDragging()608     private boolean shouldEnableChallengeDragging() {
609         return mEnableChallengeDragging || !mChallengeShowing;
610     }
611 
isChallengeInteractionBlocked()612     private boolean isChallengeInteractionBlocked() {
613         return !mChallengeInteractiveExternal || !mChallengeInteractiveInternal;
614     }
615 
resetTouch()616     private void resetTouch() {
617         mVelocityTracker.recycle();
618         mVelocityTracker = null;
619         mActivePointerId = INVALID_POINTER;
620         mDragging = mBlockDrag = false;
621     }
622 
623     @Override
onTouchEvent(MotionEvent ev)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
dispatchTouchEvent(MotionEvent ev)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 
isEdgeSwipeBeginEvent(MotionEvent ev)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      */
getDragHandleSizeAbove()753     private int getDragHandleSizeAbove() {
754         return isChallengeShowing() ? mDragHandleOpenAbove : mDragHandleClosedAbove;
755     }
getDragHandleSizeBelow()756     private int getDragHandleSizeBelow() {
757         return isChallengeShowing() ? mDragHandleOpenBelow : mDragHandleClosedBelow;
758     }
759 
isInChallengeView(float x, float y)760     private boolean isInChallengeView(float x, float y) {
761         return isPointInView(x, y, mChallengeView);
762     }
763 
isInDragHandle(float x, float y)764     private boolean isInDragHandle(float x, float y) {
765         return isPointInView(x, y, mExpandChallengeView);
766     }
767 
isPointInView(float x, float y, View view)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 
crossedDragHandle(float x, float y, float initialY)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