blob: 05b35a12e8fac698751fb8b831e3fbebf6be0b63 [file] [log] [blame]
Jim Millerd6523da2012-10-21 16:47:02 -07001/*
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
Jim Miller5ecd8112013-01-09 18:50:26 -080017package com.android.keyguard;
Adam Powell9c2c77f2012-11-01 14:52:17 -070018
Adam Powellcdf8b482012-10-30 20:56:45 -070019import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
Adam Powell6f352092012-10-25 20:49:32 -070021import android.animation.ObjectAnimator;
Jim Millerd6523da2012-10-21 16:47:02 -070022import android.content.Context;
Adam Powellfa668cc2012-10-31 16:09:21 -070023import android.content.res.Resources;
Jim Millerd6523da2012-10-21 16:47:02 -070024import android.content.res.TypedArray;
25import android.graphics.Canvas;
Daniel Sandler1bfcc822012-10-26 11:25:49 -040026import android.graphics.Paint;
Jim Millerd6523da2012-10-21 16:47:02 -070027import android.util.AttributeSet;
Adam Powell5da64302012-11-05 14:16:59 -080028import android.util.DisplayMetrics;
Adam Powell6f352092012-10-25 20:49:32 -070029import android.util.FloatProperty;
Jim Millerd6523da2012-10-21 16:47:02 -070030import android.util.Log;
Adam Powell6f352092012-10-25 20:49:32 -070031import android.util.Property;
Jim Millerd6523da2012-10-21 16:47:02 -070032import android.view.MotionEvent;
33import android.view.VelocityTracker;
34import android.view.View;
35import android.view.ViewConfiguration;
36import android.view.ViewGroup;
Svetoslav Ganov74bdd212012-10-31 18:19:33 -070037import android.view.accessibility.AccessibilityManager;
Jim Millerd6523da2012-10-21 16:47:02 -070038import android.view.animation.Interpolator;
39import android.widget.Scroller;
40
Jim Millerd6523da2012-10-21 16:47:02 -070041/**
42 * This layout handles interaction with the sliding security challenge views
43 * that overlay/resize other keyguard contents.
44 */
Jim Miller19a52672012-10-23 19:52:04 -070045public class SlidingChallengeLayout extends ViewGroup implements ChallengeLayout {
Jim Millerd6523da2012-10-21 16:47:02 -070046 private static final String TAG = "SlidingChallengeLayout";
Daniel Sandler1bfcc822012-10-26 11:25:49 -040047 private static final boolean DEBUG = false;
Jim Millerd6523da2012-10-21 16:47:02 -070048
Daniel Sandlerc606bf12012-10-28 00:12:21 -040049 // The drag handle is measured in dp above & below the top edge of the
50 // challenge view; these parameters change based on whether the challenge
51 // is open or closed.
Adam Powell9c2c77f2012-11-01 14:52:17 -070052 private static final int DRAG_HANDLE_CLOSED_ABOVE = 8; // dp
Adam Powellcfc30862012-10-29 18:21:31 -070053 private static final int DRAG_HANDLE_CLOSED_BELOW = 0; // dp
Daniel Sandlerc606bf12012-10-28 00:12:21 -040054 private static final int DRAG_HANDLE_OPEN_ABOVE = 8; // dp
55 private static final int DRAG_HANDLE_OPEN_BELOW = 0; // dp
56
Winson Chung70c2f872012-11-07 21:47:12 -080057 private static final int HANDLE_ANIMATE_DURATION = 250; // ms
Daniel Sandlerc606bf12012-10-28 00:12:21 -040058
Jim Millerd6523da2012-10-21 16:47:02 -070059 // Drawn to show the drag handle in closed state; crossfades to the challenge view
60 // when challenge is fully visible
Adam Powellcdf8b482012-10-30 20:56:45 -070061 private boolean mEdgeCaptured;
Jim Millerd6523da2012-10-21 16:47:02 -070062
Adam Powell5da64302012-11-05 14:16:59 -080063 private DisplayMetrics mDisplayMetrics;
64
Jim Millerd6523da2012-10-21 16:47:02 -070065 // Initialized during measurement from child layoutparams
Svetoslav Ganov74bdd212012-10-31 18:19:33 -070066 private View mExpandChallengeView;
Chris Wren052999f2012-11-02 14:36:56 -040067 private KeyguardSecurityContainer mChallengeView;
Adam Powelleee20932012-10-24 16:26:56 -070068 private View mScrimView;
Adam Powellcdf8b482012-10-30 20:56:45 -070069 private View mWidgetsView;
Jim Millerd6523da2012-10-21 16:47:02 -070070
71 // Range: 0 (fully hidden) to 1 (fully visible)
72 private float mChallengeOffset = 1.f;
73 private boolean mChallengeShowing = true;
Chris Wrenda8f6222012-11-06 17:36:18 -050074 private boolean mChallengeShowingTargetState = true;
Chris Wren03ad0c02012-11-06 14:45:18 -050075 private boolean mWasChallengeShowing = true;
Adam Powelleee20932012-10-24 16:26:56 -070076 private boolean mIsBouncing = false;
Jim Millerd6523da2012-10-21 16:47:02 -070077
78 private final Scroller mScroller;
Adam Cohendb1c5d52012-11-03 17:10:07 -070079 private ObjectAnimator mFader;
Jim Millerd6523da2012-10-21 16:47:02 -070080 private int mScrollState;
Adam Powell0b1b5522012-10-25 13:39:30 -070081 private OnChallengeScrolledListener mScrollListener;
82 private OnBouncerStateChangedListener mBouncerListener;
Jim Millerd6523da2012-10-21 16:47:02 -070083
84 public static final int SCROLL_STATE_IDLE = 0;
85 public static final int SCROLL_STATE_DRAGGING = 1;
86 public static final int SCROLL_STATE_SETTLING = 2;
Adam Cohendb1c5d52012-11-03 17:10:07 -070087 public static final int SCROLL_STATE_FADING = 3;
88
Adam Cohen8caabad2012-11-04 17:23:25 -080089 private static final int CHALLENGE_FADE_OUT_DURATION = 100;
90 private static final int CHALLENGE_FADE_IN_DURATION = 160;
Jim Millerd6523da2012-10-21 16:47:02 -070091
92 private static final int MAX_SETTLE_DURATION = 600; // ms
93
94 // ID of the pointer in charge of a current drag
95 private int mActivePointerId = INVALID_POINTER;
96 private static final int INVALID_POINTER = -1;
97
98 // True if the user is currently dragging the slider
99 private boolean mDragging;
100 // True if the user may not drag until a new gesture begins
101 private boolean mBlockDrag;
102
103 private VelocityTracker mVelocityTracker;
104 private int mMinVelocity;
105 private int mMaxVelocity;
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400106 private float mGestureStartX, mGestureStartY; // where did you first touch the screen?
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400107 private int mGestureStartChallengeBottom; // where was the challenge at that time?
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400108
109 private int mDragHandleClosedBelow; // handle hitrect extension into the challenge view
110 private int mDragHandleClosedAbove; // extend the handle's hitrect this far above the line
111 private int mDragHandleOpenBelow; // handle hitrect extension into the challenge view
112 private int mDragHandleOpenAbove; // extend the handle's hitrect this far above the line
113
Adam Powell0b1b5522012-10-25 13:39:30 -0700114 private int mDragHandleEdgeSlop;
Adam Powell67625e22012-10-26 15:12:30 -0700115 private int mChallengeBottomBound; // Number of pixels from the top of the challenge view
116 // that should remain on-screen
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400117
118 private int mTouchSlop;
Svetoslav Ganov74bdd212012-10-31 18:19:33 -0700119 private int mTouchSlopSquare;
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400120
Adam Powell6f352092012-10-25 20:49:32 -0700121 float mHandleAlpha;
122 float mFrameAlpha;
Adam Powellcdf8b482012-10-30 20:56:45 -0700123 float mFrameAnimationTarget = Float.MIN_VALUE;
Adam Powell6f352092012-10-25 20:49:32 -0700124 private ObjectAnimator mHandleAnimation;
125 private ObjectAnimator mFrameAnimation;
126
Adam Powellcdf8b482012-10-30 20:56:45 -0700127 private boolean mHasGlowpad;
Adam Cohendb1c5d52012-11-03 17:10:07 -0700128
129 // We have an internal and external version, and we and them together.
130 private boolean mChallengeInteractiveExternal = true;
131 private boolean mChallengeInteractiveInternal = true;
Adam Powellcdf8b482012-10-30 20:56:45 -0700132
Adam Powell6f352092012-10-25 20:49:32 -0700133 static final Property<SlidingChallengeLayout, Float> HANDLE_ALPHA =
134 new FloatProperty<SlidingChallengeLayout>("handleAlpha") {
135 @Override
136 public void setValue(SlidingChallengeLayout view, float value) {
137 view.mHandleAlpha = value;
138 view.invalidate();
139 }
140
141 @Override
142 public Float get(SlidingChallengeLayout view) {
143 return view.mHandleAlpha;
144 }
145 };
146
Jim Millerd6523da2012-10-21 16:47:02 -0700147 // True if at least one layout pass has happened since the view was attached.
148 private boolean mHasLayout;
149
150 private static final Interpolator sMotionInterpolator = new Interpolator() {
151 public float getInterpolation(float t) {
152 t -= 1.0f;
153 return t * t * t * t * t + 1.0f;
154 }
155 };
156
157 private static final Interpolator sHandleFadeInterpolator = new Interpolator() {
158 public float getInterpolation(float t) {
159 return t * t;
160 }
161 };
162
163 private final Runnable mEndScrollRunnable = new Runnable () {
164 public void run() {
165 completeChallengeScroll();
166 }
167 };
168
Adam Powelleee20932012-10-24 16:26:56 -0700169 private final OnClickListener mScrimClickListener = new OnClickListener() {
170 @Override
171 public void onClick(View v) {
172 hideBouncer();
173 }
174 };
175
Svetoslav Ganov74bdd212012-10-31 18:19:33 -0700176 private final OnClickListener mExpandChallengeClickListener = new OnClickListener() {
177 @Override
178 public void onClick(View v) {
John Spurlock78a8f122012-11-02 11:07:00 -0400179 if (!isChallengeShowing()) {
Svetoslav Ganov74bdd212012-10-31 18:19:33 -0700180 showChallenge(true);
181 }
182 }
183 };
184
Jim Millerd6523da2012-10-21 16:47:02 -0700185 /**
186 * Listener interface that reports changes in scroll state of the challenge area.
187 */
188 public interface OnChallengeScrolledListener {
189 /**
190 * The scroll state itself changed.
191 *
192 * <p>scrollState will be one of the following:</p>
193 *
194 * <ul>
195 * <li><code>SCROLL_STATE_IDLE</code> - The challenge area is stationary.</li>
196 * <li><code>SCROLL_STATE_DRAGGING</code> - The user is actively dragging
197 * the challenge area.</li>
198 * <li><code>SCROLL_STATE_SETTLING</code> - The challenge area is animating
199 * into place.</li>
200 * </ul>
201 *
202 * <p>Do not perform expensive operations (e.g. layout)
203 * while the scroll state is not <code>SCROLL_STATE_IDLE</code>.</p>
204 *
205 * @param scrollState The new scroll state of the challenge area.
206 */
207 public void onScrollStateChanged(int scrollState);
208
209 /**
210 * The precise position of the challenge area has changed.
211 *
212 * <p>NOTE: It is NOT safe to modify layout or call any View methods that may
213 * result in a requestLayout anywhere in your view hierarchy as a result of this call.
214 * It may be called during drawing.</p>
215 *
216 * @param scrollPosition New relative position of the challenge area.
217 * 1.f = fully visible/ready to be interacted with.
218 * 0.f = fully invisible/inaccessible to the user.
219 * @param challengeTop Position of the top edge of the challenge view in px in the
220 * SlidingChallengeLayout's coordinate system.
221 */
222 public void onScrollPositionChanged(float scrollPosition, int challengeTop);
223 }
224
225 public SlidingChallengeLayout(Context context) {
226 this(context, null);
227 }
228
229 public SlidingChallengeLayout(Context context, AttributeSet attrs) {
230 this(context, attrs, 0);
231 }
232
233 public SlidingChallengeLayout(Context context, AttributeSet attrs, int defStyle) {
234 super(context, attrs, defStyle);
235
Jim Millerd6523da2012-10-21 16:47:02 -0700236 mScroller = new Scroller(context, sMotionInterpolator);
237
238 final ViewConfiguration vc = ViewConfiguration.get(context);
239 mMinVelocity = vc.getScaledMinimumFlingVelocity();
240 mMaxVelocity = vc.getScaledMaximumFlingVelocity();
241
Adam Powellfa668cc2012-10-31 16:09:21 -0700242 final Resources res = getResources();
243 mDragHandleEdgeSlop = res.getDimensionPixelSize(R.dimen.kg_edge_swipe_region_size);
Adam Powell0b1b5522012-10-25 13:39:30 -0700244
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400245 mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
Svetoslav Ganov74bdd212012-10-31 18:19:33 -0700246 mTouchSlopSquare = mTouchSlop * mTouchSlop;
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400247
Adam Powell5da64302012-11-05 14:16:59 -0800248 mDisplayMetrics = res.getDisplayMetrics();
249 final float density = mDisplayMetrics.density;
Daniel Sandler1bfcc822012-10-26 11:25:49 -0400250
251 // top half of the lock icon, plus another 25% to be sure
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400252 mDragHandleClosedAbove = (int) (DRAG_HANDLE_CLOSED_ABOVE * density + 0.5f);
253 mDragHandleClosedBelow = (int) (DRAG_HANDLE_CLOSED_BELOW * density + 0.5f);
254 mDragHandleOpenAbove = (int) (DRAG_HANDLE_OPEN_ABOVE * density + 0.5f);
255 mDragHandleOpenBelow = (int) (DRAG_HANDLE_OPEN_BELOW * density + 0.5f);
256
257 // how much space to account for in the handle when closed
Adam Powellfa668cc2012-10-31 16:09:21 -0700258 mChallengeBottomBound = res.getDimensionPixelSize(R.dimen.kg_widget_pager_bottom_padding);
Daniel Sandler1bfcc822012-10-26 11:25:49 -0400259
Adam Powellcfc30862012-10-29 18:21:31 -0700260 setWillNotDraw(false);
Adam Powelleec4fe22012-11-07 18:07:15 -0800261 setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_STABLE);
Adam Powellcfc30862012-10-29 18:21:31 -0700262 }
263
John Spurlock6e36f102012-11-01 09:15:53 -0400264 public void setHandleAlpha(float alpha) {
265 if (mExpandChallengeView != null) {
266 mExpandChallengeView.setAlpha(alpha);
Jim Millerd6523da2012-10-21 16:47:02 -0700267 }
Adam Powell6f352092012-10-25 20:49:32 -0700268 }
269
John Spurlockbb5c9412012-10-31 09:46:15 -0400270 public void setChallengeInteractive(boolean interactive) {
Adam Cohendb1c5d52012-11-03 17:10:07 -0700271 mChallengeInteractiveExternal = interactive;
John Spurlock78a8f122012-11-02 11:07:00 -0400272 if (mExpandChallengeView != null) {
273 mExpandChallengeView.setEnabled(interactive);
274 }
John Spurlockbb5c9412012-10-31 09:46:15 -0400275 }
276
Adam Powell6f352092012-10-25 20:49:32 -0700277 void animateHandle(boolean visible) {
278 if (mHandleAnimation != null) {
279 mHandleAnimation.cancel();
Adam Powell67625e22012-10-26 15:12:30 -0700280 mHandleAnimation = null;
Jim Millerd6523da2012-10-21 16:47:02 -0700281 }
Adam Powell67625e22012-10-26 15:12:30 -0700282 final float targetAlpha = visible ? 1.f : 0.f;
283 if (targetAlpha == mHandleAlpha) {
284 return;
285 }
286 mHandleAnimation = ObjectAnimator.ofFloat(this, HANDLE_ALPHA, targetAlpha);
Adam Powell6f352092012-10-25 20:49:32 -0700287 mHandleAnimation.setInterpolator(sHandleFadeInterpolator);
288 mHandleAnimation.setDuration(HANDLE_ANIMATE_DURATION);
289 mHandleAnimation.start();
290 }
291
Jim Millerd6523da2012-10-21 16:47:02 -0700292 private void sendInitialListenerUpdates() {
Adam Powell0b1b5522012-10-25 13:39:30 -0700293 if (mScrollListener != null) {
Jim Millerd6523da2012-10-21 16:47:02 -0700294 int challengeTop = mChallengeView != null ? mChallengeView.getTop() : 0;
Adam Powell0b1b5522012-10-25 13:39:30 -0700295 mScrollListener.onScrollPositionChanged(mChallengeOffset, challengeTop);
296 mScrollListener.onScrollStateChanged(mScrollState);
Jim Millerd6523da2012-10-21 16:47:02 -0700297 }
298 }
299
300 public void setOnChallengeScrolledListener(OnChallengeScrolledListener listener) {
Adam Powell0b1b5522012-10-25 13:39:30 -0700301 mScrollListener = listener;
Jim Millerd6523da2012-10-21 16:47:02 -0700302 if (mHasLayout) {
303 sendInitialListenerUpdates();
304 }
305 }
306
Adam Powell0b1b5522012-10-25 13:39:30 -0700307 public void setOnBouncerStateChangedListener(OnBouncerStateChangedListener listener) {
308 mBouncerListener = listener;
309 }
310
Jim Millerd6523da2012-10-21 16:47:02 -0700311 @Override
312 public void onAttachedToWindow() {
313 super.onAttachedToWindow();
314
315 mHasLayout = false;
316 }
317
318 @Override
319 public void onDetachedFromWindow() {
320 super.onDetachedFromWindow();
321
322 removeCallbacks(mEndScrollRunnable);
323 mHasLayout = false;
324 }
325
Adam Powell0b1b5522012-10-25 13:39:30 -0700326 @Override
327 public void requestChildFocus(View child, View focused) {
328 if (mIsBouncing && child != mChallengeView) {
329 // Clear out of the bouncer if the user tries to move focus outside of
330 // the security challenge view.
331 hideBouncer();
332 }
333 super.requestChildFocus(child, focused);
334 }
335
Jim Millerd6523da2012-10-21 16:47:02 -0700336 // We want the duration of the page snap animation to be influenced by the distance that
337 // the screen has to travel, however, we don't want this duration to be effected in a
338 // purely linear fashion. Instead, we use this method to moderate the effect that the distance
339 // of travel has on the overall snap duration.
340 float distanceInfluenceForSnapDuration(float f) {
341 f -= 0.5f; // center the values about 0.
342 f *= 0.3f * Math.PI / 2.0f;
343 return (float) Math.sin(f);
344 }
345
346 void setScrollState(int state) {
347 if (mScrollState != state) {
348 mScrollState = state;
349
Adam Powell6f352092012-10-25 20:49:32 -0700350 animateHandle(state == SCROLL_STATE_IDLE && !mChallengeShowing);
Adam Powell0b1b5522012-10-25 13:39:30 -0700351 if (mScrollListener != null) {
352 mScrollListener.onScrollStateChanged(state);
Jim Millerd6523da2012-10-21 16:47:02 -0700353 }
354 }
355 }
356
357 void completeChallengeScroll() {
Chris Wrenda8f6222012-11-06 17:36:18 -0500358 setChallengeShowing(mChallengeShowingTargetState);
359 mChallengeOffset = mChallengeShowing ? 1.f : 0.f;
Jim Millerd6523da2012-10-21 16:47:02 -0700360 setScrollState(SCROLL_STATE_IDLE);
Adam Cohendb1c5d52012-11-03 17:10:07 -0700361 mChallengeInteractiveInternal = true;
Adam Powellcdf8b482012-10-30 20:56:45 -0700362 mChallengeView.setLayerType(LAYER_TYPE_NONE, null);
Jim Millerd6523da2012-10-21 16:47:02 -0700363 }
364
Adam Powelleee20932012-10-24 16:26:56 -0700365 void setScrimView(View scrim) {
366 if (mScrimView != null) {
367 mScrimView.setOnClickListener(null);
368 }
369 mScrimView = scrim;
370 mScrimView.setVisibility(mIsBouncing ? VISIBLE : GONE);
371 mScrimView.setFocusable(true);
372 mScrimView.setOnClickListener(mScrimClickListener);
373 }
374
Jim Millerd6523da2012-10-21 16:47:02 -0700375 /**
376 * Animate the bottom edge of the challenge view to the given position.
377 *
378 * @param y desired final position for the bottom edge of the challenge view in px
379 * @param velocity velocity in
380 */
381 void animateChallengeTo(int y, int velocity) {
382 if (mChallengeView == null) {
383 // Nothing to do.
384 return;
385 }
Adam Cohendb1c5d52012-11-03 17:10:07 -0700386
Adam Cohen8caabad2012-11-04 17:23:25 -0800387 cancelTransitionsInProgress();
Adam Cohendb1c5d52012-11-03 17:10:07 -0700388
389 mChallengeInteractiveInternal = false;
390 mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null);
Adam Powell67625e22012-10-26 15:12:30 -0700391 final int sy = mChallengeView.getBottom();
392 final int dy = y - sy;
Jim Millerd6523da2012-10-21 16:47:02 -0700393 if (dy == 0) {
394 completeChallengeScroll();
395 return;
396 }
397
398 setScrollState(SCROLL_STATE_SETTLING);
399
400 final int childHeight = mChallengeView.getHeight();
401 final int halfHeight = childHeight / 2;
402 final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / childHeight);
403 final float distance = halfHeight + halfHeight *
404 distanceInfluenceForSnapDuration(distanceRatio);
405
406 int duration = 0;
407 velocity = Math.abs(velocity);
408 if (velocity > 0) {
409 duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
410 } else {
411 final float childDelta = (float) Math.abs(dy) / childHeight;
412 duration = (int) ((childDelta + 1) * 100);
413 }
414 duration = Math.min(duration, MAX_SETTLE_DURATION);
415
416 mScroller.startScroll(0, sy, 0, dy, duration);
417 postInvalidateOnAnimation();
418 }
419
420 private void setChallengeShowing(boolean showChallenge) {
Svetoslav Ganov74bdd212012-10-31 18:19:33 -0700421 if (mChallengeShowing == showChallenge) {
422 return;
423 }
Adam Powell67625e22012-10-26 15:12:30 -0700424 mChallengeShowing = showChallenge;
Adam Powell5a2c5f02012-11-05 15:41:07 -0800425
426 if (mExpandChallengeView == null || mChallengeView == null) {
427 // These might not be here yet if we haven't been through layout.
428 // If we haven't, the first layout pass will set everything up correctly
429 // based on mChallengeShowing as set above.
430 return;
431 }
432
Svetoslav Ganov74bdd212012-10-31 18:19:33 -0700433 if (mChallengeShowing) {
434 mExpandChallengeView.setVisibility(View.INVISIBLE);
435 mChallengeView.setVisibility(View.VISIBLE);
436 if (AccessibilityManager.getInstance(mContext).isEnabled()) {
437 mChallengeView.requestAccessibilityFocus();
438 mChallengeView.announceForAccessibility(mContext.getString(
439 R.string.keyguard_accessibility_unlock_area_expanded));
440 }
441 } else {
442 mExpandChallengeView.setVisibility(View.VISIBLE);
443 mChallengeView.setVisibility(View.INVISIBLE);
444 if (AccessibilityManager.getInstance(mContext).isEnabled()) {
445 mExpandChallengeView.requestAccessibilityFocus();
446 mChallengeView.announceForAccessibility(mContext.getString(
447 R.string.keyguard_accessibility_unlock_area_collapsed));
448 }
449 }
Jim Millerd6523da2012-10-21 16:47:02 -0700450 }
451
452 /**
453 * @return true if the challenge is at all visible.
454 */
455 public boolean isChallengeShowing() {
456 return mChallengeShowing;
457 }
458
459 @Override
Jim Miller19a52672012-10-23 19:52:04 -0700460 public boolean isChallengeOverlapping() {
461 return mChallengeShowing;
462 }
463
464 @Override
Adam Powelleee20932012-10-24 16:26:56 -0700465 public boolean isBouncing() {
466 return mIsBouncing;
467 }
468
469 @Override
Winson Chung70c2f872012-11-07 21:47:12 -0800470 public int getBouncerAnimationDuration() {
471 return HANDLE_ANIMATE_DURATION;
472 }
473
474 @Override
Adam Powelleee20932012-10-24 16:26:56 -0700475 public void showBouncer() {
476 if (mIsBouncing) return;
Chris Wren03ad0c02012-11-06 14:45:18 -0500477 mWasChallengeShowing = mChallengeShowing;
Chris Wren03ad0c02012-11-06 14:45:18 -0500478 mIsBouncing = true;
Winson Chung6cf53bb2012-11-05 17:55:42 -0800479 showChallenge(true);
Adam Powelleee20932012-10-24 16:26:56 -0700480 if (mScrimView != null) {
Winson Chung70c2f872012-11-07 21:47:12 -0800481 Animator anim = ObjectAnimator.ofFloat(mScrimView, "alpha", 1f);
482 anim.setDuration(HANDLE_ANIMATE_DURATION);
483 anim.addListener(new AnimatorListenerAdapter() {
484 @Override
485 public void onAnimationStart(Animator animation) {
486 mScrimView.setVisibility(VISIBLE);
487 }
488 });
489 anim.start();
Adam Powelleee20932012-10-24 16:26:56 -0700490 }
Chris Wren052999f2012-11-02 14:36:56 -0400491 if (mChallengeView != null) {
492 mChallengeView.showBouncer(HANDLE_ANIMATE_DURATION);
493 }
Adam Powellcdf8b482012-10-30 20:56:45 -0700494
Adam Powell0b1b5522012-10-25 13:39:30 -0700495 if (mBouncerListener != null) {
496 mBouncerListener.onBouncerStateChanged(true);
497 }
Adam Powelleee20932012-10-24 16:26:56 -0700498 }
499
500 @Override
501 public void hideBouncer() {
502 if (!mIsBouncing) return;
Chris Wren03ad0c02012-11-06 14:45:18 -0500503 if (!mWasChallengeShowing) showChallenge(false);
Adam Powelleee20932012-10-24 16:26:56 -0700504 mIsBouncing = false;
Winson Chung70c2f872012-11-07 21:47:12 -0800505
Adam Powelleee20932012-10-24 16:26:56 -0700506 if (mScrimView != null) {
Winson Chung70c2f872012-11-07 21:47:12 -0800507 Animator anim = ObjectAnimator.ofFloat(mScrimView, "alpha", 0f);
508 anim.setDuration(HANDLE_ANIMATE_DURATION);
509 anim.addListener(new AnimatorListenerAdapter() {
510 @Override
511 public void onAnimationEnd(Animator animation) {
512 mScrimView.setVisibility(GONE);
513 }
514 });
515 anim.start();
Adam Powelleee20932012-10-24 16:26:56 -0700516 }
Chris Wren052999f2012-11-02 14:36:56 -0400517 if (mChallengeView != null) {
518 mChallengeView.hideBouncer(HANDLE_ANIMATE_DURATION);
519 }
Adam Powell0b1b5522012-10-25 13:39:30 -0700520 if (mBouncerListener != null) {
521 mBouncerListener.onBouncerStateChanged(false);
522 }
Adam Powelleee20932012-10-24 16:26:56 -0700523 }
524
Adam Powellcdf8b482012-10-30 20:56:45 -0700525 private int getChallengeMargin(boolean expanded) {
526 return expanded && mHasGlowpad ? 0 : mDragHandleEdgeSlop;
527 }
528
Adam Powellfa668cc2012-10-31 16:09:21 -0700529 private float getChallengeAlpha() {
530 float x = mChallengeOffset - 1;
531 return x * x * x + 1.f;
532 }
533
Adam Powelleee20932012-10-24 16:26:56 -0700534 @Override
Jim Millerd6523da2012-10-21 16:47:02 -0700535 public void requestDisallowInterceptTouchEvent(boolean allowIntercept) {
536 // We'll intercept whoever we feel like! ...as long as it isn't a challenge view.
537 // If there are one or more pointers in the challenge view before we take over
538 // touch events, onInterceptTouchEvent will set mBlockDrag.
539 }
540
541 @Override
542 public boolean onInterceptTouchEvent(MotionEvent ev) {
543 if (mVelocityTracker == null) {
544 mVelocityTracker = VelocityTracker.obtain();
545 }
546 mVelocityTracker.addMovement(ev);
547
548 final int action = ev.getActionMasked();
549 switch (action) {
Jim Miller022554e2012-10-22 19:03:06 -0700550 case MotionEvent.ACTION_DOWN:
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400551 mGestureStartX = ev.getX();
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400552 mGestureStartY = ev.getY();
Adam Powell995270f2012-10-26 16:10:43 -0700553 mBlockDrag = false;
Jim Miller022554e2012-10-22 19:03:06 -0700554 break;
555
Jim Millerd6523da2012-10-21 16:47:02 -0700556 case MotionEvent.ACTION_CANCEL:
557 case MotionEvent.ACTION_UP:
558 resetTouch();
559 break;
560
561 case MotionEvent.ACTION_MOVE:
562 final int count = ev.getPointerCount();
563 for (int i = 0; i < count; i++) {
564 final float x = ev.getX(i);
565 final float y = ev.getY(i);
Adam Cohendb1c5d52012-11-03 17:10:07 -0700566 if (!mIsBouncing && mActivePointerId == INVALID_POINTER
Adam Powell9c2c77f2012-11-01 14:52:17 -0700567 && (crossedDragHandle(x, y, mGestureStartY)
Adam Powellc238af52012-11-01 13:46:17 -0700568 || (isInChallengeView(x, y) &&
Adam Powell9c2c77f2012-11-01 14:52:17 -0700569 mScrollState == SCROLL_STATE_SETTLING))) {
Jim Millerd6523da2012-10-21 16:47:02 -0700570 mActivePointerId = ev.getPointerId(i);
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400571 mGestureStartX = x;
572 mGestureStartY = y;
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400573 mGestureStartChallengeBottom = getChallengeBottom();
Jim Millerd6523da2012-10-21 16:47:02 -0700574 mDragging = true;
Adam Powellcdf8b482012-10-30 20:56:45 -0700575 mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null);
Adam Powell9c2c77f2012-11-01 14:52:17 -0700576 } else if (mChallengeShowing && isInChallengeView(x, y)) {
Jim Millerd6523da2012-10-21 16:47:02 -0700577 mBlockDrag = true;
578 }
579 }
580 break;
581 }
582
Adam Cohendb1c5d52012-11-03 17:10:07 -0700583 if (mBlockDrag || isChallengeInteractionBlocked()) {
Jim Millerd6523da2012-10-21 16:47:02 -0700584 mActivePointerId = INVALID_POINTER;
585 mDragging = false;
586 }
587
588 return mDragging;
589 }
590
Adam Cohendb1c5d52012-11-03 17:10:07 -0700591 private boolean isChallengeInteractionBlocked() {
592 return !mChallengeInteractiveExternal || !mChallengeInteractiveInternal;
593 }
594
Jim Millerd6523da2012-10-21 16:47:02 -0700595 private void resetTouch() {
596 mVelocityTracker.recycle();
597 mVelocityTracker = null;
598 mActivePointerId = INVALID_POINTER;
599 mDragging = mBlockDrag = false;
600 }
601
602 @Override
603 public boolean onTouchEvent(MotionEvent ev) {
604 if (mVelocityTracker == null) {
605 mVelocityTracker = VelocityTracker.obtain();
606 }
607 mVelocityTracker.addMovement(ev);
608
609 final int action = ev.getActionMasked();
610 switch (action) {
Adam Powell995270f2012-10-26 16:10:43 -0700611 case MotionEvent.ACTION_DOWN:
612 mBlockDrag = false;
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400613 mGestureStartX = ev.getX();
Adam Powell995270f2012-10-26 16:10:43 -0700614 mGestureStartY = ev.getY();
615 break;
616
Jim Millerd6523da2012-10-21 16:47:02 -0700617 case MotionEvent.ACTION_CANCEL:
Adam Cohendb1c5d52012-11-03 17:10:07 -0700618 if (mDragging && !isChallengeInteractionBlocked()) {
Jim Millerd6523da2012-10-21 16:47:02 -0700619 showChallenge(0);
620 }
621 resetTouch();
622 break;
623
624 case MotionEvent.ACTION_POINTER_UP:
625 if (mActivePointerId != ev.getPointerId(ev.getActionIndex())) {
626 break;
627 }
628 case MotionEvent.ACTION_UP:
Adam Cohendb1c5d52012-11-03 17:10:07 -0700629 if (mDragging && !isChallengeInteractionBlocked()) {
Jim Millerd6523da2012-10-21 16:47:02 -0700630 mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
631 showChallenge((int) mVelocityTracker.getYVelocity(mActivePointerId));
632 }
633 resetTouch();
634 break;
635
636 case MotionEvent.ACTION_MOVE:
Adam Powell6f352092012-10-25 20:49:32 -0700637 if (!mDragging && !mBlockDrag && !mIsBouncing) {
Jim Millerd6523da2012-10-21 16:47:02 -0700638 final int count = ev.getPointerCount();
639 for (int i = 0; i < count; i++) {
640 final float x = ev.getX(i);
641 final float y = ev.getY(i);
642
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400643 if ((isInDragHandle(x, y) || crossedDragHandle(x, y, mGestureStartY) ||
Jim Millerd6523da2012-10-21 16:47:02 -0700644 (isInChallengeView(x, y) && mScrollState == SCROLL_STATE_SETTLING))
John Spurlock1ec46462012-11-02 11:23:27 -0400645 && mActivePointerId == INVALID_POINTER
Adam Cohendb1c5d52012-11-03 17:10:07 -0700646 && !isChallengeInteractionBlocked()) {
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400647 mGestureStartX = x;
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400648 mGestureStartY = y;
Jim Millerd6523da2012-10-21 16:47:02 -0700649 mActivePointerId = ev.getPointerId(i);
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400650 mGestureStartChallengeBottom = getChallengeBottom();
Jim Millerd6523da2012-10-21 16:47:02 -0700651 mDragging = true;
Adam Powellcdf8b482012-10-30 20:56:45 -0700652 mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null);
Jim Millerd6523da2012-10-21 16:47:02 -0700653 break;
654 }
655 }
656 }
657 // Not an else; this can be set above.
658 if (mDragging) {
659 // No-op if already in this state, but set it here in case we arrived
660 // at this point from either intercept or the above.
661 setScrollState(SCROLL_STATE_DRAGGING);
662
663 final int index = ev.findPointerIndex(mActivePointerId);
664 if (index < 0) {
665 // Oops, bogus state. We lost some touch events somewhere.
666 // Just drop it with no velocity and let things settle.
667 resetTouch();
668 showChallenge(0);
669 return true;
670 }
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400671 final float y = ev.getY(index);
672 final float pos = Math.min(y - mGestureStartY,
Adam Powell67625e22012-10-26 15:12:30 -0700673 getLayoutBottom() - mChallengeBottomBound);
Jim Millerd6523da2012-10-21 16:47:02 -0700674
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400675 moveChallengeTo(mGestureStartChallengeBottom + (int) pos);
Jim Millerd6523da2012-10-21 16:47:02 -0700676 }
677 break;
678 }
679 return true;
680 }
681
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400682 /**
Adam Powellcdf8b482012-10-30 20:56:45 -0700683 * The lifecycle of touch events is subtle and it's very easy to do something
684 * that will cause bugs that will be nasty to track when overriding this method.
685 * Normally one should always override onInterceptTouchEvent instead.
686 *
687 * To put it another way, don't try this at home.
688 */
689 @Override
690 public boolean dispatchTouchEvent(MotionEvent ev) {
691 final int action = ev.getActionMasked();
692 boolean handled = false;
693 if (action == MotionEvent.ACTION_DOWN) {
694 // Defensive programming: if we didn't get the UP or CANCEL, reset anyway.
695 mEdgeCaptured = false;
696 }
697 if (mWidgetsView != null && !mIsBouncing && (mEdgeCaptured || isEdgeSwipeBeginEvent(ev))) {
698 // Normally we would need to do a lot of extra stuff here.
699 // We can only get away with this because we haven't padded in
700 // the widget pager or otherwise transformed it during layout.
701 // We also don't support things like splitting MotionEvents.
702
703 // We set handled to captured even if dispatch is returning false here so that
704 // we don't send a different view a busted or incomplete event stream.
705 handled = mEdgeCaptured |= mWidgetsView.dispatchTouchEvent(ev);
706 }
707
708 if (!handled && !mEdgeCaptured) {
709 handled = super.dispatchTouchEvent(ev);
710 }
711
712 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
713 mEdgeCaptured = false;
714 }
715
716 return handled;
717 }
718
719 private boolean isEdgeSwipeBeginEvent(MotionEvent ev) {
720 if (ev.getActionMasked() != MotionEvent.ACTION_DOWN) {
721 return false;
722 }
723
724 final float x = ev.getX();
725 return x < mDragHandleEdgeSlop || x >= getWidth() - mDragHandleEdgeSlop;
726 }
727
728 /**
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400729 * We only want to add additional vertical space to the drag handle when the panel is fully
730 * closed.
731 */
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400732 private int getDragHandleSizeAbove() {
733 return isChallengeShowing() ? mDragHandleOpenAbove : mDragHandleClosedAbove;
734 }
735 private int getDragHandleSizeBelow() {
736 return isChallengeShowing() ? mDragHandleOpenBelow : mDragHandleClosedBelow;
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400737 }
738
Jim Millerd6523da2012-10-21 16:47:02 -0700739 private boolean isInChallengeView(float x, float y) {
Svetoslav Ganov74bdd212012-10-31 18:19:33 -0700740 return isPointInView(x, y, mChallengeView);
Jim Millerd6523da2012-10-21 16:47:02 -0700741 }
742
743 private boolean isInDragHandle(float x, float y) {
Svetoslav Ganov74bdd212012-10-31 18:19:33 -0700744 return isPointInView(x, y, mExpandChallengeView);
745 }
Jim Millerd6523da2012-10-21 16:47:02 -0700746
Svetoslav Ganov74bdd212012-10-31 18:19:33 -0700747 private boolean isPointInView(float x, float y, View view) {
748 if (view == null) {
749 return false;
750 }
751 return x >= view.getLeft() && y >= view.getTop()
752 && x < view.getRight() && y < view.getBottom();
Jim Millerd6523da2012-10-21 16:47:02 -0700753 }
754
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400755 private boolean crossedDragHandle(float x, float y, float initialY) {
Adam Powell9c2c77f2012-11-01 14:52:17 -0700756
Jim Miller022554e2012-10-22 19:03:06 -0700757 final int challengeTop = mChallengeView.getTop();
Adam Powell9c2c77f2012-11-01 14:52:17 -0700758 final boolean horizOk = x >= 0 && x < getWidth();
759
760 final boolean vertOk;
761 if (mChallengeShowing) {
762 vertOk = initialY < (challengeTop - getDragHandleSizeAbove()) &&
763 y > challengeTop + getDragHandleSizeBelow();
764 } else {
765 vertOk = initialY > challengeTop + getDragHandleSizeBelow() &&
766 y < challengeTop - getDragHandleSizeAbove();
767 }
768 return horizOk && vertOk;
Jim Miller022554e2012-10-22 19:03:06 -0700769 }
770
Adam Powell97997142012-11-06 21:32:42 -0800771 private int makeChildMeasureSpec(int maxSize, int childDimen) {
772 final int mode;
773 final int size;
774 switch (childDimen) {
775 case LayoutParams.WRAP_CONTENT:
776 mode = MeasureSpec.AT_MOST;
777 size = maxSize;
778 break;
779 case LayoutParams.MATCH_PARENT:
780 mode = MeasureSpec.EXACTLY;
781 size = maxSize;
782 break;
783 default:
784 mode = MeasureSpec.EXACTLY;
785 size = Math.min(maxSize, childDimen);
786 break;
787 }
788 return MeasureSpec.makeMeasureSpec(size, mode);
789 }
790
Jim Millerd6523da2012-10-21 16:47:02 -0700791 @Override
792 protected void onMeasure(int widthSpec, int heightSpec) {
793 if (MeasureSpec.getMode(widthSpec) != MeasureSpec.EXACTLY ||
794 MeasureSpec.getMode(heightSpec) != MeasureSpec.EXACTLY) {
795 throw new IllegalArgumentException(
796 "SlidingChallengeLayout must be measured with an exact size");
797 }
798
799 final int width = MeasureSpec.getSize(widthSpec);
800 final int height = MeasureSpec.getSize(heightSpec);
801 setMeasuredDimension(width, height);
802
803 // Find one and only one challenge view.
804 final View oldChallengeView = mChallengeView;
Svetoslav Ganov74bdd212012-10-31 18:19:33 -0700805 final View oldExpandChallengeView = mChallengeView;
Jim Millerd6523da2012-10-21 16:47:02 -0700806 mChallengeView = null;
Svetoslav Ganov74bdd212012-10-31 18:19:33 -0700807 mExpandChallengeView = null;
Jim Millerd6523da2012-10-21 16:47:02 -0700808 final int count = getChildCount();
Adam Cohen8a7785c2012-10-29 22:01:33 -0700809
810 // First iteration through the children finds special children and sets any associated
811 // state.
Jim Millerd6523da2012-10-21 16:47:02 -0700812 for (int i = 0; i < count; i++) {
813 final View child = getChildAt(i);
814 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
Adam Powelleee20932012-10-24 16:26:56 -0700815 if (lp.childType == LayoutParams.CHILD_TYPE_CHALLENGE) {
Jim Millerd6523da2012-10-21 16:47:02 -0700816 if (mChallengeView != null) {
817 throw new IllegalStateException(
818 "There may only be one child with layout_isChallenge=\"true\"");
819 }
Chris Wren052999f2012-11-02 14:36:56 -0400820 if (!(child instanceof KeyguardSecurityContainer)) {
821 throw new IllegalArgumentException(
822 "Challenge must be a KeyguardSecurityContainer");
823 }
824 mChallengeView = (KeyguardSecurityContainer) child;
Jim Millerd6523da2012-10-21 16:47:02 -0700825 if (mChallengeView != oldChallengeView) {
826 mChallengeView.setVisibility(mChallengeShowing ? VISIBLE : INVISIBLE);
827 }
Adam Powell6f352092012-10-25 20:49:32 -0700828 // We're going to play silly games with the frame's background drawable later.
Adam Powellcdf8b482012-10-30 20:56:45 -0700829 if (!mHasLayout) {
830 // Set up the margin correctly based on our content for the first run.
831 mHasGlowpad = child.findViewById(R.id.keyguard_selector_view) != null;
832 lp.leftMargin = lp.rightMargin = getChallengeMargin(true);
833 }
Svetoslav Ganov74bdd212012-10-31 18:19:33 -0700834 } else if (lp.childType == LayoutParams.CHILD_TYPE_EXPAND_CHALLENGE_HANDLE) {
835 if (mExpandChallengeView != null) {
836 throw new IllegalStateException(
837 "There may only be one child with layout_childType"
838 + "=\"expandChallengeHandle\"");
839 }
840 mExpandChallengeView = child;
841 if (mExpandChallengeView != oldExpandChallengeView) {
842 mExpandChallengeView.setVisibility(mChallengeShowing ? INVISIBLE : VISIBLE);
843 mExpandChallengeView.setOnClickListener(mExpandChallengeClickListener);
844 }
Adam Powelleee20932012-10-24 16:26:56 -0700845 } else if (lp.childType == LayoutParams.CHILD_TYPE_SCRIM) {
846 setScrimView(child);
Adam Powellcdf8b482012-10-30 20:56:45 -0700847 } else if (lp.childType == LayoutParams.CHILD_TYPE_WIDGETS) {
848 mWidgetsView = child;
Jim Millerd6523da2012-10-21 16:47:02 -0700849 }
Adam Cohen8a7785c2012-10-29 22:01:33 -0700850 }
Jim Millerd6523da2012-10-21 16:47:02 -0700851
Adam Cohen5be14de2012-10-30 11:19:48 -0700852 // We want to measure the challenge view first, since the KeyguardWidgetPager
853 // needs to do things its measure pass that are dependent on the challenge view
854 // having been measured.
Svetoslav Ganov74bdd212012-10-31 18:19:33 -0700855 if (mChallengeView != null && mChallengeView.getVisibility() != View.GONE) {
Adam Powell97997142012-11-06 21:32:42 -0800856 // This one's a little funny. If the IME is present - reported in the form
857 // of insets on the root view - we only give the challenge the space it would
858 // have had if the IME wasn't there in order to keep the rest of the layout stable.
859 // We base this on the layout_maxHeight on the challenge view. If it comes out
860 // negative or zero, either we didn't have a maxHeight or we're totally out of space,
861 // so give up and measure as if this rule weren't there.
862 int challengeHeightSpec = heightSpec;
863 final View root = getRootView();
864 if (root != null) {
865 final LayoutParams lp = (LayoutParams) mChallengeView.getLayoutParams();
866 final int specSize = MeasureSpec.getSize(heightSpec);
867 final int windowHeight = mDisplayMetrics.heightPixels - root.getPaddingTop();
868 final int diff = windowHeight - specSize;
869 final int maxChallengeHeight = lp.maxHeight - diff;
870 if (maxChallengeHeight > 0) {
871 challengeHeightSpec = makeChildMeasureSpec(maxChallengeHeight, lp.height);
872 }
873 }
874 measureChildWithMargins(mChallengeView, widthSpec, 0, challengeHeightSpec, 0);
Adam Cohen8a7785c2012-10-29 22:01:33 -0700875 }
876
877 // Measure the rest of the children
878 for (int i = 0; i < count; i++) {
879 final View child = getChildAt(i);
Svetoslav Ganov74bdd212012-10-31 18:19:33 -0700880 if (child.getVisibility() == GONE) {
881 continue;
882 }
Adam Cohen8a7785c2012-10-29 22:01:33 -0700883 // Don't measure the challenge view twice!
Adam Powell5da64302012-11-05 14:16:59 -0800884 if (child == mChallengeView) continue;
885
886 // Measure children. Widget frame measures special, so that we can ignore
887 // insets for the IME.
888 int parentWidthSpec = widthSpec, parentHeightSpec = heightSpec;
889 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
890 if (lp.childType == LayoutParams.CHILD_TYPE_WIDGETS) {
891 final View root = getRootView();
892 if (root != null) {
893 // This calculation is super dodgy and relies on several assumptions.
894 // Specifically that the root of the window will be padded in for insets
895 // and that the window is LAYOUT_IN_SCREEN.
896 final int windowWidth = mDisplayMetrics.widthPixels;
897 final int windowHeight = mDisplayMetrics.heightPixels - root.getPaddingTop();
898 parentWidthSpec = MeasureSpec.makeMeasureSpec(
899 windowWidth, MeasureSpec.EXACTLY);
900 parentHeightSpec = MeasureSpec.makeMeasureSpec(
901 windowHeight, MeasureSpec.EXACTLY);
902 }
Adam Cohen8a7785c2012-10-29 22:01:33 -0700903 }
Adam Powell5da64302012-11-05 14:16:59 -0800904 measureChildWithMargins(child, parentWidthSpec, 0, parentHeightSpec, 0);
Jim Millerd6523da2012-10-21 16:47:02 -0700905 }
906 }
907
908 @Override
909 protected void onLayout(boolean changed, int l, int t, int r, int b) {
910 final int paddingLeft = getPaddingLeft();
911 final int paddingTop = getPaddingTop();
912 final int paddingRight = getPaddingRight();
913 final int paddingBottom = getPaddingBottom();
914 final int width = r - l;
915 final int height = b - t;
916
917 final int count = getChildCount();
918 for (int i = 0; i < count; i++) {
919 final View child = getChildAt(i);
920
921 if (child.getVisibility() == GONE) continue;
922
923 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
924
Adam Powelleee20932012-10-24 16:26:56 -0700925 if (lp.childType == LayoutParams.CHILD_TYPE_CHALLENGE) {
Jim Millerd6523da2012-10-21 16:47:02 -0700926 // Challenge views pin to the bottom, offset by a portion of their height,
927 // and center horizontally.
928 final int center = (paddingLeft + width - paddingRight) / 2;
929 final int childWidth = child.getMeasuredWidth();
930 final int childHeight = child.getMeasuredHeight();
931 final int left = center - childWidth / 2;
932 final int layoutBottom = height - paddingBottom - lp.bottomMargin;
933 // We use the top of the challenge view to position the handle, so
934 // we never want less than the handle size showing at the bottom.
Adam Powell67625e22012-10-26 15:12:30 -0700935 final int bottom = layoutBottom + (int) ((childHeight - mChallengeBottomBound)
Jim Millerd6523da2012-10-21 16:47:02 -0700936 * (1 - mChallengeOffset));
Adam Powellfa668cc2012-10-31 16:09:21 -0700937 child.setAlpha(getChallengeAlpha());
Jim Millerd6523da2012-10-21 16:47:02 -0700938 child.layout(left, bottom - childHeight, left + childWidth, bottom);
Svetoslav Ganov74bdd212012-10-31 18:19:33 -0700939 } else if (lp.childType == LayoutParams.CHILD_TYPE_EXPAND_CHALLENGE_HANDLE) {
Svetoslav Ganov0f87a722012-11-01 11:09:37 -0700940 final int center = (paddingLeft + width - paddingRight) / 2;
941 final int left = center - child.getMeasuredWidth() / 2;
942 final int right = left + child.getMeasuredWidth();
943 final int bottom = height - paddingBottom - lp.bottomMargin;
944 final int top = bottom - child.getMeasuredHeight();
945 child.layout(left, top, right, bottom);
Jim Millerd6523da2012-10-21 16:47:02 -0700946 } else {
947 // Non-challenge views lay out from the upper left, layered.
948 child.layout(paddingLeft + lp.leftMargin,
949 paddingTop + lp.topMargin,
950 paddingLeft + child.getMeasuredWidth(),
951 paddingTop + child.getMeasuredHeight());
952 }
953 }
954
955 if (!mHasLayout) {
Adam Powell6f352092012-10-25 20:49:32 -0700956 mHasLayout = true;
Jim Millerd6523da2012-10-21 16:47:02 -0700957 }
Jim Millerd6523da2012-10-21 16:47:02 -0700958 }
959
Svetoslav Ganov74bdd212012-10-31 18:19:33 -0700960 @Override
961 public void draw(Canvas c) {
962 super.draw(c);
963 if (DEBUG) {
964 final Paint debugPaint = new Paint();
965 debugPaint.setColor(0x40FF00CC);
966 // show the isInDragHandle() rect
967 c.drawRect(mDragHandleEdgeSlop,
968 mChallengeView.getTop() - getDragHandleSizeAbove(),
969 getWidth() - mDragHandleEdgeSlop,
970 mChallengeView.getTop() + getDragHandleSizeBelow(),
971 debugPaint);
972 }
973 }
974
Jim Millerd6523da2012-10-21 16:47:02 -0700975 public void computeScroll() {
976 super.computeScroll();
977
978 if (!mScroller.isFinished()) {
979 if (mChallengeView == null) {
980 // Can't scroll if the view is missing.
981 Log.e(TAG, "Challenge view missing in computeScroll");
982 mScroller.abortAnimation();
983 return;
984 }
985
986 mScroller.computeScrollOffset();
987 moveChallengeTo(mScroller.getCurrY());
988
989 if (mScroller.isFinished()) {
990 post(mEndScrollRunnable);
991 }
992 }
993 }
994
Adam Cohen8caabad2012-11-04 17:23:25 -0800995 private void cancelTransitionsInProgress() {
996 if (!mScroller.isFinished()) {
997 mScroller.abortAnimation();
998 completeChallengeScroll();
999 }
1000 if (mFader != null) {
1001 mFader.cancel();
1002 }
1003 }
1004
1005 public void fadeInChallenge() {
1006 fadeChallenge(true);
1007 }
1008
1009 public void fadeOutChallenge() {
1010 fadeChallenge(false);
1011 }
1012
1013 public void fadeChallenge(final boolean show) {
1014 if (mChallengeView != null) {
1015
1016 cancelTransitionsInProgress();
1017 float alpha = show ? 1f : 0f;
1018 int duration = show ? CHALLENGE_FADE_IN_DURATION : CHALLENGE_FADE_OUT_DURATION;
1019 mFader = ObjectAnimator.ofFloat(mChallengeView, "alpha", alpha);
1020 mFader.addListener(new AnimatorListenerAdapter() {
1021 @Override
1022 public void onAnimationStart(Animator animation) {
1023 onFadeStart(show);
1024 }
1025 @Override
1026 public void onAnimationEnd(Animator animation) {
1027 onFadeEnd(show);
1028 }
1029 });
1030 mFader.setDuration(duration);
1031 mFader.start();
1032 }
1033 }
1034
1035 private int getMaxChallengeBottom() {
1036 if (mChallengeView == null) return 0;
1037 final int layoutBottom = getLayoutBottom();
1038 final int challengeHeight = mChallengeView.getMeasuredHeight();
1039
1040 return (layoutBottom + challengeHeight - mChallengeBottomBound);
1041 }
1042
1043 private int getMinChallengeBottom() {
1044 return getLayoutBottom();
1045 }
1046
1047
1048 private void onFadeStart(boolean show) {
1049 mChallengeInteractiveInternal = false;
1050 mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null);
1051
1052 if (show) {
1053 moveChallengeTo(getMinChallengeBottom());
1054 }
1055
1056 setScrollState(SCROLL_STATE_FADING);
1057 }
1058
1059 private void onFadeEnd(boolean show) {
1060 mChallengeInteractiveInternal = true;
1061 setChallengeShowing(show);
1062
1063 if (!show) {
1064 moveChallengeTo(getMaxChallengeBottom());
1065 }
1066
1067 mChallengeView.setLayerType(LAYER_TYPE_NONE, null);
1068 mFader = null;
1069 setScrollState(SCROLL_STATE_IDLE);
1070 }
1071
Adam Cohene3643132012-10-28 18:29:17 -07001072 public int getMaxChallengeTop() {
1073 if (mChallengeView == null) return 0;
1074
1075 final int layoutBottom = getLayoutBottom();
Adam Cohen8a7785c2012-10-29 22:01:33 -07001076 final int challengeHeight = mChallengeView.getMeasuredHeight();
Adam Cohene3643132012-10-28 18:29:17 -07001077 return layoutBottom - challengeHeight;
1078 }
1079
Jim Millerd6523da2012-10-21 16:47:02 -07001080 /**
1081 * Move the bottom edge of mChallengeView to a new position and notify the listener
1082 * if it represents a change in position. Changes made through this method will
1083 * be stable across layout passes. If this method is called before first layout of
1084 * this SlidingChallengeLayout it will have no effect.
1085 *
1086 * @param bottom New bottom edge in px in this SlidingChallengeLayout's coordinate system.
1087 * @return true if the challenge view was moved
1088 */
1089 private boolean moveChallengeTo(int bottom) {
1090 if (mChallengeView == null || !mHasLayout) {
1091 return false;
1092 }
1093
Daniel Sandlerb42b8422012-10-26 12:50:27 -04001094 final int layoutBottom = getLayoutBottom();
Jim Millerd6523da2012-10-21 16:47:02 -07001095 final int challengeHeight = mChallengeView.getHeight();
1096
Adam Cohen8caabad2012-11-04 17:23:25 -08001097 bottom = Math.max(getMinChallengeBottom(),
1098 Math.min(bottom, getMaxChallengeBottom()));
Jim Millerd6523da2012-10-21 16:47:02 -07001099
Adam Powell67625e22012-10-26 15:12:30 -07001100 float offset = 1.f - (float) (bottom - layoutBottom) /
1101 (challengeHeight - mChallengeBottomBound);
Jim Millerd6523da2012-10-21 16:47:02 -07001102 mChallengeOffset = offset;
1103 if (offset > 0 && !mChallengeShowing) {
1104 setChallengeShowing(true);
1105 }
1106
1107 mChallengeView.layout(mChallengeView.getLeft(),
1108 bottom - mChallengeView.getHeight(), mChallengeView.getRight(), bottom);
1109
Adam Powellfa668cc2012-10-31 16:09:21 -07001110 mChallengeView.setAlpha(getChallengeAlpha());
Adam Powell0b1b5522012-10-25 13:39:30 -07001111 if (mScrollListener != null) {
1112 mScrollListener.onScrollPositionChanged(offset, mChallengeView.getTop());
Jim Millerd6523da2012-10-21 16:47:02 -07001113 }
1114 postInvalidateOnAnimation();
1115 return true;
1116 }
1117
Daniel Sandlerb42b8422012-10-26 12:50:27 -04001118 /**
1119 * The bottom edge of this SlidingChallengeLayout's coordinate system; will coincide with
1120 * the bottom edge of mChallengeView when the challenge is fully opened.
1121 */
1122 private int getLayoutBottom() {
1123 final int bottomMargin = (mChallengeView == null)
1124 ? 0
1125 : ((LayoutParams) mChallengeView.getLayoutParams()).bottomMargin;
Adam Cohen8a7785c2012-10-29 22:01:33 -07001126 final int layoutBottom = getMeasuredHeight() - getPaddingBottom() - bottomMargin;
Daniel Sandlerb42b8422012-10-26 12:50:27 -04001127 return layoutBottom;
1128 }
1129
1130 /**
1131 * The bottom edge of mChallengeView; essentially, where the sliding challenge 'is'.
1132 */
Jim Millerd6523da2012-10-21 16:47:02 -07001133 private int getChallengeBottom() {
1134 if (mChallengeView == null) return 0;
1135
1136 return mChallengeView.getBottom();
1137 }
1138
Daniel Sandlerb42b8422012-10-26 12:50:27 -04001139 /**
Jim Millerd6523da2012-10-21 16:47:02 -07001140 * Show or hide the challenge view, animating it if necessary.
1141 * @param show true to show, false to hide
1142 */
1143 public void showChallenge(boolean show) {
1144 showChallenge(show, 0);
Adam Powell995270f2012-10-26 16:10:43 -07001145 if (!show) {
1146 // Block any drags in progress so that callers can use this to disable dragging
1147 // for other touch interactions.
1148 mBlockDrag = true;
1149 }
Jim Millerd6523da2012-10-21 16:47:02 -07001150 }
1151
1152 private void showChallenge(int velocity) {
1153 boolean show = false;
1154 if (Math.abs(velocity) > mMinVelocity) {
1155 show = velocity < 0;
1156 } else {
1157 show = mChallengeOffset >= 0.5f;
1158 }
1159 showChallenge(show, velocity);
1160 }
1161
1162 private void showChallenge(boolean show, int velocity) {
1163 if (mChallengeView == null) {
1164 setChallengeShowing(false);
1165 return;
1166 }
1167
1168 if (mHasLayout) {
Chris Wrenda8f6222012-11-06 17:36:18 -05001169 mChallengeShowingTargetState = show;
Adam Powell67625e22012-10-26 15:12:30 -07001170 final int layoutBottom = getLayoutBottom();
Jim Millerd6523da2012-10-21 16:47:02 -07001171 animateChallengeTo(show ? layoutBottom :
Adam Powell67625e22012-10-26 15:12:30 -07001172 layoutBottom + mChallengeView.getHeight() - mChallengeBottomBound, velocity);
Jim Millerd6523da2012-10-21 16:47:02 -07001173 }
1174 }
1175
Jim Miller19a52672012-10-23 19:52:04 -07001176 @Override
Jim Millerd6523da2012-10-21 16:47:02 -07001177 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
1178 return new LayoutParams(getContext(), attrs);
1179 }
1180
1181 @Override
1182 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
1183 return p instanceof LayoutParams ? new LayoutParams((LayoutParams) p) :
1184 p instanceof MarginLayoutParams ? new LayoutParams((MarginLayoutParams) p) :
1185 new LayoutParams(p);
1186 }
1187
1188 @Override
1189 protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
1190 return new LayoutParams();
1191 }
1192
1193 @Override
1194 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
1195 return p instanceof LayoutParams;
1196 }
1197
1198 public static class LayoutParams extends MarginLayoutParams {
Adam Powelleee20932012-10-24 16:26:56 -07001199 public int childType = CHILD_TYPE_NONE;
1200 public static final int CHILD_TYPE_NONE = 0;
1201 public static final int CHILD_TYPE_CHALLENGE = 2;
1202 public static final int CHILD_TYPE_SCRIM = 4;
Adam Powellcdf8b482012-10-30 20:56:45 -07001203 public static final int CHILD_TYPE_WIDGETS = 5;
Svetoslav Ganov74bdd212012-10-31 18:19:33 -07001204 public static final int CHILD_TYPE_EXPAND_CHALLENGE_HANDLE = 6;
Jim Millerd6523da2012-10-21 16:47:02 -07001205
Adam Powell97997142012-11-06 21:32:42 -08001206 public int maxHeight;
1207
Jim Millerd6523da2012-10-21 16:47:02 -07001208 public LayoutParams() {
1209 this(MATCH_PARENT, WRAP_CONTENT);
1210 }
1211
1212 public LayoutParams(int width, int height) {
1213 super(width, height);
1214 }
1215
1216 public LayoutParams(android.view.ViewGroup.LayoutParams source) {
1217 super(source);
1218 }
1219
1220 public LayoutParams(MarginLayoutParams source) {
1221 super(source);
1222 }
1223
1224 public LayoutParams(LayoutParams source) {
1225 super(source);
1226
Adam Powelleee20932012-10-24 16:26:56 -07001227 childType = source.childType;
Jim Millerd6523da2012-10-21 16:47:02 -07001228 }
1229
1230 public LayoutParams(Context c, AttributeSet attrs) {
1231 super(c, attrs);
1232
1233 final TypedArray a = c.obtainStyledAttributes(attrs,
1234 R.styleable.SlidingChallengeLayout_Layout);
Adam Powelleee20932012-10-24 16:26:56 -07001235 childType = a.getInt(R.styleable.SlidingChallengeLayout_Layout_layout_childType,
1236 CHILD_TYPE_NONE);
Adam Powell97997142012-11-06 21:32:42 -08001237 maxHeight = a.getDimensionPixelSize(
1238 R.styleable.SlidingChallengeLayout_Layout_layout_maxHeight, 0);
Jim Millerd6523da2012-10-21 16:47:02 -07001239 a.recycle();
1240 }
1241 }
1242}