blob: 7cf995c4f3f279c5882dcce8fc1876f344467ec5 [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
17package com.android.internal.policy.impl.keyguard;
18
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;
23import android.content.res.TypedArray;
24import android.graphics.Canvas;
Daniel Sandler1bfcc822012-10-26 11:25:49 -040025import android.graphics.Paint;
Adam Powellcdf8b482012-10-30 20:56:45 -070026import android.graphics.Rect;
Jim Millerd6523da2012-10-21 16:47:02 -070027import android.graphics.drawable.Drawable;
28import android.util.AttributeSet;
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;
37import android.view.animation.Interpolator;
38import android.widget.Scroller;
39
40import com.android.internal.R;
41
42/**
43 * This layout handles interaction with the sliding security challenge views
44 * that overlay/resize other keyguard contents.
45 */
Jim Miller19a52672012-10-23 19:52:04 -070046public class SlidingChallengeLayout extends ViewGroup implements ChallengeLayout {
Jim Millerd6523da2012-10-21 16:47:02 -070047 private static final String TAG = "SlidingChallengeLayout";
Daniel Sandler1bfcc822012-10-26 11:25:49 -040048 private static final boolean DEBUG = false;
Jim Millerd6523da2012-10-21 16:47:02 -070049
Daniel Sandlerc606bf12012-10-28 00:12:21 -040050 // 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.
Adam Powellcfc30862012-10-29 18:21:31 -070053 private static final int DRAG_HANDLE_CLOSED_ABOVE = 64; // dp
54 private static final int DRAG_HANDLE_CLOSED_BELOW = 0; // dp
Daniel Sandlerc606bf12012-10-28 00:12:21 -040055 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 boolean OPEN_ON_CLICK = true;
59
60 private static final int HANDLE_ANIMATE_DURATION = 200; // ms
61
Jim Millerd6523da2012-10-21 16:47:02 -070062 // Drawn to show the drag handle in closed state; crossfades to the challenge view
63 // when challenge is fully visible
64 private Drawable mHandleDrawable;
Adam Powell6f352092012-10-25 20:49:32 -070065 private Drawable mFrameDrawable;
66 private Drawable mDragIconDrawable;
Adam Powellcdf8b482012-10-30 20:56:45 -070067 private boolean mEdgeCaptured;
Jim Millerd6523da2012-10-21 16:47:02 -070068
69 // Initialized during measurement from child layoutparams
70 private View mChallengeView;
Adam Powelleee20932012-10-24 16:26:56 -070071 private View mScrimView;
Adam Powellcdf8b482012-10-30 20:56:45 -070072 private View mWidgetsView;
Jim Millerd6523da2012-10-21 16:47:02 -070073
74 // Range: 0 (fully hidden) to 1 (fully visible)
75 private float mChallengeOffset = 1.f;
76 private boolean mChallengeShowing = true;
Adam Powelleee20932012-10-24 16:26:56 -070077 private boolean mIsBouncing = false;
Jim Millerd6523da2012-10-21 16:47:02 -070078
79 private final Scroller mScroller;
80 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;
87
88 private static final int MAX_SETTLE_DURATION = 600; // ms
89
90 // ID of the pointer in charge of a current drag
91 private int mActivePointerId = INVALID_POINTER;
92 private static final int INVALID_POINTER = -1;
93
94 // True if the user is currently dragging the slider
95 private boolean mDragging;
96 // True if the user may not drag until a new gesture begins
97 private boolean mBlockDrag;
98
99 private VelocityTracker mVelocityTracker;
100 private int mMinVelocity;
101 private int mMaxVelocity;
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400102 private float mGestureStartX, mGestureStartY; // where did you first touch the screen?
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400103 private int mGestureStartChallengeBottom; // where was the challenge at that time?
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400104
105 private int mDragHandleClosedBelow; // handle hitrect extension into the challenge view
106 private int mDragHandleClosedAbove; // extend the handle's hitrect this far above the line
107 private int mDragHandleOpenBelow; // handle hitrect extension into the challenge view
108 private int mDragHandleOpenAbove; // extend the handle's hitrect this far above the line
109
Adam Powell0b1b5522012-10-25 13:39:30 -0700110 private int mDragHandleEdgeSlop;
Adam Powell67625e22012-10-26 15:12:30 -0700111 private int mChallengeBottomBound; // Number of pixels from the top of the challenge view
112 // that should remain on-screen
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400113
114 private int mTouchSlop;
115
Adam Powell6f352092012-10-25 20:49:32 -0700116 float mHandleAlpha;
117 float mFrameAlpha;
Adam Powellcdf8b482012-10-30 20:56:45 -0700118 float mFrameAnimationTarget = Float.MIN_VALUE;
Adam Powell6f352092012-10-25 20:49:32 -0700119 private ObjectAnimator mHandleAnimation;
120 private ObjectAnimator mFrameAnimation;
121
Adam Powellcdf8b482012-10-30 20:56:45 -0700122 private final Rect mTempRect = new Rect();
123
124 private boolean mHasGlowpad;
125
Adam Powell6f352092012-10-25 20:49:32 -0700126 static final Property<SlidingChallengeLayout, Float> HANDLE_ALPHA =
127 new FloatProperty<SlidingChallengeLayout>("handleAlpha") {
128 @Override
129 public void setValue(SlidingChallengeLayout view, float value) {
130 view.mHandleAlpha = value;
131 view.invalidate();
132 }
133
134 @Override
135 public Float get(SlidingChallengeLayout view) {
136 return view.mHandleAlpha;
137 }
138 };
139
140 static final Property<SlidingChallengeLayout, Float> FRAME_ALPHA =
141 new FloatProperty<SlidingChallengeLayout>("frameAlpha") {
142 @Override
143 public void setValue(SlidingChallengeLayout view, float value) {
144 if (view.mFrameDrawable != null) {
145 view.mFrameAlpha = value;
146 view.mFrameDrawable.setAlpha((int) (value * 0xFF));
147 view.mFrameDrawable.invalidateSelf();
148 }
149 }
150
151 @Override
152 public Float get(SlidingChallengeLayout view) {
153 return view.mFrameAlpha;
154 }
155 };
Jim Millerd6523da2012-10-21 16:47:02 -0700156
Jim Millerd6523da2012-10-21 16:47:02 -0700157 // True if at least one layout pass has happened since the view was attached.
158 private boolean mHasLayout;
159
160 private static final Interpolator sMotionInterpolator = new Interpolator() {
161 public float getInterpolation(float t) {
162 t -= 1.0f;
163 return t * t * t * t * t + 1.0f;
164 }
165 };
166
167 private static final Interpolator sHandleFadeInterpolator = new Interpolator() {
168 public float getInterpolation(float t) {
169 return t * t;
170 }
171 };
172
173 private final Runnable mEndScrollRunnable = new Runnable () {
174 public void run() {
175 completeChallengeScroll();
176 }
177 };
178
Adam Powelleee20932012-10-24 16:26:56 -0700179 private final OnClickListener mScrimClickListener = new OnClickListener() {
180 @Override
181 public void onClick(View v) {
182 hideBouncer();
183 }
184 };
185
Jim Millerd6523da2012-10-21 16:47:02 -0700186 /**
187 * Listener interface that reports changes in scroll state of the challenge area.
188 */
189 public interface OnChallengeScrolledListener {
190 /**
191 * The scroll state itself changed.
192 *
193 * <p>scrollState will be one of the following:</p>
194 *
195 * <ul>
196 * <li><code>SCROLL_STATE_IDLE</code> - The challenge area is stationary.</li>
197 * <li><code>SCROLL_STATE_DRAGGING</code> - The user is actively dragging
198 * the challenge area.</li>
199 * <li><code>SCROLL_STATE_SETTLING</code> - The challenge area is animating
200 * into place.</li>
201 * </ul>
202 *
203 * <p>Do not perform expensive operations (e.g. layout)
204 * while the scroll state is not <code>SCROLL_STATE_IDLE</code>.</p>
205 *
206 * @param scrollState The new scroll state of the challenge area.
207 */
208 public void onScrollStateChanged(int scrollState);
209
210 /**
211 * The precise position of the challenge area has changed.
212 *
213 * <p>NOTE: It is NOT safe to modify layout or call any View methods that may
214 * result in a requestLayout anywhere in your view hierarchy as a result of this call.
215 * It may be called during drawing.</p>
216 *
217 * @param scrollPosition New relative position of the challenge area.
218 * 1.f = fully visible/ready to be interacted with.
219 * 0.f = fully invisible/inaccessible to the user.
220 * @param challengeTop Position of the top edge of the challenge view in px in the
221 * SlidingChallengeLayout's coordinate system.
222 */
223 public void onScrollPositionChanged(float scrollPosition, int challengeTop);
224 }
225
226 public SlidingChallengeLayout(Context context) {
227 this(context, null);
228 }
229
230 public SlidingChallengeLayout(Context context, AttributeSet attrs) {
231 this(context, attrs, 0);
232 }
233
234 public SlidingChallengeLayout(Context context, AttributeSet attrs, int defStyle) {
235 super(context, attrs, defStyle);
236
237 final TypedArray a = context.obtainStyledAttributes(attrs,
238 R.styleable.SlidingChallengeLayout, defStyle, 0);
Adam Powell6f352092012-10-25 20:49:32 -0700239 setDragDrawables(a.getDrawable(R.styleable.SlidingChallengeLayout_dragHandle),
240 a.getDrawable(R.styleable.SlidingChallengeLayout_dragIcon));
Jim Millerd6523da2012-10-21 16:47:02 -0700241
242 a.recycle();
243
244 mScroller = new Scroller(context, sMotionInterpolator);
245
246 final ViewConfiguration vc = ViewConfiguration.get(context);
247 mMinVelocity = vc.getScaledMinimumFlingVelocity();
248 mMaxVelocity = vc.getScaledMaximumFlingVelocity();
249
Adam Powell0b1b5522012-10-25 13:39:30 -0700250 mDragHandleEdgeSlop = getResources().getDimensionPixelSize(
251 R.dimen.kg_edge_swipe_region_size);
252
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400253 mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
254
Adam Powell6f352092012-10-25 20:49:32 -0700255 final float density = getResources().getDisplayMetrics().density;
Daniel Sandler1bfcc822012-10-26 11:25:49 -0400256
257 // top half of the lock icon, plus another 25% to be sure
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400258 mDragHandleClosedAbove = (int) (DRAG_HANDLE_CLOSED_ABOVE * density + 0.5f);
259 mDragHandleClosedBelow = (int) (DRAG_HANDLE_CLOSED_BELOW * density + 0.5f);
260 mDragHandleOpenAbove = (int) (DRAG_HANDLE_OPEN_ABOVE * density + 0.5f);
261 mDragHandleOpenBelow = (int) (DRAG_HANDLE_OPEN_BELOW * density + 0.5f);
262
263 // how much space to account for in the handle when closed
Adam Powellcfc30862012-10-29 18:21:31 -0700264 mChallengeBottomBound = mDragHandleClosedBelow;
Daniel Sandler1bfcc822012-10-26 11:25:49 -0400265
Adam Powellcfc30862012-10-29 18:21:31 -0700266 setWillNotDraw(false);
267 }
268
269 public void setDragDrawables(Drawable handle, Drawable icon) {
Adam Powell6f352092012-10-25 20:49:32 -0700270 mHandleDrawable = handle;
271 mDragIconDrawable = icon;
272 }
273
274 public void setDragIconDrawable(Drawable d) {
275 mDragIconDrawable = d;
276 }
277
278 public void showHandle(boolean visible) {
279 if (visible) {
280 if (mHandleAnimation != null) {
281 mHandleAnimation.cancel();
282 mHandleAnimation = null;
283 }
284 mHandleAlpha = 1.f;
285 invalidate();
286 } else {
287 animateHandle(false);
Jim Millerd6523da2012-10-21 16:47:02 -0700288 }
Adam Powell6f352092012-10-25 20:49:32 -0700289 }
290
291 void animateHandle(boolean visible) {
292 if (mHandleAnimation != null) {
293 mHandleAnimation.cancel();
Adam Powell67625e22012-10-26 15:12:30 -0700294 mHandleAnimation = null;
Jim Millerd6523da2012-10-21 16:47:02 -0700295 }
Adam Powell67625e22012-10-26 15:12:30 -0700296 final float targetAlpha = visible ? 1.f : 0.f;
297 if (targetAlpha == mHandleAlpha) {
298 return;
299 }
300 mHandleAnimation = ObjectAnimator.ofFloat(this, HANDLE_ALPHA, targetAlpha);
Adam Powell6f352092012-10-25 20:49:32 -0700301 mHandleAnimation.setInterpolator(sHandleFadeInterpolator);
302 mHandleAnimation.setDuration(HANDLE_ANIMATE_DURATION);
303 mHandleAnimation.start();
304 }
305
Adam Powellcdf8b482012-10-30 20:56:45 -0700306 void animateFrame(final boolean visible, final boolean full) {
Adam Powell6f352092012-10-25 20:49:32 -0700307 if (mFrameDrawable == null) return;
308
Adam Powell67625e22012-10-26 15:12:30 -0700309 final float targetAlpha = visible ? (full ? 1.f : 0.5f) : 0.f;
Adam Powellcdf8b482012-10-30 20:56:45 -0700310 if (mFrameAnimation != null && targetAlpha != mFrameAnimationTarget) {
311 mFrameAnimation.cancel();
312 mFrameAnimationTarget = Float.MIN_VALUE;
313 }
314 if (targetAlpha == mFrameAlpha || targetAlpha == mFrameAnimationTarget) {
Adam Powell67625e22012-10-26 15:12:30 -0700315 return;
316 }
Adam Powellcdf8b482012-10-30 20:56:45 -0700317 mFrameAnimationTarget = targetAlpha;
Adam Powell67625e22012-10-26 15:12:30 -0700318
319 mFrameAnimation = ObjectAnimator.ofFloat(this, FRAME_ALPHA, targetAlpha);
Adam Powell6f352092012-10-25 20:49:32 -0700320 mFrameAnimation.setInterpolator(sHandleFadeInterpolator);
321 mFrameAnimation.setDuration(HANDLE_ANIMATE_DURATION);
Adam Powellcdf8b482012-10-30 20:56:45 -0700322 mFrameAnimation.addListener(new AnimatorListenerAdapter() {
323 @Override
324 public void onAnimationEnd(Animator animation) {
325 mFrameAnimationTarget = Float.MIN_VALUE;
326
327 if (!visible && full && mChallengeView != null) {
328 // Mess with padding/margin to remove insets on the bouncer frame.
329 mChallengeView.setPadding(0, 0, 0, 0);
330 LayoutParams lp = (LayoutParams) mChallengeView.getLayoutParams();
331 lp.leftMargin = lp.rightMargin = getChallengeMargin(true);
332 mChallengeView.setLayoutParams(lp);
333 }
334 mFrameAnimation = null;
335 }
336
337 @Override
338 public void onAnimationCancel(Animator animation) {
339 mFrameAnimationTarget = Float.MIN_VALUE;
340 mFrameAnimation = null;
341 }
342 });
Adam Powell6f352092012-10-25 20:49:32 -0700343 mFrameAnimation.start();
Jim Millerd6523da2012-10-21 16:47:02 -0700344 }
345
346 private void sendInitialListenerUpdates() {
Adam Powell0b1b5522012-10-25 13:39:30 -0700347 if (mScrollListener != null) {
Jim Millerd6523da2012-10-21 16:47:02 -0700348 int challengeTop = mChallengeView != null ? mChallengeView.getTop() : 0;
Adam Powell0b1b5522012-10-25 13:39:30 -0700349 mScrollListener.onScrollPositionChanged(mChallengeOffset, challengeTop);
350 mScrollListener.onScrollStateChanged(mScrollState);
Jim Millerd6523da2012-10-21 16:47:02 -0700351 }
352 }
353
354 public void setOnChallengeScrolledListener(OnChallengeScrolledListener listener) {
Adam Powell0b1b5522012-10-25 13:39:30 -0700355 mScrollListener = listener;
Jim Millerd6523da2012-10-21 16:47:02 -0700356 if (mHasLayout) {
357 sendInitialListenerUpdates();
358 }
359 }
360
Adam Powell0b1b5522012-10-25 13:39:30 -0700361 public void setOnBouncerStateChangedListener(OnBouncerStateChangedListener listener) {
362 mBouncerListener = listener;
363 }
364
Jim Millerd6523da2012-10-21 16:47:02 -0700365 @Override
366 public void onAttachedToWindow() {
367 super.onAttachedToWindow();
368
369 mHasLayout = false;
370 }
371
372 @Override
373 public void onDetachedFromWindow() {
374 super.onDetachedFromWindow();
375
376 removeCallbacks(mEndScrollRunnable);
377 mHasLayout = false;
378 }
379
Adam Powell0b1b5522012-10-25 13:39:30 -0700380 @Override
381 public void requestChildFocus(View child, View focused) {
382 if (mIsBouncing && child != mChallengeView) {
383 // Clear out of the bouncer if the user tries to move focus outside of
384 // the security challenge view.
385 hideBouncer();
386 }
387 super.requestChildFocus(child, focused);
388 }
389
Jim Millerd6523da2012-10-21 16:47:02 -0700390 // We want the duration of the page snap animation to be influenced by the distance that
391 // the screen has to travel, however, we don't want this duration to be effected in a
392 // purely linear fashion. Instead, we use this method to moderate the effect that the distance
393 // of travel has on the overall snap duration.
394 float distanceInfluenceForSnapDuration(float f) {
395 f -= 0.5f; // center the values about 0.
396 f *= 0.3f * Math.PI / 2.0f;
397 return (float) Math.sin(f);
398 }
399
400 void setScrollState(int state) {
401 if (mScrollState != state) {
402 mScrollState = state;
403
Adam Powell6f352092012-10-25 20:49:32 -0700404 animateHandle(state == SCROLL_STATE_IDLE && !mChallengeShowing);
Adam Powellcdf8b482012-10-30 20:56:45 -0700405 if (!mIsBouncing) {
406 animateFrame(false, false);
407 }
Adam Powell0b1b5522012-10-25 13:39:30 -0700408 if (mScrollListener != null) {
409 mScrollListener.onScrollStateChanged(state);
Jim Millerd6523da2012-10-21 16:47:02 -0700410 }
411 }
412 }
413
414 void completeChallengeScroll() {
415 setChallengeShowing(mChallengeOffset != 0);
416 setScrollState(SCROLL_STATE_IDLE);
Adam Powellcdf8b482012-10-30 20:56:45 -0700417 mChallengeView.setLayerType(LAYER_TYPE_NONE, null);
Jim Millerd6523da2012-10-21 16:47:02 -0700418 }
419
Adam Powelleee20932012-10-24 16:26:56 -0700420 void setScrimView(View scrim) {
421 if (mScrimView != null) {
422 mScrimView.setOnClickListener(null);
423 }
424 mScrimView = scrim;
425 mScrimView.setVisibility(mIsBouncing ? VISIBLE : GONE);
426 mScrimView.setFocusable(true);
427 mScrimView.setOnClickListener(mScrimClickListener);
428 }
429
Jim Millerd6523da2012-10-21 16:47:02 -0700430 /**
431 * Animate the bottom edge of the challenge view to the given position.
432 *
433 * @param y desired final position for the bottom edge of the challenge view in px
434 * @param velocity velocity in
435 */
436 void animateChallengeTo(int y, int velocity) {
437 if (mChallengeView == null) {
438 // Nothing to do.
439 return;
440 }
Adam Powell67625e22012-10-26 15:12:30 -0700441 final int sy = mChallengeView.getBottom();
442 final int dy = y - sy;
Jim Millerd6523da2012-10-21 16:47:02 -0700443 if (dy == 0) {
444 completeChallengeScroll();
445 return;
446 }
447
448 setScrollState(SCROLL_STATE_SETTLING);
449
450 final int childHeight = mChallengeView.getHeight();
451 final int halfHeight = childHeight / 2;
452 final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / childHeight);
453 final float distance = halfHeight + halfHeight *
454 distanceInfluenceForSnapDuration(distanceRatio);
455
456 int duration = 0;
457 velocity = Math.abs(velocity);
458 if (velocity > 0) {
459 duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
460 } else {
461 final float childDelta = (float) Math.abs(dy) / childHeight;
462 duration = (int) ((childDelta + 1) * 100);
463 }
464 duration = Math.min(duration, MAX_SETTLE_DURATION);
465
466 mScroller.startScroll(0, sy, 0, dy, duration);
467 postInvalidateOnAnimation();
468 }
469
470 private void setChallengeShowing(boolean showChallenge) {
Adam Powell67625e22012-10-26 15:12:30 -0700471 mChallengeShowing = showChallenge;
Jim Millerd6523da2012-10-21 16:47:02 -0700472 }
473
474 /**
475 * @return true if the challenge is at all visible.
476 */
477 public boolean isChallengeShowing() {
478 return mChallengeShowing;
479 }
480
481 @Override
Jim Miller19a52672012-10-23 19:52:04 -0700482 public boolean isChallengeOverlapping() {
483 return mChallengeShowing;
484 }
485
486 @Override
Adam Powelleee20932012-10-24 16:26:56 -0700487 public boolean isBouncing() {
488 return mIsBouncing;
489 }
490
491 @Override
492 public void showBouncer() {
493 if (mIsBouncing) return;
494 showChallenge(true);
495 mIsBouncing = true;
496 if (mScrimView != null) {
Adam Powell6f352092012-10-25 20:49:32 -0700497 mScrimView.setVisibility(VISIBLE);
Adam Powelleee20932012-10-24 16:26:56 -0700498 }
Adam Powellcdf8b482012-10-30 20:56:45 -0700499
500 // Mess with padding/margin to inset the bouncer frame.
501 // We have more space available to us otherwise.
502 if (mChallengeView != null) {
503 if (mFrameDrawable == null || !mFrameDrawable.getPadding(mTempRect)) {
504 mTempRect.set(0, 0, 0, 0);
505 }
506 mChallengeView.setPadding(mTempRect.left, mTempRect.top, mTempRect.right,
507 mTempRect.bottom);
508 final LayoutParams lp = (LayoutParams) mChallengeView.getLayoutParams();
509 lp.leftMargin = lp.rightMargin = getChallengeMargin(false);
510 mChallengeView.setLayoutParams(lp);
511 }
512
Adam Powell6f352092012-10-25 20:49:32 -0700513 animateFrame(true, true);
Adam Powellcdf8b482012-10-30 20:56:45 -0700514
Adam Powell0b1b5522012-10-25 13:39:30 -0700515 if (mBouncerListener != null) {
516 mBouncerListener.onBouncerStateChanged(true);
517 }
Adam Powelleee20932012-10-24 16:26:56 -0700518 }
519
520 @Override
521 public void hideBouncer() {
522 if (!mIsBouncing) return;
Adam Powellcdf8b482012-10-30 20:56:45 -0700523 showChallenge(false);
Adam Powelleee20932012-10-24 16:26:56 -0700524 mIsBouncing = false;
525 if (mScrimView != null) {
526 mScrimView.setVisibility(GONE);
527 }
Adam Powellcdf8b482012-10-30 20:56:45 -0700528 animateFrame(false, true);
Adam Powell0b1b5522012-10-25 13:39:30 -0700529 if (mBouncerListener != null) {
530 mBouncerListener.onBouncerStateChanged(false);
531 }
Adam Powelleee20932012-10-24 16:26:56 -0700532 }
533
Adam Powellcdf8b482012-10-30 20:56:45 -0700534 private int getChallengeMargin(boolean expanded) {
535 return expanded && mHasGlowpad ? 0 : mDragHandleEdgeSlop;
536 }
537
Adam Powelleee20932012-10-24 16:26:56 -0700538 @Override
Jim Millerd6523da2012-10-21 16:47:02 -0700539 public void requestDisallowInterceptTouchEvent(boolean allowIntercept) {
540 // We'll intercept whoever we feel like! ...as long as it isn't a challenge view.
541 // If there are one or more pointers in the challenge view before we take over
542 // touch events, onInterceptTouchEvent will set mBlockDrag.
543 }
544
545 @Override
546 public boolean onInterceptTouchEvent(MotionEvent ev) {
547 if (mVelocityTracker == null) {
548 mVelocityTracker = VelocityTracker.obtain();
549 }
550 mVelocityTracker.addMovement(ev);
551
552 final int action = ev.getActionMasked();
553 switch (action) {
Jim Miller022554e2012-10-22 19:03:06 -0700554 case MotionEvent.ACTION_DOWN:
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400555 mGestureStartX = ev.getX();
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400556 mGestureStartY = ev.getY();
Adam Powell995270f2012-10-26 16:10:43 -0700557 mBlockDrag = false;
Jim Miller022554e2012-10-22 19:03:06 -0700558 break;
559
Jim Millerd6523da2012-10-21 16:47:02 -0700560 case MotionEvent.ACTION_CANCEL:
561 case MotionEvent.ACTION_UP:
562 resetTouch();
563 break;
564
565 case MotionEvent.ACTION_MOVE:
566 final int count = ev.getPointerCount();
567 for (int i = 0; i < count; i++) {
568 final float x = ev.getX(i);
569 final float y = ev.getY(i);
570
Adam Powell6f352092012-10-25 20:49:32 -0700571 if (!mIsBouncing &&
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400572 (isInDragHandle(x, y) || crossedDragHandle(x, y, mGestureStartY) ||
Jim Millerd6523da2012-10-21 16:47:02 -0700573 (isInChallengeView(x, y) && mScrollState == SCROLL_STATE_SETTLING)) &&
574 mActivePointerId == INVALID_POINTER) {
575 mActivePointerId = ev.getPointerId(i);
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400576 mGestureStartX = x;
577 mGestureStartY = y;
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400578 mGestureStartChallengeBottom = getChallengeBottom();
Jim Millerd6523da2012-10-21 16:47:02 -0700579 mDragging = true;
Adam Powellcdf8b482012-10-30 20:56:45 -0700580 mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null);
Jim Millerd6523da2012-10-21 16:47:02 -0700581 } else if (isInChallengeView(x, y)) {
582 mBlockDrag = true;
583 }
584 }
585 break;
586 }
587
588 if (mBlockDrag) {
589 mActivePointerId = INVALID_POINTER;
590 mDragging = false;
591 }
592
593 return mDragging;
594 }
595
596 private void resetTouch() {
597 mVelocityTracker.recycle();
598 mVelocityTracker = null;
599 mActivePointerId = INVALID_POINTER;
600 mDragging = mBlockDrag = false;
601 }
602
603 @Override
604 public boolean onTouchEvent(MotionEvent ev) {
605 if (mVelocityTracker == null) {
606 mVelocityTracker = VelocityTracker.obtain();
607 }
608 mVelocityTracker.addMovement(ev);
609
610 final int action = ev.getActionMasked();
611 switch (action) {
Adam Powell995270f2012-10-26 16:10:43 -0700612 case MotionEvent.ACTION_DOWN:
613 mBlockDrag = false;
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400614 mGestureStartX = ev.getX();
Adam Powell995270f2012-10-26 16:10:43 -0700615 mGestureStartY = ev.getY();
616 break;
617
Jim Millerd6523da2012-10-21 16:47:02 -0700618 case MotionEvent.ACTION_CANCEL:
619 if (mDragging) {
620 showChallenge(0);
621 }
622 resetTouch();
623 break;
624
625 case MotionEvent.ACTION_POINTER_UP:
626 if (mActivePointerId != ev.getPointerId(ev.getActionIndex())) {
627 break;
628 }
629 case MotionEvent.ACTION_UP:
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400630 if (OPEN_ON_CLICK
631 && isInDragHandle(mGestureStartX, mGestureStartY)
632 && Math.abs(ev.getX() - mGestureStartX) <= mTouchSlop
633 && Math.abs(ev.getY() - mGestureStartY) <= mTouchSlop) {
634 showChallenge(true);
635 } else if (mDragging) {
Jim Millerd6523da2012-10-21 16:47:02 -0700636 mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
637 showChallenge((int) mVelocityTracker.getYVelocity(mActivePointerId));
638 }
639 resetTouch();
640 break;
641
642 case MotionEvent.ACTION_MOVE:
Adam Powell6f352092012-10-25 20:49:32 -0700643 if (!mDragging && !mBlockDrag && !mIsBouncing) {
Jim Millerd6523da2012-10-21 16:47:02 -0700644 final int count = ev.getPointerCount();
645 for (int i = 0; i < count; i++) {
646 final float x = ev.getX(i);
647 final float y = ev.getY(i);
648
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400649 if ((isInDragHandle(x, y) || crossedDragHandle(x, y, mGestureStartY) ||
Jim Millerd6523da2012-10-21 16:47:02 -0700650 (isInChallengeView(x, y) && mScrollState == SCROLL_STATE_SETTLING))
651 && mActivePointerId == INVALID_POINTER) {
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400652 mGestureStartX = x;
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400653 mGestureStartY = y;
Jim Millerd6523da2012-10-21 16:47:02 -0700654 mActivePointerId = ev.getPointerId(i);
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400655 mGestureStartChallengeBottom = getChallengeBottom();
Jim Millerd6523da2012-10-21 16:47:02 -0700656 mDragging = true;
Adam Powellcdf8b482012-10-30 20:56:45 -0700657 mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null);
Jim Millerd6523da2012-10-21 16:47:02 -0700658 break;
659 }
660 }
661 }
662 // Not an else; this can be set above.
663 if (mDragging) {
664 // No-op if already in this state, but set it here in case we arrived
665 // at this point from either intercept or the above.
666 setScrollState(SCROLL_STATE_DRAGGING);
667
668 final int index = ev.findPointerIndex(mActivePointerId);
669 if (index < 0) {
670 // Oops, bogus state. We lost some touch events somewhere.
671 // Just drop it with no velocity and let things settle.
672 resetTouch();
673 showChallenge(0);
674 return true;
675 }
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400676 final float y = ev.getY(index);
677 final float pos = Math.min(y - mGestureStartY,
Adam Powell67625e22012-10-26 15:12:30 -0700678 getLayoutBottom() - mChallengeBottomBound);
Jim Millerd6523da2012-10-21 16:47:02 -0700679
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400680 moveChallengeTo(mGestureStartChallengeBottom + (int) pos);
Jim Millerd6523da2012-10-21 16:47:02 -0700681 }
682 break;
683 }
684 return true;
685 }
686
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400687 /**
Adam Powellcdf8b482012-10-30 20:56:45 -0700688 * The lifecycle of touch events is subtle and it's very easy to do something
689 * that will cause bugs that will be nasty to track when overriding this method.
690 * Normally one should always override onInterceptTouchEvent instead.
691 *
692 * To put it another way, don't try this at home.
693 */
694 @Override
695 public boolean dispatchTouchEvent(MotionEvent ev) {
696 final int action = ev.getActionMasked();
697 boolean handled = false;
698 if (action == MotionEvent.ACTION_DOWN) {
699 // Defensive programming: if we didn't get the UP or CANCEL, reset anyway.
700 mEdgeCaptured = false;
701 }
702 if (mWidgetsView != null && !mIsBouncing && (mEdgeCaptured || isEdgeSwipeBeginEvent(ev))) {
703 // Normally we would need to do a lot of extra stuff here.
704 // We can only get away with this because we haven't padded in
705 // the widget pager or otherwise transformed it during layout.
706 // We also don't support things like splitting MotionEvents.
707
708 // We set handled to captured even if dispatch is returning false here so that
709 // we don't send a different view a busted or incomplete event stream.
710 handled = mEdgeCaptured |= mWidgetsView.dispatchTouchEvent(ev);
711 }
712
713 if (!handled && !mEdgeCaptured) {
714 handled = super.dispatchTouchEvent(ev);
715 }
716
717 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
718 mEdgeCaptured = false;
719 }
720
721 return handled;
722 }
723
724 private boolean isEdgeSwipeBeginEvent(MotionEvent ev) {
725 if (ev.getActionMasked() != MotionEvent.ACTION_DOWN) {
726 return false;
727 }
728
729 final float x = ev.getX();
730 return x < mDragHandleEdgeSlop || x >= getWidth() - mDragHandleEdgeSlop;
731 }
732
733 /**
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400734 * We only want to add additional vertical space to the drag handle when the panel is fully
735 * closed.
736 */
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400737 private int getDragHandleSizeAbove() {
738 return isChallengeShowing() ? mDragHandleOpenAbove : mDragHandleClosedAbove;
739 }
740 private int getDragHandleSizeBelow() {
741 return isChallengeShowing() ? mDragHandleOpenBelow : mDragHandleClosedBelow;
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400742 }
743
Jim Millerd6523da2012-10-21 16:47:02 -0700744 private boolean isInChallengeView(float x, float y) {
745 if (mChallengeView == null) return false;
746
747 return x >= mChallengeView.getLeft() && y >= mChallengeView.getTop() &&
748 x < mChallengeView.getRight() && y < mChallengeView.getBottom();
749 }
750
751 private boolean isInDragHandle(float x, float y) {
752 if (mChallengeView == null) return false;
753
Adam Powell0b1b5522012-10-25 13:39:30 -0700754 return x >= mDragHandleEdgeSlop &&
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400755 y >= mChallengeView.getTop() - getDragHandleSizeAbove() &&
Adam Powell0b1b5522012-10-25 13:39:30 -0700756 x < getWidth() - mDragHandleEdgeSlop &&
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400757 y < mChallengeView.getTop() + getDragHandleSizeBelow();
Jim Millerd6523da2012-10-21 16:47:02 -0700758 }
759
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400760 private boolean crossedDragHandle(float x, float y, float initialY) {
Jim Miller022554e2012-10-22 19:03:06 -0700761 final int challengeTop = mChallengeView.getTop();
Daniel Sandler1bfcc822012-10-26 11:25:49 -0400762 return x >= 0 &&
763 x < getWidth() &&
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400764 initialY < (challengeTop - getDragHandleSizeAbove()) &&
765 y > challengeTop + getDragHandleSizeBelow();
Jim Miller022554e2012-10-22 19:03:06 -0700766 }
767
Jim Millerd6523da2012-10-21 16:47:02 -0700768 @Override
769 protected void onMeasure(int widthSpec, int heightSpec) {
770 if (MeasureSpec.getMode(widthSpec) != MeasureSpec.EXACTLY ||
771 MeasureSpec.getMode(heightSpec) != MeasureSpec.EXACTLY) {
772 throw new IllegalArgumentException(
773 "SlidingChallengeLayout must be measured with an exact size");
774 }
775
776 final int width = MeasureSpec.getSize(widthSpec);
777 final int height = MeasureSpec.getSize(heightSpec);
778 setMeasuredDimension(width, height);
779
780 // Find one and only one challenge view.
781 final View oldChallengeView = mChallengeView;
782 mChallengeView = null;
783 final int count = getChildCount();
Adam Cohen8a7785c2012-10-29 22:01:33 -0700784
785 // First iteration through the children finds special children and sets any associated
786 // state.
Jim Millerd6523da2012-10-21 16:47:02 -0700787 for (int i = 0; i < count; i++) {
788 final View child = getChildAt(i);
789 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
790
Adam Powelleee20932012-10-24 16:26:56 -0700791 if (lp.childType == LayoutParams.CHILD_TYPE_CHALLENGE) {
Jim Millerd6523da2012-10-21 16:47:02 -0700792 if (mChallengeView != null) {
793 throw new IllegalStateException(
794 "There may only be one child with layout_isChallenge=\"true\"");
795 }
796 mChallengeView = child;
797 if (mChallengeView != oldChallengeView) {
798 mChallengeView.setVisibility(mChallengeShowing ? VISIBLE : INVISIBLE);
799 }
Adam Powell6f352092012-10-25 20:49:32 -0700800 // We're going to play silly games with the frame's background drawable later.
801 mFrameDrawable = mChallengeView.getBackground();
Adam Powellcdf8b482012-10-30 20:56:45 -0700802
803 if (!mHasLayout) {
804 // Set up the margin correctly based on our content for the first run.
805 mHasGlowpad = child.findViewById(R.id.keyguard_selector_view) != null;
806 lp.leftMargin = lp.rightMargin = getChallengeMargin(true);
807 }
Adam Powelleee20932012-10-24 16:26:56 -0700808 } else if (lp.childType == LayoutParams.CHILD_TYPE_SCRIM) {
809 setScrimView(child);
Adam Powellcdf8b482012-10-30 20:56:45 -0700810 } else if (lp.childType == LayoutParams.CHILD_TYPE_WIDGETS) {
811 mWidgetsView = child;
Jim Millerd6523da2012-10-21 16:47:02 -0700812 }
Adam Cohen5be14de2012-10-30 11:19:48 -0700813
Jim Millerd6523da2012-10-21 16:47:02 -0700814 if (child.getVisibility() == GONE) continue;
Adam Cohen8a7785c2012-10-29 22:01:33 -0700815 }
Jim Millerd6523da2012-10-21 16:47:02 -0700816
Adam Cohen5be14de2012-10-30 11:19:48 -0700817 // We want to measure the challenge view first, since the KeyguardWidgetPager
818 // needs to do things its measure pass that are dependent on the challenge view
819 // having been measured.
Adam Cohen8a7785c2012-10-29 22:01:33 -0700820 if (mChallengeView != null) {
821 measureChildWithMargins(mChallengeView, widthSpec, 0, heightSpec, 0);
822 }
823
824 // Measure the rest of the children
825 for (int i = 0; i < count; i++) {
826 final View child = getChildAt(i);
827 // Don't measure the challenge view twice!
828 if (child != mChallengeView) {
829 measureChildWithMargins(child, widthSpec, 0, heightSpec, 0);
830 }
Jim Millerd6523da2012-10-21 16:47:02 -0700831 }
832 }
833
834 @Override
835 protected void onLayout(boolean changed, int l, int t, int r, int b) {
836 final int paddingLeft = getPaddingLeft();
837 final int paddingTop = getPaddingTop();
838 final int paddingRight = getPaddingRight();
839 final int paddingBottom = getPaddingBottom();
840 final int width = r - l;
841 final int height = b - t;
842
843 final int count = getChildCount();
844 for (int i = 0; i < count; i++) {
845 final View child = getChildAt(i);
846
847 if (child.getVisibility() == GONE) continue;
848
849 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
850
Adam Powelleee20932012-10-24 16:26:56 -0700851 if (lp.childType == LayoutParams.CHILD_TYPE_CHALLENGE) {
Jim Millerd6523da2012-10-21 16:47:02 -0700852 // Challenge views pin to the bottom, offset by a portion of their height,
853 // and center horizontally.
854 final int center = (paddingLeft + width - paddingRight) / 2;
855 final int childWidth = child.getMeasuredWidth();
856 final int childHeight = child.getMeasuredHeight();
857 final int left = center - childWidth / 2;
858 final int layoutBottom = height - paddingBottom - lp.bottomMargin;
859 // We use the top of the challenge view to position the handle, so
860 // we never want less than the handle size showing at the bottom.
Adam Powell67625e22012-10-26 15:12:30 -0700861 final int bottom = layoutBottom + (int) ((childHeight - mChallengeBottomBound)
Jim Millerd6523da2012-10-21 16:47:02 -0700862 * (1 - mChallengeOffset));
Adam Powell67625e22012-10-26 15:12:30 -0700863 child.setAlpha(mChallengeOffset / 2 + 0.5f);
Jim Millerd6523da2012-10-21 16:47:02 -0700864 child.layout(left, bottom - childHeight, left + childWidth, bottom);
865 } else {
866 // Non-challenge views lay out from the upper left, layered.
867 child.layout(paddingLeft + lp.leftMargin,
868 paddingTop + lp.topMargin,
869 paddingLeft + child.getMeasuredWidth(),
870 paddingTop + child.getMeasuredHeight());
871 }
872 }
873
874 if (!mHasLayout) {
Adam Powell67625e22012-10-26 15:12:30 -0700875 if (mFrameDrawable != null) {
876 mFrameDrawable.setAlpha(0);
877 }
Adam Powell6f352092012-10-25 20:49:32 -0700878 mHasLayout = true;
Jim Millerd6523da2012-10-21 16:47:02 -0700879 }
Jim Millerd6523da2012-10-21 16:47:02 -0700880 }
881
882 public void computeScroll() {
883 super.computeScroll();
884
885 if (!mScroller.isFinished()) {
886 if (mChallengeView == null) {
887 // Can't scroll if the view is missing.
888 Log.e(TAG, "Challenge view missing in computeScroll");
889 mScroller.abortAnimation();
890 return;
891 }
892
893 mScroller.computeScrollOffset();
894 moveChallengeTo(mScroller.getCurrY());
895
896 if (mScroller.isFinished()) {
897 post(mEndScrollRunnable);
898 }
899 }
900 }
901
902 @Override
903 public void draw(Canvas c) {
904 super.draw(c);
Daniel Sandler1bfcc822012-10-26 11:25:49 -0400905
906 final Paint debugPaint;
907 if (DEBUG) {
908 debugPaint = new Paint();
909 debugPaint.setColor(0x40FF00CC);
910 // show the isInDragHandle() rect
911 c.drawRect(mDragHandleEdgeSlop,
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400912 mChallengeView.getTop() - getDragHandleSizeAbove(),
Daniel Sandler1bfcc822012-10-26 11:25:49 -0400913 getWidth() - mDragHandleEdgeSlop,
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400914 mChallengeView.getTop() + getDragHandleSizeBelow(),
Daniel Sandler1bfcc822012-10-26 11:25:49 -0400915 debugPaint);
916 }
917
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400918 if (mChallengeView != null && mHandleAlpha > 0) {
Jim Millerd6523da2012-10-21 16:47:02 -0700919 final int top = mChallengeView.getTop();
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400920 final int handleHeight;
Adam Powell6f352092012-10-25 20:49:32 -0700921 final int challengeLeft = mChallengeView.getLeft();
922 final int challengeRight = mChallengeView.getRight();
Daniel Sandlerc606bf12012-10-28 00:12:21 -0400923 if (mHandleDrawable != null) {
924 handleHeight = mHandleDrawable.getIntrinsicHeight();
925 mHandleDrawable.setBounds(challengeLeft, top, challengeRight, top + handleHeight);
926 mHandleDrawable.setAlpha((int) (mHandleAlpha * 0xFF));
927 mHandleDrawable.draw(c);
928 } else {
929 handleHeight = 0;
930 }
Adam Powell6f352092012-10-25 20:49:32 -0700931
Daniel Sandler1bfcc822012-10-26 11:25:49 -0400932 if (DEBUG) {
933 // now show the actual drag handle
934 debugPaint.setStyle(Paint.Style.STROKE);
935 debugPaint.setStrokeWidth(1);
936 debugPaint.setColor(0xFF80FF00);
937 c.drawRect(challengeLeft, top, challengeRight, top + handleHeight, debugPaint);
938 }
939
Adam Powell6f352092012-10-25 20:49:32 -0700940 if (mDragIconDrawable != null) {
Adam Powellcfc30862012-10-29 18:21:31 -0700941 final int closedTop = getLayoutBottom() - mChallengeBottomBound;
Adam Powell6f352092012-10-25 20:49:32 -0700942 final int iconWidth = mDragIconDrawable.getIntrinsicWidth();
943 final int iconHeight = mDragIconDrawable.getIntrinsicHeight();
944 final int iconLeft = (challengeLeft + challengeRight - iconWidth) / 2;
Adam Powellcfc30862012-10-29 18:21:31 -0700945 final int iconTop = closedTop +
946 (mDragHandleClosedBelow - mDragHandleClosedAbove - iconHeight) / 2;
Adam Powell6f352092012-10-25 20:49:32 -0700947 mDragIconDrawable.setBounds(iconLeft, iconTop, iconLeft + iconWidth,
948 iconTop + iconHeight);
949 mDragIconDrawable.setAlpha((int) (mHandleAlpha * 0xFF));
950 mDragIconDrawable.draw(c);
Daniel Sandler1bfcc822012-10-26 11:25:49 -0400951
952 if (DEBUG) {
953 debugPaint.setColor(0xFF00FF00);
954 c.drawRect(iconLeft, iconTop, iconLeft + iconWidth,
955 iconTop + iconHeight, debugPaint);
956 }
Adam Powell6f352092012-10-25 20:49:32 -0700957 }
Jim Millerd6523da2012-10-21 16:47:02 -0700958 }
959 }
960
Adam Cohene3643132012-10-28 18:29:17 -0700961 public int getMaxChallengeTop() {
962 if (mChallengeView == null) return 0;
963
964 final int layoutBottom = getLayoutBottom();
Adam Cohen8a7785c2012-10-29 22:01:33 -0700965 final int challengeHeight = mChallengeView.getMeasuredHeight();
Adam Cohene3643132012-10-28 18:29:17 -0700966 return layoutBottom - challengeHeight;
967 }
968
Jim Millerd6523da2012-10-21 16:47:02 -0700969 /**
970 * Move the bottom edge of mChallengeView to a new position and notify the listener
971 * if it represents a change in position. Changes made through this method will
972 * be stable across layout passes. If this method is called before first layout of
973 * this SlidingChallengeLayout it will have no effect.
974 *
975 * @param bottom New bottom edge in px in this SlidingChallengeLayout's coordinate system.
976 * @return true if the challenge view was moved
977 */
978 private boolean moveChallengeTo(int bottom) {
979 if (mChallengeView == null || !mHasLayout) {
980 return false;
981 }
982
Daniel Sandlerb42b8422012-10-26 12:50:27 -0400983 final int layoutBottom = getLayoutBottom();
Jim Millerd6523da2012-10-21 16:47:02 -0700984 final int challengeHeight = mChallengeView.getHeight();
985
986 bottom = Math.max(layoutBottom,
Adam Powell67625e22012-10-26 15:12:30 -0700987 Math.min(bottom, layoutBottom + challengeHeight - mChallengeBottomBound));
Jim Millerd6523da2012-10-21 16:47:02 -0700988
Adam Powell67625e22012-10-26 15:12:30 -0700989 float offset = 1.f - (float) (bottom - layoutBottom) /
990 (challengeHeight - mChallengeBottomBound);
Jim Millerd6523da2012-10-21 16:47:02 -0700991 mChallengeOffset = offset;
992 if (offset > 0 && !mChallengeShowing) {
993 setChallengeShowing(true);
994 }
995
996 mChallengeView.layout(mChallengeView.getLeft(),
997 bottom - mChallengeView.getHeight(), mChallengeView.getRight(), bottom);
998
Adam Powell67625e22012-10-26 15:12:30 -0700999 mChallengeView.setAlpha(offset / 2 + 0.5f);
Adam Powell0b1b5522012-10-25 13:39:30 -07001000 if (mScrollListener != null) {
1001 mScrollListener.onScrollPositionChanged(offset, mChallengeView.getTop());
Jim Millerd6523da2012-10-21 16:47:02 -07001002 }
1003 postInvalidateOnAnimation();
1004 return true;
1005 }
1006
Daniel Sandlerb42b8422012-10-26 12:50:27 -04001007 /**
1008 * The bottom edge of this SlidingChallengeLayout's coordinate system; will coincide with
1009 * the bottom edge of mChallengeView when the challenge is fully opened.
1010 */
1011 private int getLayoutBottom() {
1012 final int bottomMargin = (mChallengeView == null)
1013 ? 0
1014 : ((LayoutParams) mChallengeView.getLayoutParams()).bottomMargin;
Adam Cohen8a7785c2012-10-29 22:01:33 -07001015 final int layoutBottom = getMeasuredHeight() - getPaddingBottom() - bottomMargin;
Daniel Sandlerb42b8422012-10-26 12:50:27 -04001016 return layoutBottom;
1017 }
1018
1019 /**
1020 * The bottom edge of mChallengeView; essentially, where the sliding challenge 'is'.
1021 */
Jim Millerd6523da2012-10-21 16:47:02 -07001022 private int getChallengeBottom() {
1023 if (mChallengeView == null) return 0;
1024
1025 return mChallengeView.getBottom();
1026 }
1027
Daniel Sandlerb42b8422012-10-26 12:50:27 -04001028 /**
Jim Millerd6523da2012-10-21 16:47:02 -07001029 * Show or hide the challenge view, animating it if necessary.
1030 * @param show true to show, false to hide
1031 */
1032 public void showChallenge(boolean show) {
1033 showChallenge(show, 0);
Adam Powell995270f2012-10-26 16:10:43 -07001034 if (!show) {
1035 // Block any drags in progress so that callers can use this to disable dragging
1036 // for other touch interactions.
1037 mBlockDrag = true;
1038 }
Jim Millerd6523da2012-10-21 16:47:02 -07001039 }
1040
1041 private void showChallenge(int velocity) {
1042 boolean show = false;
1043 if (Math.abs(velocity) > mMinVelocity) {
1044 show = velocity < 0;
1045 } else {
1046 show = mChallengeOffset >= 0.5f;
1047 }
1048 showChallenge(show, velocity);
1049 }
1050
1051 private void showChallenge(boolean show, int velocity) {
1052 if (mChallengeView == null) {
1053 setChallengeShowing(false);
1054 return;
1055 }
1056
1057 if (mHasLayout) {
Adam Powell67625e22012-10-26 15:12:30 -07001058 final int layoutBottom = getLayoutBottom();
Jim Millerd6523da2012-10-21 16:47:02 -07001059 animateChallengeTo(show ? layoutBottom :
Adam Powell67625e22012-10-26 15:12:30 -07001060 layoutBottom + mChallengeView.getHeight() - mChallengeBottomBound, velocity);
Jim Millerd6523da2012-10-21 16:47:02 -07001061 }
1062 }
1063
Jim Miller19a52672012-10-23 19:52:04 -07001064 @Override
Jim Millerd6523da2012-10-21 16:47:02 -07001065 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
1066 return new LayoutParams(getContext(), attrs);
1067 }
1068
1069 @Override
1070 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
1071 return p instanceof LayoutParams ? new LayoutParams((LayoutParams) p) :
1072 p instanceof MarginLayoutParams ? new LayoutParams((MarginLayoutParams) p) :
1073 new LayoutParams(p);
1074 }
1075
1076 @Override
1077 protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
1078 return new LayoutParams();
1079 }
1080
1081 @Override
1082 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
1083 return p instanceof LayoutParams;
1084 }
1085
1086 public static class LayoutParams extends MarginLayoutParams {
Adam Powelleee20932012-10-24 16:26:56 -07001087 public int childType = CHILD_TYPE_NONE;
1088 public static final int CHILD_TYPE_NONE = 0;
1089 public static final int CHILD_TYPE_CHALLENGE = 2;
1090 public static final int CHILD_TYPE_SCRIM = 4;
Adam Powellcdf8b482012-10-30 20:56:45 -07001091 public static final int CHILD_TYPE_WIDGETS = 5;
Jim Millerd6523da2012-10-21 16:47:02 -07001092
1093 public LayoutParams() {
1094 this(MATCH_PARENT, WRAP_CONTENT);
1095 }
1096
1097 public LayoutParams(int width, int height) {
1098 super(width, height);
1099 }
1100
1101 public LayoutParams(android.view.ViewGroup.LayoutParams source) {
1102 super(source);
1103 }
1104
1105 public LayoutParams(MarginLayoutParams source) {
1106 super(source);
1107 }
1108
1109 public LayoutParams(LayoutParams source) {
1110 super(source);
1111
Adam Powelleee20932012-10-24 16:26:56 -07001112 childType = source.childType;
Jim Millerd6523da2012-10-21 16:47:02 -07001113 }
1114
1115 public LayoutParams(Context c, AttributeSet attrs) {
1116 super(c, attrs);
1117
1118 final TypedArray a = c.obtainStyledAttributes(attrs,
1119 R.styleable.SlidingChallengeLayout_Layout);
Adam Powelleee20932012-10-24 16:26:56 -07001120 childType = a.getInt(R.styleable.SlidingChallengeLayout_Layout_layout_childType,
1121 CHILD_TYPE_NONE);
Jim Millerd6523da2012-10-21 16:47:02 -07001122 a.recycle();
1123 }
1124 }
1125}