blob: aee48c6633bf03f9295aca0ba8ae1a0b1b96e6ae [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
Chet Haasea18a86b2010-09-07 13:20:00 -070019import android.animation.ObjectAnimator;
Romain Guye5ebcb02010-10-15 13:57:28 -070020import android.animation.PropertyValuesHolder;
Adam Cohen44729e32010-07-22 16:00:07 -070021import android.content.Context;
Adam Cohen32a42f12010-08-11 19:34:30 -070022import android.graphics.Bitmap;
Adam Cohen839f4a52010-08-26 17:36:48 -070023import android.graphics.BlurMaskFilter;
Adam Cohen32a42f12010-08-11 19:34:30 -070024import 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;
Adam Cohen9b073942010-08-19 16:49:52 -070030import android.graphics.RectF;
Adam Cohend51bbb52010-10-18 10:59:49 -070031import android.graphics.Region;
Adam Cohen839f4a52010-08-26 17:36:48 -070032import android.graphics.TableMaskFilter;
Adam Cohen44729e32010-07-22 16:00:07 -070033import android.util.AttributeSet;
34import android.util.Log;
35import android.view.MotionEvent;
36import android.view.VelocityTracker;
37import android.view.View;
38import android.view.ViewConfiguration;
39import android.view.ViewGroup;
Adam Cohenb04f7ad2010-08-15 13:22:42 -070040import android.view.animation.LinearInterpolator;
Adam Cohen44729e32010-07-22 16:00:07 -070041import android.widget.RemoteViews.RemoteView;
42
43@RemoteView
44/**
45 * A view that displays its children in a stack and allows users to discretely swipe
46 * through the children.
47 */
48public class StackView extends AdapterViewAnimator {
49 private final String TAG = "StackView";
50
51 /**
52 * Default animation parameters
53 */
Romain Guye5ebcb02010-10-15 13:57:28 -070054 private static final int DEFAULT_ANIMATION_DURATION = 400;
55 private static final int MINIMUM_ANIMATION_DURATION = 50;
Adam Cohen44729e32010-07-22 16:00:07 -070056
57 /**
Adam Cohen839f4a52010-08-26 17:36:48 -070058 * Parameters effecting the perspective visuals
59 */
60 private static float PERSPECTIVE_SHIFT_FACTOR = 0.12f;
Romain Guye5ebcb02010-10-15 13:57:28 -070061 @SuppressWarnings({"FieldCanBeLocal"})
Adam Cohen839f4a52010-08-26 17:36:48 -070062 private static float PERSPECTIVE_SCALE_FACTOR = 0.35f;
63
64 /**
65 * Represent the two possible stack modes, one where items slide up, and the other
66 * where items slide down. The perspective is also inverted between these two modes.
67 */
68 private static final int ITEMS_SLIDE_UP = 0;
69 private static final int ITEMS_SLIDE_DOWN = 1;
70
71 /**
Adam Cohen44729e32010-07-22 16:00:07 -070072 * These specify the different gesture states
73 */
Romain Guy5b53f912010-08-16 18:24:33 -070074 private static final int GESTURE_NONE = 0;
75 private static final int GESTURE_SLIDE_UP = 1;
76 private static final int GESTURE_SLIDE_DOWN = 2;
Adam Cohen44729e32010-07-22 16:00:07 -070077
78 /**
79 * Specifies how far you need to swipe (up or down) before it
80 * will be consider a completed gesture when you lift your finger
81 */
Adam Cohena9238c82010-10-25 14:01:29 -070082 private static final float SWIPE_THRESHOLD_RATIO = 0.2f;
83
84 /**
85 * Specifies the total distance, relative to the size of the stack,
86 * that views will be slid, either up or down
87 */
Romain Guy5b53f912010-08-16 18:24:33 -070088 private static final float SLIDE_UP_RATIO = 0.7f;
Adam Cohen44729e32010-07-22 16:00:07 -070089
Adam Cohen44729e32010-07-22 16:00:07 -070090 /**
91 * Sentinel value for no current active pointer.
92 * Used by {@link #mActivePointerId}.
93 */
94 private static final int INVALID_POINTER = -1;
95
96 /**
Adam Cohen839f4a52010-08-26 17:36:48 -070097 * Number of active views in the stack. One fewer view is actually visible, as one is hidden.
98 */
99 private static final int NUM_ACTIVE_VIEWS = 5;
100
Adam Cohendfcdddd2010-09-10 14:38:40 -0700101 private static final int FRAME_PADDING = 4;
Adam Cohen839f4a52010-08-26 17:36:48 -0700102
Patrick Dubroye80202d2010-11-16 15:41:08 -0800103 private final Rect mTouchRect = new Rect();
104
Adam Cohen839f4a52010-08-26 17:36:48 -0700105 /**
Adam Cohen44729e32010-07-22 16:00:07 -0700106 * These variables are all related to the current state of touch interaction
107 * with the stack
108 */
Adam Cohen44729e32010-07-22 16:00:07 -0700109 private float mInitialY;
110 private float mInitialX;
111 private int mActivePointerId;
Adam Cohen44729e32010-07-22 16:00:07 -0700112 private int mYVelocity = 0;
113 private int mSwipeGestureType = GESTURE_NONE;
Adam Cohendfcdddd2010-09-10 14:38:40 -0700114 private int mSlideAmount;
Adam Cohen44729e32010-07-22 16:00:07 -0700115 private int mSwipeThreshold;
116 private int mTouchSlop;
117 private int mMaximumVelocity;
118 private VelocityTracker mVelocityTracker;
Adam Cohen3352b682010-10-26 13:54:00 -0700119 private boolean mTransitionIsSetup = false;
Adam Cohen44729e32010-07-22 16:00:07 -0700120
Adam Cohen9b073942010-08-19 16:49:52 -0700121 private static HolographicHelper sHolographicHelper;
Adam Cohen32a42f12010-08-11 19:34:30 -0700122 private ImageView mHighlight;
Adam Cohen8baf5df2010-11-11 15:23:41 -0800123 private ImageView mClickFeedback;
124 private boolean mClickFeedbackIsValid = false;
Adam Cohen32a42f12010-08-11 19:34:30 -0700125 private StackSlider mStackSlider;
Adam Cohen44729e32010-07-22 16:00:07 -0700126 private boolean mFirstLayoutHappened = false;
Adam Cohen839f4a52010-08-26 17:36:48 -0700127 private int mStackMode;
Adam Cohendfcdddd2010-09-10 14:38:40 -0700128 private int mFramePadding;
Adam Cohen0ac116b2010-11-11 11:39:53 -0800129 private final Rect stackInvalidateRect = new Rect();
Adam Cohen44729e32010-07-22 16:00:07 -0700130
Adam Cohen44729e32010-07-22 16:00:07 -0700131 public StackView(Context context) {
132 super(context);
133 initStackView();
134 }
135
136 public StackView(Context context, AttributeSet attrs) {
137 super(context, attrs);
138 initStackView();
139 }
140
141 private void initStackView() {
Adam Cohen96d8d562010-10-24 11:12:18 -0700142 configureViewAnimator(NUM_ACTIVE_VIEWS, 1);
Adam Cohen44729e32010-07-22 16:00:07 -0700143 setStaticTransformationsEnabled(true);
144 final ViewConfiguration configuration = ViewConfiguration.get(getContext());
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700145 mTouchSlop = configuration.getScaledTouchSlop();
Adam Cohen44729e32010-07-22 16:00:07 -0700146 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
147 mActivePointerId = INVALID_POINTER;
Adam Cohen32a42f12010-08-11 19:34:30 -0700148
149 mHighlight = new ImageView(getContext());
150 mHighlight.setLayoutParams(new LayoutParams(mHighlight));
151 addViewInLayout(mHighlight, -1, new LayoutParams(mHighlight));
Adam Cohen8baf5df2010-11-11 15:23:41 -0800152
153 mClickFeedback = new ImageView(getContext());
154 mClickFeedback.setLayoutParams(new LayoutParams(mClickFeedback));
155 addViewInLayout(mClickFeedback, -1, new LayoutParams(mClickFeedback));
156 mClickFeedback.setVisibility(INVISIBLE);
157
Adam Cohen32a42f12010-08-11 19:34:30 -0700158 mStackSlider = new StackSlider();
159
Adam Cohen9b073942010-08-19 16:49:52 -0700160 if (sHolographicHelper == null) {
Adam Cohendfcdddd2010-09-10 14:38:40 -0700161 sHolographicHelper = new HolographicHelper(mContext);
Adam Cohen32a42f12010-08-11 19:34:30 -0700162 }
Adam Cohen9b073942010-08-19 16:49:52 -0700163 setClipChildren(false);
164 setClipToPadding(false);
Adam Cohen1480fdd2010-08-25 17:24:53 -0700165
Adam Cohen839f4a52010-08-26 17:36:48 -0700166 // This sets the form of the StackView, which is currently to have the perspective-shifted
167 // views above the active view, and have items slide down when sliding out. The opposite is
168 // available by using ITEMS_SLIDE_UP.
169 mStackMode = ITEMS_SLIDE_DOWN;
170
Adam Cohen1480fdd2010-08-25 17:24:53 -0700171 // This is a flag to indicate the the stack is loading for the first time
172 mWhichChild = -1;
Adam Cohendfcdddd2010-09-10 14:38:40 -0700173
174 // Adjust the frame padding based on the density, since the highlight changes based
175 // on the density
176 final float density = mContext.getResources().getDisplayMetrics().density;
177 mFramePadding = (int) Math.ceil(density * FRAME_PADDING);
Adam Cohen44729e32010-07-22 16:00:07 -0700178 }
179
180 /**
181 * Animate the views between different relative indexes within the {@link AdapterViewAnimator}
182 */
183 void animateViewForTransition(int fromIndex, int toIndex, View view) {
Adam Cohen96d8d562010-10-24 11:12:18 -0700184 if (fromIndex == -1 && toIndex != 0) {
Adam Cohen44729e32010-07-22 16:00:07 -0700185 // Fade item in
186 if (view.getAlpha() == 1) {
187 view.setAlpha(0);
188 }
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700189 view.setVisibility(VISIBLE);
190
Chet Haase2794eb32010-10-12 16:29:28 -0700191 ObjectAnimator fadeIn = ObjectAnimator.ofFloat(view, "alpha", view.getAlpha(), 1.0f);
192 fadeIn.setDuration(DEFAULT_ANIMATION_DURATION);
Adam Cohen44729e32010-07-22 16:00:07 -0700193 fadeIn.start();
Adam Cohen96d8d562010-10-24 11:12:18 -0700194 } else if (fromIndex == 0 && toIndex == 1) {
Adam Cohen44729e32010-07-22 16:00:07 -0700195 // Slide item in
196 view.setVisibility(VISIBLE);
Adam Cohen32a42f12010-08-11 19:34:30 -0700197
Adam Cohen839f4a52010-08-26 17:36:48 -0700198 int duration = Math.round(mStackSlider.getDurationForNeutralPosition(mYVelocity));
Adam Cohen44729e32010-07-22 16:00:07 -0700199
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700200 StackSlider animationSlider = new StackSlider(mStackSlider);
Chet Haase2794eb32010-10-12 16:29:28 -0700201 PropertyValuesHolder slideInY = PropertyValuesHolder.ofFloat("YProgress", 0.0f);
202 PropertyValuesHolder slideInX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
203 ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
Adam Cohen839f4a52010-08-26 17:36:48 -0700204 slideInX, slideInY);
Chet Haase2794eb32010-10-12 16:29:28 -0700205 pa.setDuration(duration);
Adam Cohen839f4a52010-08-26 17:36:48 -0700206 pa.setInterpolator(new LinearInterpolator());
207 pa.start();
Adam Cohen96d8d562010-10-24 11:12:18 -0700208 } else if (fromIndex == 1 && toIndex == 0) {
Adam Cohen44729e32010-07-22 16:00:07 -0700209 // Slide item out
Adam Cohen839f4a52010-08-26 17:36:48 -0700210 int duration = Math.round(mStackSlider.getDurationForOffscreenPosition(mYVelocity));
Adam Cohen44729e32010-07-22 16:00:07 -0700211
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700212 StackSlider animationSlider = new StackSlider(mStackSlider);
Chet Haase2794eb32010-10-12 16:29:28 -0700213 PropertyValuesHolder slideOutY = PropertyValuesHolder.ofFloat("YProgress", 1.0f);
214 PropertyValuesHolder slideOutX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
215 ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
216 slideOutX, slideOutY);
217 pa.setDuration(duration);
Adam Cohen839f4a52010-08-26 17:36:48 -0700218 pa.setInterpolator(new LinearInterpolator());
219 pa.start();
Adam Cohen96d8d562010-10-24 11:12:18 -0700220 } else if (fromIndex == -1 && toIndex == 0) {
Adam Cohen44729e32010-07-22 16:00:07 -0700221 // Make sure this view that is "waiting in the wings" is invisible
222 view.setAlpha(0.0f);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700223 view.setVisibility(INVISIBLE);
224 LayoutParams lp = (LayoutParams) view.getLayoutParams();
Adam Cohendfcdddd2010-09-10 14:38:40 -0700225 lp.setVerticalOffset(-mSlideAmount);
Adam Cohen44729e32010-07-22 16:00:07 -0700226 } else if (toIndex == -1) {
227 // Fade item out
Chet Haase2794eb32010-10-12 16:29:28 -0700228 ObjectAnimator fadeOut = ObjectAnimator.ofFloat(view, "alpha", view.getAlpha(), 0.0f);
229 fadeOut.setDuration(DEFAULT_ANIMATION_DURATION);
Adam Cohen44729e32010-07-22 16:00:07 -0700230 fadeOut.start();
231 }
Adam Cohen839f4a52010-08-26 17:36:48 -0700232
233 // Implement the faked perspective
234 if (toIndex != -1) {
Adam Cohenf04e2252010-09-09 18:41:20 -0700235 transformViewAtIndex(toIndex, view);
236 }
237 }
Adam Cohen839f4a52010-08-26 17:36:48 -0700238
Adam Cohen3352b682010-10-26 13:54:00 -0700239 private void setupStackSlider(View v, int mode) {
240 mStackSlider.setMode(mode);
241 if (v != null) {
242 mHighlight.setImageBitmap(sHolographicHelper.createOutline(v));
243 mHighlight.setRotation(v.getRotation());
244 mHighlight.setTranslationY(v.getTranslationY());
245 mHighlight.bringToFront();
246 v.bringToFront();
247 mStackSlider.setView(v);
248
249 v.setVisibility(VISIBLE);
250 }
251 }
252
253 @Override
254 @android.view.RemotableViewMethod
255 public void showNext() {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800256 if (mSwipeGestureType != GESTURE_NONE) return;
Adam Cohen3352b682010-10-26 13:54:00 -0700257 if (!mTransitionIsSetup) {
258 View v = getViewAtRelativeIndex(1);
259 if (v != null) {
260 setupStackSlider(v, StackSlider.NORMAL_MODE);
261 mStackSlider.setYProgress(0);
262 mStackSlider.setXProgress(0);
263 }
264 }
265 super.showNext();
266 }
267
268 @Override
269 @android.view.RemotableViewMethod
270 public void showPrevious() {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800271 if (mSwipeGestureType != GESTURE_NONE) return;
Adam Cohen3352b682010-10-26 13:54:00 -0700272 if (!mTransitionIsSetup) {
273 View v = getViewAtRelativeIndex(0);
274 if (v != null) {
275 setupStackSlider(v, StackSlider.NORMAL_MODE);
276 mStackSlider.setYProgress(1);
277 mStackSlider.setXProgress(0);
278 }
279 }
280 super.showPrevious();
281 }
282
Adam Cohenf04e2252010-09-09 18:41:20 -0700283 private void transformViewAtIndex(int index, View view) {
Adam Cohendfcdddd2010-09-10 14:38:40 -0700284 float maxPerpectiveShift = mMeasuredHeight * PERSPECTIVE_SHIFT_FACTOR;
Adam Cohen839f4a52010-08-26 17:36:48 -0700285
Adam Cohen96d8d562010-10-24 11:12:18 -0700286 index = mMaxNumActiveViews - index - 1;
287 if (index == mMaxNumActiveViews - 1) index--;
Adam Cohen839f4a52010-08-26 17:36:48 -0700288
Adam Cohen96d8d562010-10-24 11:12:18 -0700289 float r = (index * 1.0f) / (mMaxNumActiveViews - 2);
Adam Cohen839f4a52010-08-26 17:36:48 -0700290
Adam Cohenf04e2252010-09-09 18:41:20 -0700291 float scale = 1 - PERSPECTIVE_SCALE_FACTOR * (1 - r);
Chet Haase2794eb32010-10-12 16:29:28 -0700292 PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", scale);
293 PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", scale);
Adam Cohen839f4a52010-08-26 17:36:48 -0700294
Adam Cohenf04e2252010-09-09 18:41:20 -0700295 r = (float) Math.pow(r, 2);
Adam Cohen839f4a52010-08-26 17:36:48 -0700296
Adam Cohenf04e2252010-09-09 18:41:20 -0700297 int stackDirection = (mStackMode == ITEMS_SLIDE_UP) ? 1 : -1;
Adam Cohendfcdddd2010-09-10 14:38:40 -0700298 float perspectiveTranslation = -stackDirection * r * maxPerpectiveShift;
299 float scaleShiftCorrection = stackDirection * (1 - scale) *
300 (mMeasuredHeight * (1 - PERSPECTIVE_SHIFT_FACTOR) / 2.0f);
301 float transY = perspectiveTranslation + scaleShiftCorrection;
Adam Cohenf04e2252010-09-09 18:41:20 -0700302
Chet Haase2794eb32010-10-12 16:29:28 -0700303 PropertyValuesHolder translationY = PropertyValuesHolder.ofFloat("translationY", transY);
304 ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(view, scaleX, scaleY, translationY);
305 pa.setDuration(100);
Adam Cohenf04e2252010-09-09 18:41:20 -0700306 pa.start();
307 }
308
Adam Cohen96d8d562010-10-24 11:12:18 -0700309 @Override
310 void showOnly(int childIndex, boolean animate, boolean onLayout) {
311 super.showOnly(childIndex, animate, onLayout);
312
313 // Here we need to make sure that the z-order of the children is correct
Adam Cohen3352b682010-10-26 13:54:00 -0700314 for (int i = mCurrentWindowEnd; i >= mCurrentWindowStart; i--) {
Adam Cohen96d8d562010-10-24 11:12:18 -0700315 int index = modulo(i, getWindowSize());
Adam Cohen0ac116b2010-11-11 11:39:53 -0800316 ViewAndIndex vi = mViewsMap.get(index);
317 if (vi != null) {
318 View v = mViewsMap.get(index).view;
319 if (v != null) v.bringToFront();
320 }
Adam Cohen96d8d562010-10-24 11:12:18 -0700321 }
Adam Cohen3352b682010-10-26 13:54:00 -0700322 mTransitionIsSetup = false;
Adam Cohen8baf5df2010-11-11 15:23:41 -0800323 mClickFeedbackIsValid = false;
324 }
325
326 void updateClickFeedback() {
327 if (!mClickFeedbackIsValid) {
Adam Cohen9c295482010-11-18 15:19:48 -0800328 View v = getViewAtRelativeIndex(1);
Adam Cohen8baf5df2010-11-11 15:23:41 -0800329 if (v != null) {
330 mClickFeedback.setImageBitmap(sHolographicHelper.createOutline(v,
331 HolographicHelper.CLICK_FEEDBACK));
332 mClickFeedback.setTranslationX(v.getTranslationX());
333 mClickFeedback.setTranslationY(v.getTranslationY());
334 }
335 mClickFeedbackIsValid = true;
336 }
337 }
338
339 @Override
340 void showTapFeedback(View v) {
341 updateClickFeedback();
342 mClickFeedback.setVisibility(VISIBLE);
343 mClickFeedback.bringToFront();
344 invalidate();
345 }
346
347 @Override
348 void hideTapFeedback(View v) {
349 mClickFeedback.setVisibility(INVISIBLE);
350 invalidate();
Adam Cohen96d8d562010-10-24 11:12:18 -0700351 }
352
Adam Cohenf04e2252010-09-09 18:41:20 -0700353 private void updateChildTransforms() {
Adam Cohen96d8d562010-10-24 11:12:18 -0700354 for (int i = 0; i < getNumActiveViews(); i++) {
Adam Cohenf04e2252010-09-09 18:41:20 -0700355 View v = getViewAtRelativeIndex(i);
356 if (v != null) {
357 transformViewAtIndex(i, v);
358 }
Adam Cohen839f4a52010-08-26 17:36:48 -0700359 }
Adam Cohen44729e32010-07-22 16:00:07 -0700360 }
361
Adam Cohendfcdddd2010-09-10 14:38:40 -0700362 @Override
363 FrameLayout getFrameForChild() {
364 FrameLayout fl = new FrameLayout(mContext);
365 fl.setPadding(mFramePadding, mFramePadding, mFramePadding, mFramePadding);
366 return fl;
367 }
368
Adam Cohen44729e32010-07-22 16:00:07 -0700369 /**
370 * Apply any necessary tranforms for the child that is being added.
371 */
372 void applyTransformForChildAtIndex(View child, int relativeIndex) {
Adam Cohen44729e32010-07-22 16:00:07 -0700373 }
374
375 @Override
Adam Cohen9b073942010-08-19 16:49:52 -0700376 protected void dispatchDraw(Canvas canvas) {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800377 canvas.getClipBounds(stackInvalidateRect);
Adam Cohend51bbb52010-10-18 10:59:49 -0700378 final int childCount = getChildCount();
379 for (int i = 0; i < childCount; i++) {
380 LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
Adam Cohen0ac116b2010-11-11 11:39:53 -0800381 stackInvalidateRect.union(lp.getInvalidateRect());
Adam Cohend51bbb52010-10-18 10:59:49 -0700382 lp.resetInvalidateRect();
Adam Cohen44729e32010-07-22 16:00:07 -0700383 }
Adam Cohend51bbb52010-10-18 10:59:49 -0700384 canvas.save(Canvas.CLIP_SAVE_FLAG);
Adam Cohen0ac116b2010-11-11 11:39:53 -0800385 canvas.clipRect(stackInvalidateRect, Region.Op.UNION);
Adam Cohend51bbb52010-10-18 10:59:49 -0700386 super.dispatchDraw(canvas);
387 canvas.restore();
Adam Cohen9b073942010-08-19 16:49:52 -0700388 }
Adam Cohen44729e32010-07-22 16:00:07 -0700389
Adam Cohen9b073942010-08-19 16:49:52 -0700390 private void onLayout() {
Adam Cohen44729e32010-07-22 16:00:07 -0700391 if (!mFirstLayoutHappened) {
Adam Cohendfcdddd2010-09-10 14:38:40 -0700392 mSlideAmount = Math.round(SLIDE_UP_RATIO * getMeasuredHeight());
Adam Cohenf04e2252010-09-09 18:41:20 -0700393 updateChildTransforms();
Adam Cohendfcdddd2010-09-10 14:38:40 -0700394 mSwipeThreshold = Math.round(SWIPE_THRESHOLD_RATIO * mSlideAmount);
Adam Cohen44729e32010-07-22 16:00:07 -0700395 mFirstLayoutHappened = true;
396 }
397 }
398
399 @Override
400 public boolean onInterceptTouchEvent(MotionEvent ev) {
401 int action = ev.getAction();
402 switch(action & MotionEvent.ACTION_MASK) {
Adam Cohen44729e32010-07-22 16:00:07 -0700403 case MotionEvent.ACTION_DOWN: {
404 if (mActivePointerId == INVALID_POINTER) {
405 mInitialX = ev.getX();
406 mInitialY = ev.getY();
407 mActivePointerId = ev.getPointerId(0);
408 }
409 break;
410 }
411 case MotionEvent.ACTION_MOVE: {
412 int pointerIndex = ev.findPointerIndex(mActivePointerId);
413 if (pointerIndex == INVALID_POINTER) {
414 // no data for our primary pointer, this shouldn't happen, log it
415 Log.d(TAG, "Error: No data for our primary pointer.");
416 return false;
417 }
Adam Cohen44729e32010-07-22 16:00:07 -0700418 float newY = ev.getY(pointerIndex);
419 float deltaY = newY - mInitialY;
420
Adam Cohen32a42f12010-08-11 19:34:30 -0700421 beginGestureIfNeeded(deltaY);
Adam Cohen44729e32010-07-22 16:00:07 -0700422 break;
423 }
424 case MotionEvent.ACTION_POINTER_UP: {
425 onSecondaryPointerUp(ev);
426 break;
427 }
428 case MotionEvent.ACTION_UP:
429 case MotionEvent.ACTION_CANCEL: {
430 mActivePointerId = INVALID_POINTER;
431 mSwipeGestureType = GESTURE_NONE;
Adam Cohen44729e32010-07-22 16:00:07 -0700432 }
433 }
434
435 return mSwipeGestureType != GESTURE_NONE;
436 }
437
Adam Cohen32a42f12010-08-11 19:34:30 -0700438 private void beginGestureIfNeeded(float deltaY) {
439 if ((int) Math.abs(deltaY) > mTouchSlop && mSwipeGestureType == GESTURE_NONE) {
Adam Cohen3d07af02010-08-18 17:46:23 -0700440 int swipeGestureType = deltaY < 0 ? GESTURE_SLIDE_UP : GESTURE_SLIDE_DOWN;
Adam Cohen32a42f12010-08-11 19:34:30 -0700441 cancelLongPress();
442 requestDisallowInterceptTouchEvent(true);
443
Adam Cohend51bbb52010-10-18 10:59:49 -0700444 if (mAdapter == null) return;
445
Adam Cohen839f4a52010-08-26 17:36:48 -0700446 int activeIndex;
447 if (mStackMode == ITEMS_SLIDE_UP) {
Adam Cohen96d8d562010-10-24 11:12:18 -0700448 activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1;
Adam Cohen839f4a52010-08-26 17:36:48 -0700449 } else {
Adam Cohen96d8d562010-10-24 11:12:18 -0700450 activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 1 : 0;
Adam Cohen839f4a52010-08-26 17:36:48 -0700451 }
Adam Cohen32a42f12010-08-11 19:34:30 -0700452
Adam Cohen3352b682010-10-26 13:54:00 -0700453 int stackMode;
Adam Cohen1b065cd2010-09-28 14:53:47 -0700454 if (mLoopViews) {
Adam Cohen3352b682010-10-26 13:54:00 -0700455 stackMode = StackSlider.NORMAL_MODE;
Adam Cohen96d8d562010-10-24 11:12:18 -0700456 } else if (mCurrentWindowStartUnbounded + activeIndex == -1) {
457 activeIndex++;
Adam Cohen3352b682010-10-26 13:54:00 -0700458 stackMode = StackSlider.BEGINNING_OF_STACK_MODE;
Adam Cohen96d8d562010-10-24 11:12:18 -0700459 } else if (mCurrentWindowStartUnbounded + activeIndex == mAdapter.getCount() - 1) {
Adam Cohen3352b682010-10-26 13:54:00 -0700460 stackMode = StackSlider.END_OF_STACK_MODE;
Adam Cohen3d07af02010-08-18 17:46:23 -0700461 } else {
Adam Cohen3352b682010-10-26 13:54:00 -0700462 stackMode = StackSlider.NORMAL_MODE;
Adam Cohen32a42f12010-08-11 19:34:30 -0700463 }
Adam Cohen3d07af02010-08-18 17:46:23 -0700464
Adam Cohen3352b682010-10-26 13:54:00 -0700465 mTransitionIsSetup = stackMode == StackSlider.NORMAL_MODE;
466
Adam Cohen3d07af02010-08-18 17:46:23 -0700467 View v = getViewAtRelativeIndex(activeIndex);
468 if (v == null) return;
469
Adam Cohen3352b682010-10-26 13:54:00 -0700470 setupStackSlider(v, stackMode);
Adam Cohen3d07af02010-08-18 17:46:23 -0700471
472 // We only register this gesture if we've made it this far without a problem
473 mSwipeGestureType = swipeGestureType;
Adam Cohena32edd42010-10-26 10:35:01 -0700474 cancelHandleClick();
Adam Cohen32a42f12010-08-11 19:34:30 -0700475 }
476 }
477
Adam Cohen44729e32010-07-22 16:00:07 -0700478 @Override
479 public boolean onTouchEvent(MotionEvent ev) {
Adam Cohena32edd42010-10-26 10:35:01 -0700480 super.onTouchEvent(ev);
481
Adam Cohen44729e32010-07-22 16:00:07 -0700482 int action = ev.getAction();
483 int pointerIndex = ev.findPointerIndex(mActivePointerId);
484 if (pointerIndex == INVALID_POINTER) {
485 // no data for our primary pointer, this shouldn't happen, log it
486 Log.d(TAG, "Error: No data for our primary pointer.");
487 return false;
488 }
489
490 float newY = ev.getY(pointerIndex);
Adam Cohen32a42f12010-08-11 19:34:30 -0700491 float newX = ev.getX(pointerIndex);
Adam Cohen44729e32010-07-22 16:00:07 -0700492 float deltaY = newY - mInitialY;
Adam Cohen32a42f12010-08-11 19:34:30 -0700493 float deltaX = newX - mInitialX;
Adam Cohen44729e32010-07-22 16:00:07 -0700494 if (mVelocityTracker == null) {
495 mVelocityTracker = VelocityTracker.obtain();
496 }
497 mVelocityTracker.addMovement(ev);
498
499 switch (action & MotionEvent.ACTION_MASK) {
500 case MotionEvent.ACTION_MOVE: {
Adam Cohen32a42f12010-08-11 19:34:30 -0700501 beginGestureIfNeeded(deltaY);
502
Adam Cohendfcdddd2010-09-10 14:38:40 -0700503 float rx = deltaX / (mSlideAmount * 1.0f);
Adam Cohen32a42f12010-08-11 19:34:30 -0700504 if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
Adam Cohendfcdddd2010-09-10 14:38:40 -0700505 float r = (deltaY - mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
Adam Cohen839f4a52010-08-26 17:36:48 -0700506 if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
Adam Cohen32a42f12010-08-11 19:34:30 -0700507 mStackSlider.setYProgress(1 - r);
508 mStackSlider.setXProgress(rx);
509 return true;
510 } else if (mSwipeGestureType == GESTURE_SLIDE_UP) {
Adam Cohendfcdddd2010-09-10 14:38:40 -0700511 float r = -(deltaY + mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
Adam Cohen839f4a52010-08-26 17:36:48 -0700512 if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
Adam Cohen32a42f12010-08-11 19:34:30 -0700513 mStackSlider.setYProgress(r);
514 mStackSlider.setXProgress(rx);
515 return true;
Adam Cohen44729e32010-07-22 16:00:07 -0700516 }
Adam Cohen44729e32010-07-22 16:00:07 -0700517 break;
518 }
519 case MotionEvent.ACTION_UP: {
520 handlePointerUp(ev);
521 break;
522 }
523 case MotionEvent.ACTION_POINTER_UP: {
524 onSecondaryPointerUp(ev);
525 break;
526 }
527 case MotionEvent.ACTION_CANCEL: {
528 mActivePointerId = INVALID_POINTER;
Adam Cohen44729e32010-07-22 16:00:07 -0700529 mSwipeGestureType = GESTURE_NONE;
Adam Cohen44729e32010-07-22 16:00:07 -0700530 break;
531 }
532 }
533 return true;
534 }
535
Adam Cohen44729e32010-07-22 16:00:07 -0700536 private void onSecondaryPointerUp(MotionEvent ev) {
537 final int activePointerIndex = ev.getActionIndex();
538 final int pointerId = ev.getPointerId(activePointerIndex);
539 if (pointerId == mActivePointerId) {
540
Adam Cohen96d8d562010-10-24 11:12:18 -0700541 int activeViewIndex = (mSwipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1;
Adam Cohen44729e32010-07-22 16:00:07 -0700542
543 View v = getViewAtRelativeIndex(activeViewIndex);
544 if (v == null) return;
545
546 // Our primary pointer has gone up -- let's see if we can find
547 // another pointer on the view. If so, then we should replace
548 // our primary pointer with this new pointer and adjust things
549 // so that the view doesn't jump
550 for (int index = 0; index < ev.getPointerCount(); index++) {
551 if (index != activePointerIndex) {
552
553 float x = ev.getX(index);
554 float y = ev.getY(index);
555
Patrick Dubroye80202d2010-11-16 15:41:08 -0800556 mTouchRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
557 if (mTouchRect.contains(Math.round(x), Math.round(y))) {
Adam Cohen44729e32010-07-22 16:00:07 -0700558 float oldX = ev.getX(activePointerIndex);
559 float oldY = ev.getY(activePointerIndex);
560
561 // adjust our frame of reference to avoid a jump
562 mInitialY += (y - oldY);
563 mInitialX += (x - oldX);
564
565 mActivePointerId = ev.getPointerId(index);
566 if (mVelocityTracker != null) {
567 mVelocityTracker.clear();
568 }
569 // ok, we're good, we found a new pointer which is touching the active view
570 return;
571 }
572 }
573 }
574 // if we made it this far, it means we didn't find a satisfactory new pointer :(,
Adam Cohen3d07af02010-08-18 17:46:23 -0700575 // so end the gesture
Adam Cohen44729e32010-07-22 16:00:07 -0700576 handlePointerUp(ev);
577 }
578 }
579
580 private void handlePointerUp(MotionEvent ev) {
581 int pointerIndex = ev.findPointerIndex(mActivePointerId);
582 float newY = ev.getY(pointerIndex);
583 int deltaY = (int) (newY - mInitialY);
584
Adam Cohen3d07af02010-08-18 17:46:23 -0700585 if (mVelocityTracker != null) {
586 mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
587 mYVelocity = (int) mVelocityTracker.getYVelocity(mActivePointerId);
588 }
Adam Cohen44729e32010-07-22 16:00:07 -0700589
590 if (mVelocityTracker != null) {
591 mVelocityTracker.recycle();
592 mVelocityTracker = null;
593 }
594
Adam Cohen3d07af02010-08-18 17:46:23 -0700595 if (deltaY > mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_DOWN
596 && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800597 // We reset the gesture variable, because otherwise we will ignore showPrevious() /
598 // showNext();
599 mSwipeGestureType = GESTURE_NONE;
600
Adam Cohen44729e32010-07-22 16:00:07 -0700601 // Swipe threshold exceeded, swipe down
Adam Cohen839f4a52010-08-26 17:36:48 -0700602 if (mStackMode == ITEMS_SLIDE_UP) {
Adam Cohen839f4a52010-08-26 17:36:48 -0700603 showPrevious();
Adam Cohen96d8d562010-10-24 11:12:18 -0700604 } else {
605 showNext();
Adam Cohen839f4a52010-08-26 17:36:48 -0700606 }
Adam Cohen32a42f12010-08-11 19:34:30 -0700607 mHighlight.bringToFront();
Adam Cohen3d07af02010-08-18 17:46:23 -0700608 } else if (deltaY < -mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_UP
609 && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800610 // We reset the gesture variable, because otherwise we will ignore showPrevious() /
611 // showNext();
612 mSwipeGestureType = GESTURE_NONE;
613
Adam Cohen44729e32010-07-22 16:00:07 -0700614 // Swipe threshold exceeded, swipe up
Adam Cohen839f4a52010-08-26 17:36:48 -0700615 if (mStackMode == ITEMS_SLIDE_UP) {
Adam Cohen839f4a52010-08-26 17:36:48 -0700616 showNext();
Adam Cohen96d8d562010-10-24 11:12:18 -0700617 } else {
618 showPrevious();
Adam Cohen839f4a52010-08-26 17:36:48 -0700619 }
620
Adam Cohen32a42f12010-08-11 19:34:30 -0700621 mHighlight.bringToFront();
Adam Cohen839f4a52010-08-26 17:36:48 -0700622 } else if (mSwipeGestureType == GESTURE_SLIDE_UP ) {
Adam Cohen44729e32010-07-22 16:00:07 -0700623 // Didn't swipe up far enough, snap back down
Adam Cohen839f4a52010-08-26 17:36:48 -0700624 int duration;
625 float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 1 : 0;
626 if (mStackMode == ITEMS_SLIDE_UP || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
627 duration = Math.round(mStackSlider.getDurationForNeutralPosition());
628 } else {
629 duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
630 }
Adam Cohen44729e32010-07-22 16:00:07 -0700631
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700632 StackSlider animationSlider = new StackSlider(mStackSlider);
Chet Haase2794eb32010-10-12 16:29:28 -0700633 PropertyValuesHolder snapBackY = PropertyValuesHolder.ofFloat("YProgress", finalYProgress);
634 PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
635 ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
Adam Cohen839f4a52010-08-26 17:36:48 -0700636 snapBackX, snapBackY);
Chet Haase2794eb32010-10-12 16:29:28 -0700637 pa.setDuration(duration);
Adam Cohen839f4a52010-08-26 17:36:48 -0700638 pa.setInterpolator(new LinearInterpolator());
639 pa.start();
Adam Cohen32a42f12010-08-11 19:34:30 -0700640 } else if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
Adam Cohen44729e32010-07-22 16:00:07 -0700641 // Didn't swipe down far enough, snap back up
Adam Cohen839f4a52010-08-26 17:36:48 -0700642 float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 0 : 1;
643 int duration;
644 if (mStackMode == ITEMS_SLIDE_DOWN || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
645 duration = Math.round(mStackSlider.getDurationForNeutralPosition());
646 } else {
647 duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
648 }
Adam Cohen3d07af02010-08-18 17:46:23 -0700649
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700650 StackSlider animationSlider = new StackSlider(mStackSlider);
Chet Haase2794eb32010-10-12 16:29:28 -0700651 PropertyValuesHolder snapBackY =
652 PropertyValuesHolder.ofFloat("YProgress",finalYProgress);
653 PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
654 ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
Adam Cohen839f4a52010-08-26 17:36:48 -0700655 snapBackX, snapBackY);
Chet Haase2794eb32010-10-12 16:29:28 -0700656 pa.setDuration(duration);
Adam Cohen839f4a52010-08-26 17:36:48 -0700657 pa.start();
Adam Cohen44729e32010-07-22 16:00:07 -0700658 }
659
660 mActivePointerId = INVALID_POINTER;
Adam Cohen44729e32010-07-22 16:00:07 -0700661 mSwipeGestureType = GESTURE_NONE;
Adam Cohen32a42f12010-08-11 19:34:30 -0700662 }
663
664 private class StackSlider {
665 View mView;
666 float mYProgress;
667 float mXProgress;
668
Adam Cohen3d07af02010-08-18 17:46:23 -0700669 static final int NORMAL_MODE = 0;
670 static final int BEGINNING_OF_STACK_MODE = 1;
671 static final int END_OF_STACK_MODE = 2;
672
673 int mMode = NORMAL_MODE;
674
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700675 public StackSlider() {
676 }
677
678 public StackSlider(StackSlider copy) {
679 mView = copy.mView;
680 mYProgress = copy.mYProgress;
681 mXProgress = copy.mXProgress;
Adam Cohen3d07af02010-08-18 17:46:23 -0700682 mMode = copy.mMode;
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700683 }
684
Adam Cohen32a42f12010-08-11 19:34:30 -0700685 private float cubic(float r) {
Adam Cohen839f4a52010-08-26 17:36:48 -0700686 return (float) (Math.pow(2 * r - 1, 3) + 1) / 2.0f;
Adam Cohen32a42f12010-08-11 19:34:30 -0700687 }
688
689 private float highlightAlphaInterpolator(float r) {
690 float pivot = 0.4f;
691 if (r < pivot) {
Adam Cohen839f4a52010-08-26 17:36:48 -0700692 return 0.85f * cubic(r / pivot);
Adam Cohen32a42f12010-08-11 19:34:30 -0700693 } else {
Adam Cohen839f4a52010-08-26 17:36:48 -0700694 return 0.85f * cubic(1 - (r - pivot) / (1 - pivot));
Adam Cohen32a42f12010-08-11 19:34:30 -0700695 }
696 }
697
698 private float viewAlphaInterpolator(float r) {
699 float pivot = 0.3f;
700 if (r > pivot) {
Adam Cohen839f4a52010-08-26 17:36:48 -0700701 return (r - pivot) / (1 - pivot);
Adam Cohen32a42f12010-08-11 19:34:30 -0700702 } else {
703 return 0;
704 }
705 }
706
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700707 private float rotationInterpolator(float r) {
708 float pivot = 0.2f;
709 if (r < pivot) {
710 return 0;
711 } else {
Adam Cohen839f4a52010-08-26 17:36:48 -0700712 return (r - pivot) / (1 - pivot);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700713 }
714 }
715
Adam Cohen32a42f12010-08-11 19:34:30 -0700716 void setView(View v) {
717 mView = v;
718 }
719
720 public void setYProgress(float r) {
721 // enforce r between 0 and 1
722 r = Math.min(1.0f, r);
723 r = Math.max(0, r);
724
725 mYProgress = r;
Adam Cohena02fdf12010-11-03 13:27:40 -0700726 if (mView == null) return;
727
Adam Cohen32a42f12010-08-11 19:34:30 -0700728 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
729 final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
730
Adam Cohen839f4a52010-08-26 17:36:48 -0700731 int stackDirection = (mStackMode == ITEMS_SLIDE_UP) ? 1 : -1;
732
Adam Cohen3d07af02010-08-18 17:46:23 -0700733 switch (mMode) {
734 case NORMAL_MODE:
Adam Cohendfcdddd2010-09-10 14:38:40 -0700735 viewLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
736 highlightLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
Adam Cohen3d07af02010-08-18 17:46:23 -0700737 mHighlight.setAlpha(highlightAlphaInterpolator(r));
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700738
Adam Cohen839f4a52010-08-26 17:36:48 -0700739 float alpha = viewAlphaInterpolator(1 - r);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700740
Adam Cohen3d07af02010-08-18 17:46:23 -0700741 // We make sure that views which can't be seen (have 0 alpha) are also invisible
742 // so that they don't interfere with click events.
743 if (mView.getAlpha() == 0 && alpha != 0 && mView.getVisibility() != VISIBLE) {
744 mView.setVisibility(VISIBLE);
745 } else if (alpha == 0 && mView.getAlpha() != 0
746 && mView.getVisibility() == VISIBLE) {
747 mView.setVisibility(INVISIBLE);
748 }
749
750 mView.setAlpha(alpha);
Adam Cohen839f4a52010-08-26 17:36:48 -0700751 mView.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
752 mHighlight.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
Adam Cohen3d07af02010-08-18 17:46:23 -0700753 break;
Adam Cohen96d8d562010-10-24 11:12:18 -0700754 case END_OF_STACK_MODE:
Adam Cohen839f4a52010-08-26 17:36:48 -0700755 r = r * 0.2f;
Adam Cohendfcdddd2010-09-10 14:38:40 -0700756 viewLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount));
757 highlightLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount));
Adam Cohen3d07af02010-08-18 17:46:23 -0700758 mHighlight.setAlpha(highlightAlphaInterpolator(r));
759 break;
Adam Cohen96d8d562010-10-24 11:12:18 -0700760 case BEGINNING_OF_STACK_MODE:
Adam Cohen839f4a52010-08-26 17:36:48 -0700761 r = (1-r) * 0.2f;
Adam Cohendfcdddd2010-09-10 14:38:40 -0700762 viewLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount));
763 highlightLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount));
Adam Cohen3d07af02010-08-18 17:46:23 -0700764 mHighlight.setAlpha(highlightAlphaInterpolator(r));
765 break;
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700766 }
Adam Cohen32a42f12010-08-11 19:34:30 -0700767 }
768
769 public void setXProgress(float r) {
770 // enforce r between 0 and 1
Adam Cohen3d07af02010-08-18 17:46:23 -0700771 r = Math.min(2.0f, r);
772 r = Math.max(-2.0f, r);
Adam Cohen32a42f12010-08-11 19:34:30 -0700773
774 mXProgress = r;
775
Adam Cohena02fdf12010-11-03 13:27:40 -0700776 if (mView == null) return;
Adam Cohen32a42f12010-08-11 19:34:30 -0700777 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
778 final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
779
Adam Cohen3d07af02010-08-18 17:46:23 -0700780 r *= 0.2f;
Adam Cohendfcdddd2010-09-10 14:38:40 -0700781 viewLp.setHorizontalOffset(Math.round(r * mSlideAmount));
782 highlightLp.setHorizontalOffset(Math.round(r * mSlideAmount));
Adam Cohen32a42f12010-08-11 19:34:30 -0700783 }
784
Adam Cohen3d07af02010-08-18 17:46:23 -0700785 void setMode(int mode) {
786 mMode = mode;
787 }
788
789 float getDurationForNeutralPosition() {
Adam Cohen839f4a52010-08-26 17:36:48 -0700790 return getDuration(false, 0);
Adam Cohen3d07af02010-08-18 17:46:23 -0700791 }
792
793 float getDurationForOffscreenPosition() {
Adam Cohen839f4a52010-08-26 17:36:48 -0700794 return getDuration(true, 0);
Adam Cohen3d07af02010-08-18 17:46:23 -0700795 }
796
Adam Cohen839f4a52010-08-26 17:36:48 -0700797 float getDurationForNeutralPosition(float velocity) {
798 return getDuration(false, velocity);
799 }
800
801 float getDurationForOffscreenPosition(float velocity) {
802 return getDuration(true, velocity);
803 }
804
805 private float getDuration(boolean invert, float velocity) {
Adam Cohen3d07af02010-08-18 17:46:23 -0700806 if (mView != null) {
807 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
808
Adam Cohen839f4a52010-08-26 17:36:48 -0700809 float d = (float) Math.sqrt(Math.pow(viewLp.horizontalOffset, 2) +
810 Math.pow(viewLp.verticalOffset, 2));
Adam Cohendfcdddd2010-09-10 14:38:40 -0700811 float maxd = (float) Math.sqrt(Math.pow(mSlideAmount, 2) +
812 Math.pow(0.4f * mSlideAmount, 2));
Adam Cohen839f4a52010-08-26 17:36:48 -0700813
814 if (velocity == 0) {
815 return (invert ? (1 - d / maxd) : d / maxd) * DEFAULT_ANIMATION_DURATION;
816 } else {
817 float duration = invert ? d / Math.abs(velocity) :
818 (maxd - d) / Math.abs(velocity);
819 if (duration < MINIMUM_ANIMATION_DURATION ||
820 duration > DEFAULT_ANIMATION_DURATION) {
821 return getDuration(invert, 0);
822 } else {
823 return duration;
824 }
825 }
Adam Cohen3d07af02010-08-18 17:46:23 -0700826 }
827 return 0;
828 }
829
Romain Guye5ebcb02010-10-15 13:57:28 -0700830 // Used for animations
831 @SuppressWarnings({"UnusedDeclaration"})
Adam Cohen839f4a52010-08-26 17:36:48 -0700832 public float getYProgress() {
Adam Cohen32a42f12010-08-11 19:34:30 -0700833 return mYProgress;
834 }
835
Romain Guye5ebcb02010-10-15 13:57:28 -0700836 // Used for animations
837 @SuppressWarnings({"UnusedDeclaration"})
Adam Cohen839f4a52010-08-26 17:36:48 -0700838 public float getXProgress() {
Adam Cohen32a42f12010-08-11 19:34:30 -0700839 return mXProgress;
840 }
Adam Cohen44729e32010-07-22 16:00:07 -0700841 }
842
843 @Override
844 public void onRemoteAdapterConnected() {
845 super.onRemoteAdapterConnected();
Adam Cohen1480fdd2010-08-25 17:24:53 -0700846 // On first run, we want to set the stack to the end.
Adam Cohen96d8d562010-10-24 11:12:18 -0700847 if (mWhichChild == -1) {
848 mWhichChild = 0;
Adam Cohen1480fdd2010-08-25 17:24:53 -0700849 }
Adam Cohen96d8d562010-10-24 11:12:18 -0700850 setDisplayedChild(mWhichChild);
Adam Cohen44729e32010-07-22 16:00:07 -0700851 }
Adam Cohen32a42f12010-08-11 19:34:30 -0700852
Adam Cohen9b073942010-08-19 16:49:52 -0700853 LayoutParams createOrReuseLayoutParams(View v) {
854 final ViewGroup.LayoutParams currentLp = v.getLayoutParams();
855 if (currentLp instanceof LayoutParams) {
856 LayoutParams lp = (LayoutParams) currentLp;
857 lp.setHorizontalOffset(0);
858 lp.setVerticalOffset(0);
Adam Cohen839f4a52010-08-26 17:36:48 -0700859 lp.width = 0;
860 lp.width = 0;
Adam Cohen9b073942010-08-19 16:49:52 -0700861 return lp;
862 }
863 return new LayoutParams(v);
Adam Cohen32a42f12010-08-11 19:34:30 -0700864 }
865
Adam Cohen9b073942010-08-19 16:49:52 -0700866 @Override
867 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
868 boolean dataChanged = mDataChanged;
869 if (dataChanged) {
870 handleDataChanged();
871
872 // if the data changes, mWhichChild might be out of the bounds of the adapter
873 // in this case, we reset mWhichChild to the beginning
874 if (mWhichChild >= mAdapter.getCount())
875 mWhichChild = 0;
876
877 showOnly(mWhichChild, true, true);
Winson Chung6364f2b2010-09-29 11:14:30 -0700878 refreshChildren();
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700879 }
880
Adam Cohen9b073942010-08-19 16:49:52 -0700881 final int childCount = getChildCount();
882 for (int i = 0; i < childCount; i++) {
883 final View child = getChildAt(i);
Adam Cohen32a42f12010-08-11 19:34:30 -0700884
Adam Cohen9b073942010-08-19 16:49:52 -0700885 int childRight = mPaddingLeft + child.getMeasuredWidth();
886 int childBottom = mPaddingTop + child.getMeasuredHeight();
887 LayoutParams lp = (LayoutParams) child.getLayoutParams();
Adam Cohen32a42f12010-08-11 19:34:30 -0700888
Adam Cohen9b073942010-08-19 16:49:52 -0700889 child.layout(mPaddingLeft + lp.horizontalOffset, mPaddingTop + lp.verticalOffset,
890 childRight + lp.horizontalOffset, childBottom + lp.verticalOffset);
891
Adam Cohen9b073942010-08-19 16:49:52 -0700892 }
893
894 mDataChanged = false;
895 onLayout();
Adam Cohen32a42f12010-08-11 19:34:30 -0700896 }
897
Adam Cohen839f4a52010-08-26 17:36:48 -0700898 private void measureChildren() {
899 final int count = getChildCount();
900 final int childWidth = mMeasuredWidth - mPaddingLeft - mPaddingRight;
901 final int childHeight = Math.round(mMeasuredHeight*(1-PERSPECTIVE_SHIFT_FACTOR))
902 - mPaddingTop - mPaddingBottom;
903
904 for (int i = 0; i < count; i++) {
905 final View child = getChildAt(i);
906 child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
907 MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
908 }
909 }
910
911 @Override
912 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
913 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
914 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
915 final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
916 final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
917
918 boolean haveChildRefSize = (mReferenceChildWidth != -1 && mReferenceChildHeight != -1);
919
920 // We need to deal with the case where our parent hasn't told us how
921 // big we should be. In this case we should
922 float factor = 1/(1 - PERSPECTIVE_SHIFT_FACTOR);
923 if (heightSpecMode == MeasureSpec.UNSPECIFIED) {
924 heightSpecSize = haveChildRefSize ?
925 Math.round(mReferenceChildHeight * (1 + factor)) +
926 mPaddingTop + mPaddingBottom : 0;
927 } else if (heightSpecMode == MeasureSpec.AT_MOST) {
928 heightSpecSize = haveChildRefSize ? Math.min(
929 Math.round(mReferenceChildHeight * (1 + factor)) + mPaddingTop +
930 mPaddingBottom, heightSpecSize) : 0;
931 }
932
933 if (widthSpecMode == MeasureSpec.UNSPECIFIED) {
934 widthSpecSize = haveChildRefSize ? mReferenceChildWidth + mPaddingLeft +
935 mPaddingRight : 0;
936 } else if (heightSpecMode == MeasureSpec.AT_MOST) {
937 widthSpecSize = haveChildRefSize ? Math.min(mReferenceChildWidth + mPaddingLeft +
938 mPaddingRight, widthSpecSize) : 0;
939 }
940
941 setMeasuredDimension(widthSpecSize, heightSpecSize);
942 measureChildren();
943 }
944
Adam Cohen9b073942010-08-19 16:49:52 -0700945 class LayoutParams extends ViewGroup.LayoutParams {
946 int horizontalOffset;
947 int verticalOffset;
948 View mView;
Adam Cohend51bbb52010-10-18 10:59:49 -0700949 private final Rect parentRect = new Rect();
950 private final Rect invalidateRect = new Rect();
951 private final RectF invalidateRectf = new RectF();
952 private final Rect globalInvalidateRect = new Rect();
Adam Cohen32a42f12010-08-11 19:34:30 -0700953
Adam Cohen9b073942010-08-19 16:49:52 -0700954 LayoutParams(View view) {
955 super(0, 0);
Adam Cohen839f4a52010-08-26 17:36:48 -0700956 width = 0;
957 height = 0;
Adam Cohen9b073942010-08-19 16:49:52 -0700958 horizontalOffset = 0;
959 verticalOffset = 0;
960 mView = view;
961 }
Adam Cohen32a42f12010-08-11 19:34:30 -0700962
Adam Cohen9b073942010-08-19 16:49:52 -0700963 LayoutParams(Context c, AttributeSet attrs) {
964 super(c, attrs);
965 horizontalOffset = 0;
966 verticalOffset = 0;
Adam Cohen839f4a52010-08-26 17:36:48 -0700967 width = 0;
968 height = 0;
Adam Cohen9b073942010-08-19 16:49:52 -0700969 }
Adam Cohen32a42f12010-08-11 19:34:30 -0700970
Adam Cohen9b073942010-08-19 16:49:52 -0700971 void invalidateGlobalRegion(View v, Rect r) {
Adam Cohend51bbb52010-10-18 10:59:49 -0700972 // We need to make a new rect here, so as not to modify the one passed
973 globalInvalidateRect.set(r);
Adam Cohen9b073942010-08-19 16:49:52 -0700974 View p = v;
975 if (!(v.getParent() != null && v.getParent() instanceof View)) return;
976
Adam Cohen9b073942010-08-19 16:49:52 -0700977 boolean firstPass = true;
978 parentRect.set(0, 0, 0, 0);
979 int depth = 0;
Adam Cohenb7f4d032010-09-16 15:25:54 -0700980 while (p.getParent() != null && p.getParent() instanceof View
Adam Cohend51bbb52010-10-18 10:59:49 -0700981 && !parentRect.contains(globalInvalidateRect)) {
Adam Cohen9b073942010-08-19 16:49:52 -0700982 if (!firstPass) {
Adam Cohend51bbb52010-10-18 10:59:49 -0700983 globalInvalidateRect.offset(p.getLeft() - p.getScrollX(), p.getTop()
984 - p.getScrollY());
Adam Cohen9b073942010-08-19 16:49:52 -0700985 depth++;
986 }
987 firstPass = false;
988 p = (View) p.getParent();
Adam Cohenb7f4d032010-09-16 15:25:54 -0700989 parentRect.set(p.getScrollX(), p.getScrollY(),
990 p.getWidth() + p.getScrollX(), p.getHeight() + p.getScrollY());
Adam Cohen839f4a52010-08-26 17:36:48 -0700991
Adam Cohen9b073942010-08-19 16:49:52 -0700992 }
993
Adam Cohend51bbb52010-10-18 10:59:49 -0700994 p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top,
995 globalInvalidateRect.right, globalInvalidateRect.bottom);
Adam Cohen9b073942010-08-19 16:49:52 -0700996 }
997
Adam Cohend51bbb52010-10-18 10:59:49 -0700998 Rect getInvalidateRect() {
999 return invalidateRect;
1000 }
1001
1002 void resetInvalidateRect() {
Adam Cohen0ac116b2010-11-11 11:39:53 -08001003 invalidateRect.set(0, 0, 0, 0);
Adam Cohend51bbb52010-10-18 10:59:49 -07001004 }
1005
Chet Haasea18a86b2010-09-07 13:20:00 -07001006 // This is public so that ObjectAnimator can access it
Adam Cohen9b073942010-08-19 16:49:52 -07001007 public void setVerticalOffset(int newVerticalOffset) {
Adam Cohen0ac116b2010-11-11 11:39:53 -08001008 setOffsets(horizontalOffset, newVerticalOffset);
1009 }
1010
1011 public void setHorizontalOffset(int newHorizontalOffset) {
1012 setOffsets(newHorizontalOffset, verticalOffset);
1013 }
1014
1015 public void setOffsets(int newHorizontalOffset, int newVerticalOffset) {
1016 int horizontalOffsetDelta = newHorizontalOffset - horizontalOffset;
1017 horizontalOffset = newHorizontalOffset;
1018 int verticalOffsetDelta = newVerticalOffset - verticalOffset;
Adam Cohen9b073942010-08-19 16:49:52 -07001019 verticalOffset = newVerticalOffset;
1020
1021 if (mView != null) {
1022 mView.requestLayout();
Adam Cohen0ac116b2010-11-11 11:39:53 -08001023 int left = Math.min(mView.getLeft() + horizontalOffsetDelta, mView.getLeft());
1024 int right = Math.max(mView.getRight() + horizontalOffsetDelta, mView.getRight());
1025 int top = Math.min(mView.getTop() + verticalOffsetDelta, mView.getTop());
1026 int bottom = Math.max(mView.getBottom() + verticalOffsetDelta, mView.getBottom());
Adam Cohen9b073942010-08-19 16:49:52 -07001027
Adam Cohen0ac116b2010-11-11 11:39:53 -08001028 invalidateRectf.set(left, top, right, bottom);
Adam Cohen9b073942010-08-19 16:49:52 -07001029
1030 float xoffset = -invalidateRectf.left;
1031 float yoffset = -invalidateRectf.top;
1032 invalidateRectf.offset(xoffset, yoffset);
1033 mView.getMatrix().mapRect(invalidateRectf);
1034 invalidateRectf.offset(-xoffset, -yoffset);
1035
Adam Cohen0ac116b2010-11-11 11:39:53 -08001036 invalidateRect.set((int) Math.floor(invalidateRectf.left),
Adam Cohen9b073942010-08-19 16:49:52 -07001037 (int) Math.floor(invalidateRectf.top),
1038 (int) Math.ceil(invalidateRectf.right),
1039 (int) Math.ceil(invalidateRectf.bottom));
1040
1041 invalidateGlobalRegion(mView, invalidateRect);
1042 }
1043 }
1044 }
1045
1046 private static class HolographicHelper {
1047 private final Paint mHolographicPaint = new Paint();
1048 private final Paint mErasePaint = new Paint();
Adam Cohen839f4a52010-08-26 17:36:48 -07001049 private final Paint mBlurPaint = new Paint();
Adam Cohen8baf5df2010-11-11 15:23:41 -08001050 private static final int RES_OUT = 0;
1051 private static final int CLICK_FEEDBACK = 1;
1052 private float mDensity;
Patrick Dubroye80202d2010-11-16 15:41:08 -08001053 private BlurMaskFilter mSmallBlurMaskFilter;
1054 private BlurMaskFilter mLargeBlurMaskFilter;
1055 private final Canvas mCanvas = new Canvas();
1056 private final Canvas mMaskCanvas = new Canvas();
1057 private final int[] mTmpXY = new int[2];
1058 private final Matrix mIdentityMatrix = new Matrix();
Adam Cohen9b073942010-08-19 16:49:52 -07001059
Adam Cohendfcdddd2010-09-10 14:38:40 -07001060 HolographicHelper(Context context) {
Adam Cohen8baf5df2010-11-11 15:23:41 -08001061 mDensity = context.getResources().getDisplayMetrics().density;
Adam Cohendfcdddd2010-09-10 14:38:40 -07001062
Adam Cohen9b073942010-08-19 16:49:52 -07001063 mHolographicPaint.setFilterBitmap(true);
Adam Cohen839f4a52010-08-26 17:36:48 -07001064 mHolographicPaint.setMaskFilter(TableMaskFilter.CreateClipTable(0, 30));
Adam Cohen9b073942010-08-19 16:49:52 -07001065 mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
1066 mErasePaint.setFilterBitmap(true);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001067
1068 mSmallBlurMaskFilter = new BlurMaskFilter(2 * mDensity, BlurMaskFilter.Blur.NORMAL);
1069 mLargeBlurMaskFilter = new BlurMaskFilter(4 * mDensity, BlurMaskFilter.Blur.NORMAL);
Adam Cohen9b073942010-08-19 16:49:52 -07001070 }
1071
1072 Bitmap createOutline(View v) {
Adam Cohen8baf5df2010-11-11 15:23:41 -08001073 return createOutline(v, RES_OUT);
1074 }
1075
1076 Bitmap createOutline(View v, int type) {
1077 if (type == RES_OUT) {
1078 mHolographicPaint.setColor(0xff6699ff);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001079 mBlurPaint.setMaskFilter(mSmallBlurMaskFilter);
Adam Cohen8baf5df2010-11-11 15:23:41 -08001080 } else if (type == CLICK_FEEDBACK) {
1081 mHolographicPaint.setColor(0x886699ff);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001082 mBlurPaint.setMaskFilter(mLargeBlurMaskFilter);
Adam Cohen8baf5df2010-11-11 15:23:41 -08001083 }
1084
Adam Cohen9b073942010-08-19 16:49:52 -07001085 if (v.getMeasuredWidth() == 0 || v.getMeasuredHeight() == 0) {
1086 return null;
1087 }
1088
1089 Bitmap bitmap = Bitmap.createBitmap(v.getMeasuredWidth(), v.getMeasuredHeight(),
1090 Bitmap.Config.ARGB_8888);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001091 mCanvas.setBitmap(bitmap);
Adam Cohen9b073942010-08-19 16:49:52 -07001092
1093 float rotationX = v.getRotationX();
Adam Cohen839f4a52010-08-26 17:36:48 -07001094 float rotation = v.getRotation();
1095 float translationY = v.getTranslationY();
Adam Cohen9b073942010-08-19 16:49:52 -07001096 v.setRotationX(0);
Adam Cohen839f4a52010-08-26 17:36:48 -07001097 v.setRotation(0);
1098 v.setTranslationY(0);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001099 v.draw(mCanvas);
Adam Cohen9b073942010-08-19 16:49:52 -07001100 v.setRotationX(rotationX);
Adam Cohen839f4a52010-08-26 17:36:48 -07001101 v.setRotation(rotation);
1102 v.setTranslationY(translationY);
Adam Cohen9b073942010-08-19 16:49:52 -07001103
Patrick Dubroye80202d2010-11-16 15:41:08 -08001104 drawOutline(mCanvas, bitmap);
Adam Cohen9b073942010-08-19 16:49:52 -07001105 return bitmap;
1106 }
1107
Adam Cohen9b073942010-08-19 16:49:52 -07001108 void drawOutline(Canvas dest, Bitmap src) {
Patrick Dubroye80202d2010-11-16 15:41:08 -08001109 final int[] xy = mTmpXY;
Adam Cohen839f4a52010-08-26 17:36:48 -07001110 Bitmap mask = src.extractAlpha(mBlurPaint, xy);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001111 mMaskCanvas.setBitmap(mask);
1112 mMaskCanvas.drawBitmap(src, -xy[0], -xy[1], mErasePaint);
Adam Cohen9b073942010-08-19 16:49:52 -07001113 dest.drawColor(0, PorterDuff.Mode.CLEAR);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001114 dest.setMatrix(mIdentityMatrix);
Adam Cohen839f4a52010-08-26 17:36:48 -07001115 dest.drawBitmap(mask, xy[0], xy[1], mHolographicPaint);
Adam Cohen9b073942010-08-19 16:49:52 -07001116 mask.recycle();
1117 }
Adam Cohen32a42f12010-08-11 19:34:30 -07001118 }
Adam Cohen44729e32010-07-22 16:00:07 -07001119}