blob: c3e8838f057bb24a0f837f17e890123c2b9595e9 [file] [log] [blame]
Adam Cohen44729e32010-07-22 16:00:07 -07001/*
2 * Copyright (C) 2010 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 android.widget;
18
19import java.util.WeakHashMap;
20
21import android.animation.PropertyAnimator;
22import android.content.Context;
Adam Cohen32a42f12010-08-11 19:34:30 -070023import android.graphics.Bitmap;
24import android.graphics.Canvas;
25import android.graphics.Matrix;
26import android.graphics.Paint;
27import android.graphics.PorterDuff;
28import android.graphics.PorterDuffXfermode;
Adam Cohen44729e32010-07-22 16:00:07 -070029import android.graphics.Rect;
30import android.util.AttributeSet;
31import android.util.Log;
32import android.view.MotionEvent;
33import android.view.VelocityTracker;
34import android.view.View;
35import android.view.ViewConfiguration;
36import android.view.ViewGroup;
Adam Cohenb04f7ad2010-08-15 13:22:42 -070037import android.view.animation.LinearInterpolator;
Adam Cohen44729e32010-07-22 16:00:07 -070038import android.widget.RemoteViews.RemoteView;
39
40@RemoteView
41/**
42 * A view that displays its children in a stack and allows users to discretely swipe
43 * through the children.
44 */
45public class StackView extends AdapterViewAnimator {
46 private final String TAG = "StackView";
47
48 /**
49 * Default animation parameters
50 */
Adam Cohen3d07af02010-08-18 17:46:23 -070051 private final int DEFAULT_ANIMATION_DURATION = 400;
Adam Cohenb04f7ad2010-08-15 13:22:42 -070052 private final int MINIMUM_ANIMATION_DURATION = 50;
Adam Cohen44729e32010-07-22 16:00:07 -070053
54 /**
55 * These specify the different gesture states
56 */
Romain Guy5b53f912010-08-16 18:24:33 -070057 private static final int GESTURE_NONE = 0;
58 private static final int GESTURE_SLIDE_UP = 1;
59 private static final int GESTURE_SLIDE_DOWN = 2;
Adam Cohen44729e32010-07-22 16:00:07 -070060
61 /**
62 * Specifies how far you need to swipe (up or down) before it
63 * will be consider a completed gesture when you lift your finger
64 */
Romain Guy5b53f912010-08-16 18:24:33 -070065 private static final float SWIPE_THRESHOLD_RATIO = 0.35f;
66 private static final float SLIDE_UP_RATIO = 0.7f;
Adam Cohen44729e32010-07-22 16:00:07 -070067
68 private final WeakHashMap<View, Float> mRotations = new WeakHashMap<View, Float>();
69 private final WeakHashMap<View, Integer>
70 mChildrenToApplyTransformsTo = new WeakHashMap<View, Integer>();
71
72 /**
73 * Sentinel value for no current active pointer.
74 * Used by {@link #mActivePointerId}.
75 */
76 private static final int INVALID_POINTER = -1;
77
78 /**
79 * These variables are all related to the current state of touch interaction
80 * with the stack
81 */
Adam Cohen44729e32010-07-22 16:00:07 -070082 private float mInitialY;
83 private float mInitialX;
84 private int mActivePointerId;
Adam Cohen44729e32010-07-22 16:00:07 -070085 private int mYVelocity = 0;
86 private int mSwipeGestureType = GESTURE_NONE;
87 private int mViewHeight;
88 private int mSwipeThreshold;
89 private int mTouchSlop;
90 private int mMaximumVelocity;
91 private VelocityTracker mVelocityTracker;
92
Adam Cohen32a42f12010-08-11 19:34:30 -070093 private ImageView mHighlight;
94 private StackSlider mStackSlider;
Adam Cohen44729e32010-07-22 16:00:07 -070095 private boolean mFirstLayoutHappened = false;
96
Adam Cohen44729e32010-07-22 16:00:07 -070097 public StackView(Context context) {
98 super(context);
99 initStackView();
100 }
101
102 public StackView(Context context, AttributeSet attrs) {
103 super(context, attrs);
104 initStackView();
105 }
106
107 private void initStackView() {
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700108 configureViewAnimator(4, 2, false);
Adam Cohen44729e32010-07-22 16:00:07 -0700109 setStaticTransformationsEnabled(true);
110 final ViewConfiguration configuration = ViewConfiguration.get(getContext());
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700111 mTouchSlop = configuration.getScaledTouchSlop();
Adam Cohen44729e32010-07-22 16:00:07 -0700112 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
113 mActivePointerId = INVALID_POINTER;
Adam Cohen32a42f12010-08-11 19:34:30 -0700114
115 mHighlight = new ImageView(getContext());
116 mHighlight.setLayoutParams(new LayoutParams(mHighlight));
117 addViewInLayout(mHighlight, -1, new LayoutParams(mHighlight));
118 mStackSlider = new StackSlider();
119
120 if (!sPaintsInitialized) {
Romain Guy5b53f912010-08-16 18:24:33 -0700121 initializePaints();
Adam Cohen32a42f12010-08-11 19:34:30 -0700122 }
Adam Cohen44729e32010-07-22 16:00:07 -0700123 }
124
125 /**
126 * Animate the views between different relative indexes within the {@link AdapterViewAnimator}
127 */
128 void animateViewForTransition(int fromIndex, int toIndex, View view) {
129 if (fromIndex == -1 && toIndex == 0) {
130 // Fade item in
131 if (view.getAlpha() == 1) {
132 view.setAlpha(0);
133 }
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700134 view.setVisibility(VISIBLE);
135
Adam Cohen44729e32010-07-22 16:00:07 -0700136 PropertyAnimator fadeIn = new PropertyAnimator(DEFAULT_ANIMATION_DURATION,
137 view, "alpha", view.getAlpha(), 1.0f);
138 fadeIn.start();
139 } else if (fromIndex == mNumActiveViews - 1 && toIndex == mNumActiveViews - 2) {
140 // Slide item in
141 view.setVisibility(VISIBLE);
Adam Cohen32a42f12010-08-11 19:34:30 -0700142
Adam Cohen44729e32010-07-22 16:00:07 -0700143 LayoutParams lp = (LayoutParams) view.getLayoutParams();
Adam Cohen3d07af02010-08-18 17:46:23 -0700144 int largestDuration =
145 Math.round(mStackSlider.getDurationForNeutralPosition()*DEFAULT_ANIMATION_DURATION);
Adam Cohen44729e32010-07-22 16:00:07 -0700146
Adam Cohen44729e32010-07-22 16:00:07 -0700147 int duration = largestDuration;
148 if (mYVelocity != 0) {
149 duration = 1000*(0 - lp.verticalOffset)/Math.abs(mYVelocity);
150 }
151
152 duration = Math.min(duration, largestDuration);
153 duration = Math.max(duration, MINIMUM_ANIMATION_DURATION);
154
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700155 StackSlider animationSlider = new StackSlider(mStackSlider);
156 PropertyAnimator slideInY = new PropertyAnimator(duration, animationSlider,
Adam Cohen32a42f12010-08-11 19:34:30 -0700157 "YProgress", mStackSlider.getYProgress(), 0);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700158 slideInY.setInterpolator(new LinearInterpolator());
Adam Cohen32a42f12010-08-11 19:34:30 -0700159 slideInY.start();
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700160 PropertyAnimator slideInX = new PropertyAnimator(duration, animationSlider,
Adam Cohen32a42f12010-08-11 19:34:30 -0700161 "XProgress", mStackSlider.getXProgress(), 0);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700162 slideInX.setInterpolator(new LinearInterpolator());
Adam Cohen32a42f12010-08-11 19:34:30 -0700163 slideInX.start();
Adam Cohen44729e32010-07-22 16:00:07 -0700164 } else if (fromIndex == mNumActiveViews - 2 && toIndex == mNumActiveViews - 1) {
165 // Slide item out
166 LayoutParams lp = (LayoutParams) view.getLayoutParams();
167
Adam Cohen3d07af02010-08-18 17:46:23 -0700168 int largestDuration = Math.round(mStackSlider.getDurationForOffscreenPosition()*
169 DEFAULT_ANIMATION_DURATION);
Adam Cohen44729e32010-07-22 16:00:07 -0700170 int duration = largestDuration;
171 if (mYVelocity != 0) {
172 duration = 1000*(lp.verticalOffset + mViewHeight)/Math.abs(mYVelocity);
173 }
174
175 duration = Math.min(duration, largestDuration);
176 duration = Math.max(duration, MINIMUM_ANIMATION_DURATION);
177
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700178 StackSlider animationSlider = new StackSlider(mStackSlider);
179 PropertyAnimator slideOutY = new PropertyAnimator(duration, animationSlider,
Adam Cohen32a42f12010-08-11 19:34:30 -0700180 "YProgress", mStackSlider.getYProgress(), 1);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700181 slideOutY.setInterpolator(new LinearInterpolator());
Adam Cohen32a42f12010-08-11 19:34:30 -0700182 slideOutY.start();
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700183 PropertyAnimator slideOutX = new PropertyAnimator(duration, animationSlider,
Adam Cohen32a42f12010-08-11 19:34:30 -0700184 "XProgress", mStackSlider.getXProgress(), 0);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700185 slideOutX.setInterpolator(new LinearInterpolator());
Adam Cohen32a42f12010-08-11 19:34:30 -0700186 slideOutX.start();
Adam Cohen44729e32010-07-22 16:00:07 -0700187 } else if (fromIndex == -1 && toIndex == mNumActiveViews - 1) {
188 // Make sure this view that is "waiting in the wings" is invisible
189 view.setAlpha(0.0f);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700190 view.setVisibility(INVISIBLE);
191 LayoutParams lp = (LayoutParams) view.getLayoutParams();
192 lp.setVerticalOffset(-mViewHeight);
Adam Cohen44729e32010-07-22 16:00:07 -0700193 } else if (toIndex == -1) {
194 // Fade item out
195 PropertyAnimator fadeOut = new PropertyAnimator(DEFAULT_ANIMATION_DURATION,
196 view, "alpha", view.getAlpha(), 0);
197 fadeOut.start();
198 }
199 }
200
201 /**
202 * Apply any necessary tranforms for the child that is being added.
203 */
204 void applyTransformForChildAtIndex(View child, int relativeIndex) {
Adam Cohen44729e32010-07-22 16:00:07 -0700205 if (!mRotations.containsKey(child)) {
Romain Guy5b53f912010-08-16 18:24:33 -0700206 float rotation = (float) (Math.random()*26 - 13);
Adam Cohen44729e32010-07-22 16:00:07 -0700207 mRotations.put(child, rotation);
Adam Cohen44729e32010-07-22 16:00:07 -0700208 }
209
210 // Child has been removed
211 if (relativeIndex == -1) {
212 if (mRotations.containsKey(child)) {
213 mRotations.remove(child);
214 }
215 if (mChildrenToApplyTransformsTo.containsKey(child)) {
216 mChildrenToApplyTransformsTo.remove(child);
217 }
218 }
219
220 // if this view is already in the layout, we need to
221 // wait until layout has finished in order to set the
222 // pivot point of the rotation (requiring getMeasuredWidth/Height())
223 mChildrenToApplyTransformsTo.put(child, relativeIndex);
224 }
225
226 @Override
227 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
228 super.onLayout(changed, left, top, right, bottom);
229
230 if (!mChildrenToApplyTransformsTo.isEmpty()) {
231 for (View child: mChildrenToApplyTransformsTo.keySet()) {
232 if (mRotations.containsKey(child)) {
233 child.setPivotX(child.getMeasuredWidth()/2);
234 child.setPivotY(child.getMeasuredHeight()/2);
235 child.setRotation(mRotations.get(child));
236 }
237 }
238 mChildrenToApplyTransformsTo.clear();
239 }
240
241 if (!mFirstLayoutHappened) {
Romain Guy5b53f912010-08-16 18:24:33 -0700242 mViewHeight = Math.round(SLIDE_UP_RATIO*getMeasuredHeight());
243 mSwipeThreshold = Math.round(SWIPE_THRESHOLD_RATIO*mViewHeight);
Adam Cohen44729e32010-07-22 16:00:07 -0700244
245 // TODO: Right now this walks all the way up the view hierarchy and disables
246 // ClipChildren and ClipToPadding. We're probably going to want to reset
247 // these flags as well.
248 setClipChildren(false);
Adam Cohen3d07af02010-08-18 17:46:23 -0700249 setClipToPadding(false);
Adam Cohen44729e32010-07-22 16:00:07 -0700250 ViewGroup view = this;
251 while (view.getParent() != null && view.getParent() instanceof ViewGroup) {
252 view = (ViewGroup) view.getParent();
253 view.setClipChildren(false);
254 view.setClipToPadding(false);
255 }
Adam Cohen44729e32010-07-22 16:00:07 -0700256 mFirstLayoutHappened = true;
257 }
258 }
259
260 @Override
261 public boolean onInterceptTouchEvent(MotionEvent ev) {
262 int action = ev.getAction();
263 switch(action & MotionEvent.ACTION_MASK) {
264
265 case MotionEvent.ACTION_DOWN: {
266 if (mActivePointerId == INVALID_POINTER) {
267 mInitialX = ev.getX();
268 mInitialY = ev.getY();
269 mActivePointerId = ev.getPointerId(0);
270 }
271 break;
272 }
273 case MotionEvent.ACTION_MOVE: {
274 int pointerIndex = ev.findPointerIndex(mActivePointerId);
275 if (pointerIndex == INVALID_POINTER) {
276 // no data for our primary pointer, this shouldn't happen, log it
277 Log.d(TAG, "Error: No data for our primary pointer.");
278 return false;
279 }
Adam Cohen44729e32010-07-22 16:00:07 -0700280 float newY = ev.getY(pointerIndex);
281 float deltaY = newY - mInitialY;
282
Adam Cohen32a42f12010-08-11 19:34:30 -0700283 beginGestureIfNeeded(deltaY);
Adam Cohen44729e32010-07-22 16:00:07 -0700284 break;
285 }
286 case MotionEvent.ACTION_POINTER_UP: {
287 onSecondaryPointerUp(ev);
288 break;
289 }
290 case MotionEvent.ACTION_UP:
291 case MotionEvent.ACTION_CANCEL: {
292 mActivePointerId = INVALID_POINTER;
293 mSwipeGestureType = GESTURE_NONE;
Adam Cohen44729e32010-07-22 16:00:07 -0700294 }
295 }
296
297 return mSwipeGestureType != GESTURE_NONE;
298 }
299
Adam Cohen32a42f12010-08-11 19:34:30 -0700300 private void beginGestureIfNeeded(float deltaY) {
301 if ((int) Math.abs(deltaY) > mTouchSlop && mSwipeGestureType == GESTURE_NONE) {
Adam Cohen3d07af02010-08-18 17:46:23 -0700302 int swipeGestureType = deltaY < 0 ? GESTURE_SLIDE_UP : GESTURE_SLIDE_DOWN;
Adam Cohen32a42f12010-08-11 19:34:30 -0700303 cancelLongPress();
304 requestDisallowInterceptTouchEvent(true);
305
Adam Cohen3d07af02010-08-18 17:46:23 -0700306 int activeIndex = swipeGestureType == GESTURE_SLIDE_DOWN ? mNumActiveViews - 1
Adam Cohen32a42f12010-08-11 19:34:30 -0700307 : mNumActiveViews - 2;
308
Adam Cohen3d07af02010-08-18 17:46:23 -0700309 if (mAdapter == null) return;
310
311 if (mCurrentWindowStartUnbounded + activeIndex == 0) {
312 mStackSlider.setMode(StackSlider.BEGINNING_OF_STACK_MODE);
313 } else if (mCurrentWindowStartUnbounded + activeIndex == mAdapter.getCount()) {
314 activeIndex--;
315 mStackSlider.setMode(StackSlider.END_OF_STACK_MODE);
316 } else {
317 mStackSlider.setMode(StackSlider.NORMAL_MODE);
Adam Cohen32a42f12010-08-11 19:34:30 -0700318 }
Adam Cohen3d07af02010-08-18 17:46:23 -0700319
320 View v = getViewAtRelativeIndex(activeIndex);
321 if (v == null) return;
322
323 mHighlight.setImageBitmap(createOutline(v));
324 mHighlight.bringToFront();
325 v.bringToFront();
326 mStackSlider.setView(v);
327
328 if (swipeGestureType == GESTURE_SLIDE_DOWN)
329 v.setVisibility(VISIBLE);
330
331 // We only register this gesture if we've made it this far without a problem
332 mSwipeGestureType = swipeGestureType;
Adam Cohen32a42f12010-08-11 19:34:30 -0700333 }
334 }
335
Adam Cohen44729e32010-07-22 16:00:07 -0700336 @Override
337 public boolean onTouchEvent(MotionEvent ev) {
338 int action = ev.getAction();
339 int pointerIndex = ev.findPointerIndex(mActivePointerId);
340 if (pointerIndex == INVALID_POINTER) {
341 // no data for our primary pointer, this shouldn't happen, log it
342 Log.d(TAG, "Error: No data for our primary pointer.");
343 return false;
344 }
345
346 float newY = ev.getY(pointerIndex);
Adam Cohen32a42f12010-08-11 19:34:30 -0700347 float newX = ev.getX(pointerIndex);
Adam Cohen44729e32010-07-22 16:00:07 -0700348 float deltaY = newY - mInitialY;
Adam Cohen32a42f12010-08-11 19:34:30 -0700349 float deltaX = newX - mInitialX;
Adam Cohen44729e32010-07-22 16:00:07 -0700350 if (mVelocityTracker == null) {
351 mVelocityTracker = VelocityTracker.obtain();
352 }
353 mVelocityTracker.addMovement(ev);
354
355 switch (action & MotionEvent.ACTION_MASK) {
356 case MotionEvent.ACTION_MOVE: {
Adam Cohen32a42f12010-08-11 19:34:30 -0700357 beginGestureIfNeeded(deltaY);
358
Adam Cohen3d07af02010-08-18 17:46:23 -0700359 float rx = deltaX/(mViewHeight*1.0f);
Adam Cohen32a42f12010-08-11 19:34:30 -0700360 if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
361 float r = (deltaY-mTouchSlop*1.0f)/mViewHeight*1.0f;
362 mStackSlider.setYProgress(1 - r);
363 mStackSlider.setXProgress(rx);
364 return true;
365 } else if (mSwipeGestureType == GESTURE_SLIDE_UP) {
366 float r = -(deltaY + mTouchSlop*1.0f)/mViewHeight*1.0f;
367 mStackSlider.setYProgress(r);
368 mStackSlider.setXProgress(rx);
369 return true;
Adam Cohen44729e32010-07-22 16:00:07 -0700370 }
Adam Cohen44729e32010-07-22 16:00:07 -0700371 break;
372 }
373 case MotionEvent.ACTION_UP: {
374 handlePointerUp(ev);
375 break;
376 }
377 case MotionEvent.ACTION_POINTER_UP: {
378 onSecondaryPointerUp(ev);
379 break;
380 }
381 case MotionEvent.ACTION_CANCEL: {
382 mActivePointerId = INVALID_POINTER;
Adam Cohen44729e32010-07-22 16:00:07 -0700383 mSwipeGestureType = GESTURE_NONE;
Adam Cohen44729e32010-07-22 16:00:07 -0700384 break;
385 }
386 }
387 return true;
388 }
389
390 private final Rect touchRect = new Rect();
391 private void onSecondaryPointerUp(MotionEvent ev) {
392 final int activePointerIndex = ev.getActionIndex();
393 final int pointerId = ev.getPointerId(activePointerIndex);
394 if (pointerId == mActivePointerId) {
395
396 int activeViewIndex = (mSwipeGestureType == GESTURE_SLIDE_DOWN) ? mNumActiveViews - 1
397 : mNumActiveViews - 2;
398
399 View v = getViewAtRelativeIndex(activeViewIndex);
400 if (v == null) return;
401
402 // Our primary pointer has gone up -- let's see if we can find
403 // another pointer on the view. If so, then we should replace
404 // our primary pointer with this new pointer and adjust things
405 // so that the view doesn't jump
406 for (int index = 0; index < ev.getPointerCount(); index++) {
407 if (index != activePointerIndex) {
408
409 float x = ev.getX(index);
410 float y = ev.getY(index);
411
412 touchRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
Romain Guy5b53f912010-08-16 18:24:33 -0700413 if (touchRect.contains(Math.round(x), Math.round(y))) {
Adam Cohen44729e32010-07-22 16:00:07 -0700414 float oldX = ev.getX(activePointerIndex);
415 float oldY = ev.getY(activePointerIndex);
416
417 // adjust our frame of reference to avoid a jump
418 mInitialY += (y - oldY);
419 mInitialX += (x - oldX);
420
421 mActivePointerId = ev.getPointerId(index);
422 if (mVelocityTracker != null) {
423 mVelocityTracker.clear();
424 }
425 // ok, we're good, we found a new pointer which is touching the active view
426 return;
427 }
428 }
429 }
430 // if we made it this far, it means we didn't find a satisfactory new pointer :(,
Adam Cohen3d07af02010-08-18 17:46:23 -0700431 // so end the gesture
Adam Cohen44729e32010-07-22 16:00:07 -0700432 handlePointerUp(ev);
433 }
434 }
435
436 private void handlePointerUp(MotionEvent ev) {
437 int pointerIndex = ev.findPointerIndex(mActivePointerId);
438 float newY = ev.getY(pointerIndex);
439 int deltaY = (int) (newY - mInitialY);
440
Adam Cohen3d07af02010-08-18 17:46:23 -0700441 if (mVelocityTracker != null) {
442 mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
443 mYVelocity = (int) mVelocityTracker.getYVelocity(mActivePointerId);
444 }
Adam Cohen44729e32010-07-22 16:00:07 -0700445
446 if (mVelocityTracker != null) {
447 mVelocityTracker.recycle();
448 mVelocityTracker = null;
449 }
450
Adam Cohen3d07af02010-08-18 17:46:23 -0700451 if (deltaY > mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_DOWN
452 && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
Adam Cohen44729e32010-07-22 16:00:07 -0700453 // Swipe threshold exceeded, swipe down
454 showNext();
Adam Cohen32a42f12010-08-11 19:34:30 -0700455 mHighlight.bringToFront();
Adam Cohen3d07af02010-08-18 17:46:23 -0700456 } else if (deltaY < -mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_UP
457 && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
Adam Cohen44729e32010-07-22 16:00:07 -0700458 // Swipe threshold exceeded, swipe up
459 showPrevious();
Adam Cohen32a42f12010-08-11 19:34:30 -0700460 mHighlight.bringToFront();
461 } else if (mSwipeGestureType == GESTURE_SLIDE_UP) {
Adam Cohen44729e32010-07-22 16:00:07 -0700462 // Didn't swipe up far enough, snap back down
Adam Cohen3d07af02010-08-18 17:46:23 -0700463 int duration =
464 Math.round(mStackSlider.getDurationForNeutralPosition()*DEFAULT_ANIMATION_DURATION);
Adam Cohen44729e32010-07-22 16:00:07 -0700465
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700466 StackSlider animationSlider = new StackSlider(mStackSlider);
467 PropertyAnimator snapBackY = new PropertyAnimator(duration, animationSlider,
Adam Cohen32a42f12010-08-11 19:34:30 -0700468 "YProgress", mStackSlider.getYProgress(), 0);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700469 snapBackY.setInterpolator(new LinearInterpolator());
Adam Cohen32a42f12010-08-11 19:34:30 -0700470 snapBackY.start();
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700471 PropertyAnimator snapBackX = new PropertyAnimator(duration, animationSlider,
Adam Cohen32a42f12010-08-11 19:34:30 -0700472 "XProgress", mStackSlider.getXProgress(), 0);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700473 snapBackX.setInterpolator(new LinearInterpolator());
Adam Cohen32a42f12010-08-11 19:34:30 -0700474 snapBackX.start();
475 } else if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
Adam Cohen44729e32010-07-22 16:00:07 -0700476 // Didn't swipe down far enough, snap back up
Adam Cohen3d07af02010-08-18 17:46:23 -0700477 int duration = Math.round(mStackSlider.getDurationForOffscreenPosition()*
478 DEFAULT_ANIMATION_DURATION);
479
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700480 StackSlider animationSlider = new StackSlider(mStackSlider);
481 PropertyAnimator snapBackY = new PropertyAnimator(duration, animationSlider,
Adam Cohen32a42f12010-08-11 19:34:30 -0700482 "YProgress", mStackSlider.getYProgress(), 1);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700483 snapBackY.setInterpolator(new LinearInterpolator());
Adam Cohen32a42f12010-08-11 19:34:30 -0700484 snapBackY.start();
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700485 PropertyAnimator snapBackX = new PropertyAnimator(duration, animationSlider,
Adam Cohen32a42f12010-08-11 19:34:30 -0700486 "XProgress", mStackSlider.getXProgress(), 0);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700487 snapBackX.setInterpolator(new LinearInterpolator());
Adam Cohen32a42f12010-08-11 19:34:30 -0700488 snapBackX.start();
Adam Cohen44729e32010-07-22 16:00:07 -0700489 }
490
491 mActivePointerId = INVALID_POINTER;
Adam Cohen44729e32010-07-22 16:00:07 -0700492 mSwipeGestureType = GESTURE_NONE;
Adam Cohen32a42f12010-08-11 19:34:30 -0700493 }
494
495 private class StackSlider {
496 View mView;
497 float mYProgress;
498 float mXProgress;
499
Adam Cohen3d07af02010-08-18 17:46:23 -0700500 static final int NORMAL_MODE = 0;
501 static final int BEGINNING_OF_STACK_MODE = 1;
502 static final int END_OF_STACK_MODE = 2;
503
504 int mMode = NORMAL_MODE;
505
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700506 public StackSlider() {
507 }
508
509 public StackSlider(StackSlider copy) {
510 mView = copy.mView;
511 mYProgress = copy.mYProgress;
512 mXProgress = copy.mXProgress;
Adam Cohen3d07af02010-08-18 17:46:23 -0700513 mMode = copy.mMode;
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700514 }
515
Adam Cohen32a42f12010-08-11 19:34:30 -0700516 private float cubic(float r) {
517 return (float) (Math.pow(2*r-1, 3) + 1)/2.0f;
518 }
519
520 private float highlightAlphaInterpolator(float r) {
521 float pivot = 0.4f;
522 if (r < pivot) {
523 return 0.85f*cubic(r/pivot);
524 } else {
525 return 0.85f*cubic(1 - (r-pivot)/(1-pivot));
526 }
527 }
528
529 private float viewAlphaInterpolator(float r) {
530 float pivot = 0.3f;
531 if (r > pivot) {
532 return (r - pivot)/(1 - pivot);
533 } else {
534 return 0;
535 }
536 }
537
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700538 private float rotationInterpolator(float r) {
539 float pivot = 0.2f;
540 if (r < pivot) {
541 return 0;
542 } else {
543 return (r-pivot)/(1-pivot);
544 }
545 }
546
Adam Cohen32a42f12010-08-11 19:34:30 -0700547 void setView(View v) {
548 mView = v;
549 }
550
551 public void setYProgress(float r) {
552 // enforce r between 0 and 1
553 r = Math.min(1.0f, r);
554 r = Math.max(0, r);
555
556 mYProgress = r;
Adam Cohen32a42f12010-08-11 19:34:30 -0700557 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
558 final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
559
Adam Cohen3d07af02010-08-18 17:46:23 -0700560 switch (mMode) {
561 case NORMAL_MODE:
562 viewLp.setVerticalOffset(Math.round(-r*mViewHeight));
563 highlightLp.setVerticalOffset(Math.round(-r*mViewHeight));
564 mHighlight.setAlpha(highlightAlphaInterpolator(r));
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700565
Adam Cohen3d07af02010-08-18 17:46:23 -0700566 float alpha = viewAlphaInterpolator(1-r);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700567
Adam Cohen3d07af02010-08-18 17:46:23 -0700568 // We make sure that views which can't be seen (have 0 alpha) are also invisible
569 // so that they don't interfere with click events.
570 if (mView.getAlpha() == 0 && alpha != 0 && mView.getVisibility() != VISIBLE) {
571 mView.setVisibility(VISIBLE);
572 } else if (alpha == 0 && mView.getAlpha() != 0
573 && mView.getVisibility() == VISIBLE) {
574 mView.setVisibility(INVISIBLE);
575 }
576
577 mView.setAlpha(alpha);
578 mView.setRotationX(90.0f*rotationInterpolator(r));
579 mHighlight.setRotationX(90.0f*rotationInterpolator(r));
580 break;
581 case BEGINNING_OF_STACK_MODE:
582 r = r*0.2f;
583 viewLp.setVerticalOffset(Math.round(-r*mViewHeight));
584 highlightLp.setVerticalOffset(Math.round(-r*mViewHeight));
585 mHighlight.setAlpha(highlightAlphaInterpolator(r));
586 break;
587 case END_OF_STACK_MODE:
588 r = (1-r)*0.2f;
589 viewLp.setVerticalOffset(Math.round(r*mViewHeight));
590 highlightLp.setVerticalOffset(Math.round(r*mViewHeight));
591 mHighlight.setAlpha(highlightAlphaInterpolator(r));
592 break;
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700593 }
Adam Cohen32a42f12010-08-11 19:34:30 -0700594 }
595
596 public void setXProgress(float r) {
597 // enforce r between 0 and 1
Adam Cohen3d07af02010-08-18 17:46:23 -0700598 r = Math.min(2.0f, r);
599 r = Math.max(-2.0f, r);
Adam Cohen32a42f12010-08-11 19:34:30 -0700600
601 mXProgress = r;
602
603 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
604 final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
605
Adam Cohen3d07af02010-08-18 17:46:23 -0700606 r *= 0.2f;
Romain Guy5b53f912010-08-16 18:24:33 -0700607 viewLp.setHorizontalOffset(Math.round(r*mViewHeight));
608 highlightLp.setHorizontalOffset(Math.round(r*mViewHeight));
Adam Cohen32a42f12010-08-11 19:34:30 -0700609 }
610
Adam Cohen3d07af02010-08-18 17:46:23 -0700611 void setMode(int mode) {
612 mMode = mode;
613 }
614
615 float getDurationForNeutralPosition() {
616 return getDuration(false);
617 }
618
619 float getDurationForOffscreenPosition() {
620 return getDuration(mMode == END_OF_STACK_MODE ? false : true);
621 }
622
623 private float getDuration(boolean invert) {
624 if (mView != null) {
625 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
626
627 float d = (float) Math.sqrt(Math.pow(viewLp.horizontalOffset,2) +
628 Math.pow(viewLp.verticalOffset,2));
629 float maxd = (float) Math.sqrt(Math.pow(mViewHeight, 2) +
630 Math.pow(0.4f*mViewHeight, 2));
631 return invert ? (1-d/maxd) : d/maxd;
632 }
633 return 0;
634 }
635
Adam Cohen32a42f12010-08-11 19:34:30 -0700636 float getYProgress() {
637 return mYProgress;
638 }
639
640 float getXProgress() {
641 return mXProgress;
642 }
Adam Cohen3d07af02010-08-18 17:46:23 -0700643
Adam Cohen44729e32010-07-22 16:00:07 -0700644 }
645
646 @Override
647 public void onRemoteAdapterConnected() {
648 super.onRemoteAdapterConnected();
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700649 setDisplayedChild(mWhichChild);
Adam Cohen44729e32010-07-22 16:00:07 -0700650 }
Adam Cohen32a42f12010-08-11 19:34:30 -0700651
652 private static final Paint sHolographicPaint = new Paint();
653 private static final Paint sErasePaint = new Paint();
654 private static boolean sPaintsInitialized = false;
655 private static final float STROKE_WIDTH = 3.0f;
656
Romain Guy5b53f912010-08-16 18:24:33 -0700657 static void initializePaints() {
Adam Cohen32a42f12010-08-11 19:34:30 -0700658 sHolographicPaint.setColor(0xff6699ff);
659 sHolographicPaint.setFilterBitmap(true);
660 sErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
661 sErasePaint.setFilterBitmap(true);
662 sPaintsInitialized = true;
663 }
664
665 static Bitmap createOutline(View v) {
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700666 if (v.getMeasuredWidth() == 0 || v.getMeasuredHeight() == 0) {
667 return null;
668 }
669
Adam Cohen32a42f12010-08-11 19:34:30 -0700670 Bitmap bitmap = Bitmap.createBitmap(v.getMeasuredWidth(), v.getMeasuredHeight(),
671 Bitmap.Config.ARGB_8888);
672 Canvas canvas = new Canvas(bitmap);
673
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700674 float rotationX = v.getRotationX();
675 v.setRotationX(0);
Adam Cohen32a42f12010-08-11 19:34:30 -0700676 canvas.concat(v.getMatrix());
677 v.draw(canvas);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700678 v.setRotationX(rotationX);
Adam Cohen32a42f12010-08-11 19:34:30 -0700679
680 Bitmap outlineBitmap = Bitmap.createBitmap(v.getMeasuredWidth(), v.getMeasuredHeight(),
681 Bitmap.Config.ARGB_8888);
682 Canvas outlineCanvas = new Canvas(outlineBitmap);
Romain Guy5b53f912010-08-16 18:24:33 -0700683 drawOutline(outlineCanvas, bitmap);
Adam Cohen32a42f12010-08-11 19:34:30 -0700684 bitmap.recycle();
685 return outlineBitmap;
686 }
687
Romain Guy5b53f912010-08-16 18:24:33 -0700688 static void drawOutline(Canvas dest, Bitmap src) {
Adam Cohen32a42f12010-08-11 19:34:30 -0700689 dest.drawColor(0, PorterDuff.Mode.CLEAR);
690
691 Bitmap mask = src.extractAlpha();
692 Matrix id = new Matrix();
693
694 Matrix m = new Matrix();
695 float xScale = STROKE_WIDTH*2/(src.getWidth());
696 float yScale = STROKE_WIDTH*2/(src.getHeight());
697 m.preScale(1+xScale, 1+yScale, src.getWidth()/2, src.getHeight()/2);
698 dest.drawBitmap(mask, m, sHolographicPaint);
699
700 dest.drawBitmap(src, id, sErasePaint);
701 mask.recycle();
702 }
Adam Cohen44729e32010-07-22 16:00:07 -0700703}