blob: 21c61bdf67ab907f9a6f89c489695a926e5ec595 [file] [log] [blame]
Adam Cohenc99ff732011-01-16 16:11:41 -08001/* Copyright (C) 2010 The Android Open Source Project
Adam Cohen44729e32010-07-22 16:00:07 -07002 *
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16package android.widget;
17
Adam Cohenc0b53be2010-12-17 19:23:41 -080018import java.lang.ref.WeakReference;
19
Chet Haasea18a86b2010-09-07 13:20:00 -070020import android.animation.ObjectAnimator;
Romain Guye5ebcb02010-10-15 13:57:28 -070021import android.animation.PropertyValuesHolder;
Adam Cohen44729e32010-07-22 16:00:07 -070022import android.content.Context;
Adam Cohen32a42f12010-08-11 19:34:30 -070023import android.graphics.Bitmap;
Adam Cohen839f4a52010-08-26 17:36:48 -070024import android.graphics.BlurMaskFilter;
Adam Cohen32a42f12010-08-11 19:34:30 -070025import android.graphics.Canvas;
26import android.graphics.Matrix;
27import android.graphics.Paint;
28import android.graphics.PorterDuff;
29import android.graphics.PorterDuffXfermode;
Adam Cohen44729e32010-07-22 16:00:07 -070030import android.graphics.Rect;
Adam Cohen9b073942010-08-19 16:49:52 -070031import android.graphics.RectF;
Adam Cohend51bbb52010-10-18 10:59:49 -070032import android.graphics.Region;
Adam Cohen839f4a52010-08-26 17:36:48 -070033import android.graphics.TableMaskFilter;
Adam Cohen44729e32010-07-22 16:00:07 -070034import android.util.AttributeSet;
35import android.util.Log;
Adam Cohena8a7c922011-02-28 17:20:44 -080036import android.view.InputDevice;
Adam Cohen44729e32010-07-22 16:00:07 -070037import android.view.MotionEvent;
38import android.view.VelocityTracker;
39import android.view.View;
40import android.view.ViewConfiguration;
41import android.view.ViewGroup;
Adam Cohenb04f7ad2010-08-15 13:22:42 -070042import android.view.animation.LinearInterpolator;
Adam Cohen44729e32010-07-22 16:00:07 -070043import android.widget.RemoteViews.RemoteView;
44
45@RemoteView
46/**
47 * A view that displays its children in a stack and allows users to discretely swipe
48 * through the children.
49 */
50public class StackView extends AdapterViewAnimator {
51 private final String TAG = "StackView";
52
53 /**
54 * Default animation parameters
55 */
Romain Guye5ebcb02010-10-15 13:57:28 -070056 private static final int DEFAULT_ANIMATION_DURATION = 400;
Adam Cohen026e1212010-12-13 12:29:17 -080057 private static final int FADE_IN_ANIMATION_DURATION = 800;
Romain Guye5ebcb02010-10-15 13:57:28 -070058 private static final int MINIMUM_ANIMATION_DURATION = 50;
Adam Cohenc0b53be2010-12-17 19:23:41 -080059 private static final int STACK_RELAYOUT_DURATION = 100;
Adam Cohen44729e32010-07-22 16:00:07 -070060
61 /**
Adam Cohen839f4a52010-08-26 17:36:48 -070062 * Parameters effecting the perspective visuals
63 */
Adam Cohen026e1212010-12-13 12:29:17 -080064 private static final float PERSPECTIVE_SHIFT_FACTOR_Y = 0.1f;
65 private static final float PERSPECTIVE_SHIFT_FACTOR_X = 0.1f;
66
Adam Cohen36f43902010-12-15 21:17:33 -080067 private float mPerspectiveShiftX;
68 private float mPerspectiveShiftY;
69 private float mNewPerspectiveShiftX;
70 private float mNewPerspectiveShiftY;
71
Romain Guye5ebcb02010-10-15 13:57:28 -070072 @SuppressWarnings({"FieldCanBeLocal"})
Adam Cohen78db1aa2011-01-25 12:24:23 -080073 private static final float PERSPECTIVE_SCALE_FACTOR = 0f;
Adam Cohen839f4a52010-08-26 17:36:48 -070074
75 /**
76 * Represent the two possible stack modes, one where items slide up, and the other
77 * where items slide down. The perspective is also inverted between these two modes.
78 */
79 private static final int ITEMS_SLIDE_UP = 0;
80 private static final int ITEMS_SLIDE_DOWN = 1;
81
82 /**
Adam Cohen44729e32010-07-22 16:00:07 -070083 * These specify the different gesture states
84 */
Romain Guy5b53f912010-08-16 18:24:33 -070085 private static final int GESTURE_NONE = 0;
86 private static final int GESTURE_SLIDE_UP = 1;
87 private static final int GESTURE_SLIDE_DOWN = 2;
Adam Cohen44729e32010-07-22 16:00:07 -070088
89 /**
90 * Specifies how far you need to swipe (up or down) before it
91 * will be consider a completed gesture when you lift your finger
92 */
Adam Cohena9238c82010-10-25 14:01:29 -070093 private static final float SWIPE_THRESHOLD_RATIO = 0.2f;
94
95 /**
96 * Specifies the total distance, relative to the size of the stack,
97 * that views will be slid, either up or down
98 */
Romain Guy5b53f912010-08-16 18:24:33 -070099 private static final float SLIDE_UP_RATIO = 0.7f;
Adam Cohen44729e32010-07-22 16:00:07 -0700100
Adam Cohen44729e32010-07-22 16:00:07 -0700101 /**
102 * Sentinel value for no current active pointer.
103 * Used by {@link #mActivePointerId}.
104 */
105 private static final int INVALID_POINTER = -1;
106
107 /**
Adam Cohen839f4a52010-08-26 17:36:48 -0700108 * Number of active views in the stack. One fewer view is actually visible, as one is hidden.
109 */
110 private static final int NUM_ACTIVE_VIEWS = 5;
111
Adam Cohendfcdddd2010-09-10 14:38:40 -0700112 private static final int FRAME_PADDING = 4;
Adam Cohen839f4a52010-08-26 17:36:48 -0700113
Patrick Dubroye80202d2010-11-16 15:41:08 -0800114 private final Rect mTouchRect = new Rect();
115
Adam Cohen26e30bb2010-12-03 18:16:12 -0800116 private static final int MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE = 5000;
117
Adam Cohena8a7c922011-02-28 17:20:44 -0800118 private static long MIN_TIME_BETWEEN_SCROLLS = 100;
119
Adam Cohen839f4a52010-08-26 17:36:48 -0700120 /**
Adam Cohen44729e32010-07-22 16:00:07 -0700121 * These variables are all related to the current state of touch interaction
122 * with the stack
123 */
Adam Cohen44729e32010-07-22 16:00:07 -0700124 private float mInitialY;
125 private float mInitialX;
126 private int mActivePointerId;
Adam Cohen44729e32010-07-22 16:00:07 -0700127 private int mYVelocity = 0;
128 private int mSwipeGestureType = GESTURE_NONE;
Adam Cohendfcdddd2010-09-10 14:38:40 -0700129 private int mSlideAmount;
Adam Cohen44729e32010-07-22 16:00:07 -0700130 private int mSwipeThreshold;
131 private int mTouchSlop;
132 private int mMaximumVelocity;
133 private VelocityTracker mVelocityTracker;
Adam Cohen3352b682010-10-26 13:54:00 -0700134 private boolean mTransitionIsSetup = false;
Adam Cohen44729e32010-07-22 16:00:07 -0700135
Adam Cohen9b073942010-08-19 16:49:52 -0700136 private static HolographicHelper sHolographicHelper;
Adam Cohen32a42f12010-08-11 19:34:30 -0700137 private ImageView mHighlight;
Adam Cohen8baf5df2010-11-11 15:23:41 -0800138 private ImageView mClickFeedback;
139 private boolean mClickFeedbackIsValid = false;
Adam Cohen32a42f12010-08-11 19:34:30 -0700140 private StackSlider mStackSlider;
Adam Cohen44729e32010-07-22 16:00:07 -0700141 private boolean mFirstLayoutHappened = false;
Adam Cohen26e30bb2010-12-03 18:16:12 -0800142 private long mLastInteractionTime = 0;
Adam Cohena8a7c922011-02-28 17:20:44 -0800143 private long mLastScrollTime;
Adam Cohen839f4a52010-08-26 17:36:48 -0700144 private int mStackMode;
Adam Cohendfcdddd2010-09-10 14:38:40 -0700145 private int mFramePadding;
Adam Cohen0ac116b2010-11-11 11:39:53 -0800146 private final Rect stackInvalidateRect = new Rect();
Adam Cohen44729e32010-07-22 16:00:07 -0700147
Adam Cohen44729e32010-07-22 16:00:07 -0700148 public StackView(Context context) {
149 super(context);
150 initStackView();
151 }
152
153 public StackView(Context context, AttributeSet attrs) {
154 super(context, attrs);
155 initStackView();
156 }
157
158 private void initStackView() {
Adam Cohen96d8d562010-10-24 11:12:18 -0700159 configureViewAnimator(NUM_ACTIVE_VIEWS, 1);
Adam Cohen44729e32010-07-22 16:00:07 -0700160 setStaticTransformationsEnabled(true);
161 final ViewConfiguration configuration = ViewConfiguration.get(getContext());
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700162 mTouchSlop = configuration.getScaledTouchSlop();
Adam Cohen44729e32010-07-22 16:00:07 -0700163 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
164 mActivePointerId = INVALID_POINTER;
Adam Cohen32a42f12010-08-11 19:34:30 -0700165
166 mHighlight = new ImageView(getContext());
167 mHighlight.setLayoutParams(new LayoutParams(mHighlight));
168 addViewInLayout(mHighlight, -1, new LayoutParams(mHighlight));
Adam Cohen8baf5df2010-11-11 15:23:41 -0800169
170 mClickFeedback = new ImageView(getContext());
171 mClickFeedback.setLayoutParams(new LayoutParams(mClickFeedback));
172 addViewInLayout(mClickFeedback, -1, new LayoutParams(mClickFeedback));
173 mClickFeedback.setVisibility(INVISIBLE);
174
Adam Cohen32a42f12010-08-11 19:34:30 -0700175 mStackSlider = new StackSlider();
176
Adam Cohen9b073942010-08-19 16:49:52 -0700177 if (sHolographicHelper == null) {
Adam Cohendfcdddd2010-09-10 14:38:40 -0700178 sHolographicHelper = new HolographicHelper(mContext);
Adam Cohen32a42f12010-08-11 19:34:30 -0700179 }
Adam Cohen9b073942010-08-19 16:49:52 -0700180 setClipChildren(false);
181 setClipToPadding(false);
Adam Cohen1480fdd2010-08-25 17:24:53 -0700182
Adam Cohen839f4a52010-08-26 17:36:48 -0700183 // This sets the form of the StackView, which is currently to have the perspective-shifted
184 // views above the active view, and have items slide down when sliding out. The opposite is
185 // available by using ITEMS_SLIDE_UP.
186 mStackMode = ITEMS_SLIDE_DOWN;
187
Adam Cohen1480fdd2010-08-25 17:24:53 -0700188 // This is a flag to indicate the the stack is loading for the first time
189 mWhichChild = -1;
Adam Cohendfcdddd2010-09-10 14:38:40 -0700190
191 // Adjust the frame padding based on the density, since the highlight changes based
192 // on the density
193 final float density = mContext.getResources().getDisplayMetrics().density;
194 mFramePadding = (int) Math.ceil(density * FRAME_PADDING);
Adam Cohen44729e32010-07-22 16:00:07 -0700195 }
196
197 /**
198 * Animate the views between different relative indexes within the {@link AdapterViewAnimator}
199 */
Adam Cohen78db1aa2011-01-25 12:24:23 -0800200 void transformViewForTransition(int fromIndex, int toIndex, final View view, boolean animate) {
Adam Cohen69d66e02011-01-12 14:39:13 -0800201 ObjectAnimator alphaOa = null;
202 ObjectAnimator oldAlphaOa = null;
203
Adam Cohen78db1aa2011-01-25 12:24:23 -0800204 if (!animate) {
205 ((StackFrame) view).cancelSliderAnimator();
206 view.setRotationX(0f);
207 LayoutParams lp = (LayoutParams) view.getLayoutParams();
208 lp.setVerticalOffset(0);
209 lp.setHorizontalOffset(0);
Adam Cohen69d66e02011-01-12 14:39:13 -0800210 }
211
Adam Cohen50204582011-01-16 17:28:25 -0800212 if (fromIndex == -1 && toIndex == getNumActiveViews() -1) {
Adam Cohen44729e32010-07-22 16:00:07 -0700213 // Fade item in
214 if (view.getAlpha() == 1) {
215 view.setAlpha(0);
216 }
Adam Cohen50204582011-01-16 17:28:25 -0800217 transformViewAtIndex(toIndex, view, false);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700218 view.setVisibility(VISIBLE);
219
Adam Cohen78db1aa2011-01-25 12:24:23 -0800220 ((StackFrame) view).cancelAlphaAnimator();
221 if (animate) {
222 alphaOa = ObjectAnimator.ofFloat(view, "alpha", view.getAlpha(), 1.0f);
223 alphaOa.setDuration(FADE_IN_ANIMATION_DURATION);
224 ((StackFrame) view).setAlphaAnimator(alphaOa);
225 alphaOa.start();
226 } else {
227 view.setAlpha(1.0f);
228 }
Adam Cohen96d8d562010-10-24 11:12:18 -0700229 } else if (fromIndex == 0 && toIndex == 1) {
Adam Cohen44729e32010-07-22 16:00:07 -0700230 // Slide item in
Adam Cohen78db1aa2011-01-25 12:24:23 -0800231 ((StackFrame) view).cancelSliderAnimator();
Adam Cohen44729e32010-07-22 16:00:07 -0700232 view.setVisibility(VISIBLE);
Adam Cohen32a42f12010-08-11 19:34:30 -0700233
Adam Cohen839f4a52010-08-26 17:36:48 -0700234 int duration = Math.round(mStackSlider.getDurationForNeutralPosition(mYVelocity));
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700235 StackSlider animationSlider = new StackSlider(mStackSlider);
Adam Cohen69d66e02011-01-12 14:39:13 -0800236 animationSlider.setView(view);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800237
238 if (animate) {
239 PropertyValuesHolder slideInY = PropertyValuesHolder.ofFloat("YProgress", 0.0f);
240 PropertyValuesHolder slideInX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
241 ObjectAnimator slideIn = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
242 slideInX, slideInY);
243 slideIn.setDuration(duration);
244 slideIn.setInterpolator(new LinearInterpolator());
245 ((StackFrame) view).setSliderAnimator(slideIn);
246 slideIn.start();
247 } else {
248 animationSlider.setYProgress(0f);
249 animationSlider.setXProgress(0f);
250 }
Adam Cohen96d8d562010-10-24 11:12:18 -0700251 } else if (fromIndex == 1 && toIndex == 0) {
Adam Cohen44729e32010-07-22 16:00:07 -0700252 // Slide item out
Adam Cohen78db1aa2011-01-25 12:24:23 -0800253 ((StackFrame) view).cancelSliderAnimator();
Adam Cohen839f4a52010-08-26 17:36:48 -0700254 int duration = Math.round(mStackSlider.getDurationForOffscreenPosition(mYVelocity));
Adam Cohen44729e32010-07-22 16:00:07 -0700255
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700256 StackSlider animationSlider = new StackSlider(mStackSlider);
Adam Cohen69d66e02011-01-12 14:39:13 -0800257 animationSlider.setView(view);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800258 if (animate) {
259 PropertyValuesHolder slideOutY = PropertyValuesHolder.ofFloat("YProgress", 1.0f);
260 PropertyValuesHolder slideOutX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
261 ObjectAnimator slideOut = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
262 slideOutX, slideOutY);
263 slideOut.setDuration(duration);
264 slideOut.setInterpolator(new LinearInterpolator());
265 ((StackFrame) view).setSliderAnimator(slideOut);
266 slideOut.start();
267 } else {
268 animationSlider.setYProgress(1.0f);
269 animationSlider.setXProgress(0f);
270 }
Adam Cohen69d66e02011-01-12 14:39:13 -0800271 } else if (toIndex == 0) {
Adam Cohen44729e32010-07-22 16:00:07 -0700272 // Make sure this view that is "waiting in the wings" is invisible
273 view.setAlpha(0.0f);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700274 view.setVisibility(INVISIBLE);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800275 } else if ((fromIndex == 0 || fromIndex == 1) && toIndex > 1) {
Adam Cohen69d66e02011-01-12 14:39:13 -0800276 view.setVisibility(VISIBLE);
277 view.setAlpha(1.0f);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800278 view.setRotationX(0f);
279 LayoutParams lp = (LayoutParams) view.getLayoutParams();
280 lp.setVerticalOffset(0);
281 lp.setHorizontalOffset(0);
Adam Cohenc0b53be2010-12-17 19:23:41 -0800282 } else if (fromIndex == -1) {
283 view.setAlpha(1.0f);
284 view.setVisibility(VISIBLE);
Adam Cohen44729e32010-07-22 16:00:07 -0700285 } else if (toIndex == -1) {
286 // Fade item out
Adam Cohen78db1aa2011-01-25 12:24:23 -0800287 ((StackFrame) view).cancelAlphaAnimator();
288 if (animate) {
289 alphaOa = ObjectAnimator.ofFloat(view, "alpha", view.getAlpha(), 0.0f);
290 alphaOa.setDuration(STACK_RELAYOUT_DURATION);
291 ((StackFrame) view).setAlphaAnimator(alphaOa);
292 alphaOa.start();
293 } else {
294 view.setAlpha(0f);
295 }
Adam Cohen44729e32010-07-22 16:00:07 -0700296 }
Adam Cohen839f4a52010-08-26 17:36:48 -0700297
298 // Implement the faked perspective
299 if (toIndex != -1) {
Adam Cohen78db1aa2011-01-25 12:24:23 -0800300 transformViewAtIndex(toIndex, view, animate);
Adam Cohenf04e2252010-09-09 18:41:20 -0700301 }
302 }
Adam Cohen839f4a52010-08-26 17:36:48 -0700303
Adam Cohen36f43902010-12-15 21:17:33 -0800304 private void transformViewAtIndex(int index, final View view, boolean animate) {
305 final float maxPerspectiveShiftY = mPerspectiveShiftY;
306 final float maxPerspectiveShiftX = mPerspectiveShiftX;
Adam Cohen026e1212010-12-13 12:29:17 -0800307
Adam Cohenc99ff732011-01-16 16:11:41 -0800308 if (mStackMode == ITEMS_SLIDE_DOWN) {
309 index = mMaxNumActiveViews - index - 1;
310 if (index == mMaxNumActiveViews - 1) index--;
311 } else {
312 index--;
313 if (index < 0) index++;
314 }
Adam Cohen026e1212010-12-13 12:29:17 -0800315
316 float r = (index * 1.0f) / (mMaxNumActiveViews - 2);
317
Adam Cohen36f43902010-12-15 21:17:33 -0800318 final float scale = 1 - PERSPECTIVE_SCALE_FACTOR * (1 - r);
Adam Cohen026e1212010-12-13 12:29:17 -0800319
Adam Cohenc99ff732011-01-16 16:11:41 -0800320 float perspectiveTranslationY = r * maxPerspectiveShiftY;
321 float scaleShiftCorrectionY = (scale - 1) *
Adam Cohen026e1212010-12-13 12:29:17 -0800322 (getMeasuredHeight() * (1 - PERSPECTIVE_SHIFT_FACTOR_Y) / 2.0f);
Adam Cohen36f43902010-12-15 21:17:33 -0800323 final float transY = perspectiveTranslationY + scaleShiftCorrectionY;
Adam Cohen026e1212010-12-13 12:29:17 -0800324
325 float perspectiveTranslationX = (1 - r) * maxPerspectiveShiftX;
326 float scaleShiftCorrectionX = (1 - scale) *
327 (getMeasuredWidth() * (1 - PERSPECTIVE_SHIFT_FACTOR_X) / 2.0f);
Adam Cohen36f43902010-12-15 21:17:33 -0800328 final float transX = perspectiveTranslationX + scaleShiftCorrectionX;
Adam Cohen026e1212010-12-13 12:29:17 -0800329
Adam Cohenc99ff732011-01-16 16:11:41 -0800330 // If this view is currently being animated for a certain position, we need to cancel
Adam Cohen69d66e02011-01-12 14:39:13 -0800331 // this animation so as not to interfere with the new transformation.
Adam Cohen78db1aa2011-01-25 12:24:23 -0800332 if (view instanceof StackFrame) {
333 ((StackFrame) view).cancelTransformAnimator();
Adam Cohen69d66e02011-01-12 14:39:13 -0800334 }
335
Adam Cohen36f43902010-12-15 21:17:33 -0800336 if (animate) {
337 PropertyValuesHolder translationX = PropertyValuesHolder.ofFloat("translationX", transX);
338 PropertyValuesHolder translationY = PropertyValuesHolder.ofFloat("translationY", transY);
339 PropertyValuesHolder scalePropX = PropertyValuesHolder.ofFloat("scaleX", scale);
340 PropertyValuesHolder scalePropY = PropertyValuesHolder.ofFloat("scaleY", scale);
Adam Cohen026e1212010-12-13 12:29:17 -0800341
Adam Cohen36f43902010-12-15 21:17:33 -0800342 ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(view, scalePropX, scalePropY,
343 translationY, translationX);
Adam Cohenc0b53be2010-12-17 19:23:41 -0800344 oa.setDuration(STACK_RELAYOUT_DURATION);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800345 if (view instanceof StackFrame) {
346 ((StackFrame) view).setTransformAnimator(oa);
347 }
Adam Cohen36f43902010-12-15 21:17:33 -0800348 oa.start();
349 } else {
Adam Cohen36f43902010-12-15 21:17:33 -0800350 view.setTranslationX(transX);
351 view.setTranslationY(transY);
352 view.setScaleX(scale);
353 view.setScaleY(scale);
354 }
Adam Cohen026e1212010-12-13 12:29:17 -0800355 }
356
Adam Cohen3352b682010-10-26 13:54:00 -0700357 private void setupStackSlider(View v, int mode) {
358 mStackSlider.setMode(mode);
359 if (v != null) {
360 mHighlight.setImageBitmap(sHolographicHelper.createOutline(v));
361 mHighlight.setRotation(v.getRotation());
362 mHighlight.setTranslationY(v.getTranslationY());
Adam Cohen026e1212010-12-13 12:29:17 -0800363 mHighlight.setTranslationX(v.getTranslationX());
Adam Cohen3352b682010-10-26 13:54:00 -0700364 mHighlight.bringToFront();
365 v.bringToFront();
366 mStackSlider.setView(v);
367
368 v.setVisibility(VISIBLE);
369 }
370 }
371
Adam Cohen0e2de6d2011-01-19 17:16:34 -0800372 /**
373 * {@inheritDoc}
374 */
Adam Cohen3352b682010-10-26 13:54:00 -0700375 @Override
376 @android.view.RemotableViewMethod
377 public void showNext() {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800378 if (mSwipeGestureType != GESTURE_NONE) return;
Adam Cohen3352b682010-10-26 13:54:00 -0700379 if (!mTransitionIsSetup) {
380 View v = getViewAtRelativeIndex(1);
381 if (v != null) {
382 setupStackSlider(v, StackSlider.NORMAL_MODE);
383 mStackSlider.setYProgress(0);
384 mStackSlider.setXProgress(0);
385 }
386 }
387 super.showNext();
388 }
389
Adam Cohen0e2de6d2011-01-19 17:16:34 -0800390 /**
391 * {@inheritDoc}
392 */
Adam Cohen3352b682010-10-26 13:54:00 -0700393 @Override
394 @android.view.RemotableViewMethod
395 public void showPrevious() {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800396 if (mSwipeGestureType != GESTURE_NONE) return;
Adam Cohen3352b682010-10-26 13:54:00 -0700397 if (!mTransitionIsSetup) {
398 View v = getViewAtRelativeIndex(0);
399 if (v != null) {
400 setupStackSlider(v, StackSlider.NORMAL_MODE);
401 mStackSlider.setYProgress(1);
402 mStackSlider.setXProgress(0);
403 }
404 }
405 super.showPrevious();
406 }
407
Adam Cohen96d8d562010-10-24 11:12:18 -0700408 @Override
Adam Cohenef17dd42011-01-20 17:20:57 -0800409 void showOnly(int childIndex, boolean animate) {
410 super.showOnly(childIndex, animate);
Adam Cohen96d8d562010-10-24 11:12:18 -0700411
412 // Here we need to make sure that the z-order of the children is correct
Adam Cohen3352b682010-10-26 13:54:00 -0700413 for (int i = mCurrentWindowEnd; i >= mCurrentWindowStart; i--) {
Adam Cohen96d8d562010-10-24 11:12:18 -0700414 int index = modulo(i, getWindowSize());
Adam Cohen0ac116b2010-11-11 11:39:53 -0800415 ViewAndIndex vi = mViewsMap.get(index);
416 if (vi != null) {
417 View v = mViewsMap.get(index).view;
418 if (v != null) v.bringToFront();
419 }
Adam Cohen96d8d562010-10-24 11:12:18 -0700420 }
Adam Cohen78db1aa2011-01-25 12:24:23 -0800421 if (mHighlight != null) {
422 mHighlight.bringToFront();
423 }
Adam Cohen3352b682010-10-26 13:54:00 -0700424 mTransitionIsSetup = false;
Adam Cohen8baf5df2010-11-11 15:23:41 -0800425 mClickFeedbackIsValid = false;
426 }
427
428 void updateClickFeedback() {
429 if (!mClickFeedbackIsValid) {
Adam Cohen9c295482010-11-18 15:19:48 -0800430 View v = getViewAtRelativeIndex(1);
Adam Cohen8baf5df2010-11-11 15:23:41 -0800431 if (v != null) {
432 mClickFeedback.setImageBitmap(sHolographicHelper.createOutline(v,
433 HolographicHelper.CLICK_FEEDBACK));
434 mClickFeedback.setTranslationX(v.getTranslationX());
435 mClickFeedback.setTranslationY(v.getTranslationY());
436 }
437 mClickFeedbackIsValid = true;
438 }
439 }
440
441 @Override
442 void showTapFeedback(View v) {
443 updateClickFeedback();
444 mClickFeedback.setVisibility(VISIBLE);
445 mClickFeedback.bringToFront();
446 invalidate();
447 }
448
449 @Override
450 void hideTapFeedback(View v) {
451 mClickFeedback.setVisibility(INVISIBLE);
452 invalidate();
Adam Cohen96d8d562010-10-24 11:12:18 -0700453 }
454
Adam Cohenf04e2252010-09-09 18:41:20 -0700455 private void updateChildTransforms() {
Adam Cohen96d8d562010-10-24 11:12:18 -0700456 for (int i = 0; i < getNumActiveViews(); i++) {
Adam Cohenf04e2252010-09-09 18:41:20 -0700457 View v = getViewAtRelativeIndex(i);
458 if (v != null) {
Adam Cohen36f43902010-12-15 21:17:33 -0800459 transformViewAtIndex(i, v, false);
Adam Cohenf04e2252010-09-09 18:41:20 -0700460 }
Adam Cohen839f4a52010-08-26 17:36:48 -0700461 }
Adam Cohen44729e32010-07-22 16:00:07 -0700462 }
463
Adam Cohen78db1aa2011-01-25 12:24:23 -0800464 private static class StackFrame extends FrameLayout {
465 WeakReference<ObjectAnimator> alphaAnimator;
466 WeakReference<ObjectAnimator> transformAnimator;
467 WeakReference<ObjectAnimator> sliderAnimator;
468
469 public StackFrame(Context context) {
470 super(context);
471 }
472
473 void setAlphaAnimator(ObjectAnimator oa) {
474 alphaAnimator = new WeakReference<ObjectAnimator>(oa);
475 }
476
477 void setTransformAnimator(ObjectAnimator oa) {
478 transformAnimator = new WeakReference<ObjectAnimator>(oa);
479 }
480
481 void setSliderAnimator(ObjectAnimator oa) {
482 sliderAnimator = new WeakReference<ObjectAnimator>(oa);
483 }
484
485 boolean cancelAlphaAnimator() {
486 if (alphaAnimator != null) {
487 ObjectAnimator oa = alphaAnimator.get();
488 if (oa != null) {
489 oa.cancel();
490 return true;
491 }
492 }
493 return false;
494 }
495
496 boolean cancelTransformAnimator() {
497 if (transformAnimator != null) {
498 ObjectAnimator oa = transformAnimator.get();
499 if (oa != null) {
500 oa.cancel();
501 return true;
502 }
503 }
504 return false;
505 }
506
507 boolean cancelSliderAnimator() {
508 if (sliderAnimator != null) {
509 ObjectAnimator oa = sliderAnimator.get();
510 if (oa != null) {
511 oa.cancel();
512 return true;
513 }
514 }
515 return false;
516 }
517 }
518
Adam Cohendfcdddd2010-09-10 14:38:40 -0700519 @Override
520 FrameLayout getFrameForChild() {
Adam Cohen78db1aa2011-01-25 12:24:23 -0800521 StackFrame fl = new StackFrame(mContext);
Adam Cohendfcdddd2010-09-10 14:38:40 -0700522 fl.setPadding(mFramePadding, mFramePadding, mFramePadding, mFramePadding);
523 return fl;
524 }
525
Adam Cohen44729e32010-07-22 16:00:07 -0700526 /**
527 * Apply any necessary tranforms for the child that is being added.
528 */
529 void applyTransformForChildAtIndex(View child, int relativeIndex) {
Adam Cohen44729e32010-07-22 16:00:07 -0700530 }
531
532 @Override
Adam Cohen9b073942010-08-19 16:49:52 -0700533 protected void dispatchDraw(Canvas canvas) {
Adam Cohen321aa2b2011-03-08 13:33:29 -0800534 boolean expandClipRegion = false;
535
Adam Cohen0ac116b2010-11-11 11:39:53 -0800536 canvas.getClipBounds(stackInvalidateRect);
Adam Cohend51bbb52010-10-18 10:59:49 -0700537 final int childCount = getChildCount();
538 for (int i = 0; i < childCount; i++) {
Adam Cohene86ff4d2011-01-21 17:46:11 -0800539 final View child = getChildAt(i);
540 LayoutParams lp = (LayoutParams) child.getLayoutParams();
541 if ((lp.horizontalOffset == 0 && lp.verticalOffset == 0) ||
542 child.getAlpha() == 0f || child.getVisibility() != VISIBLE) {
543 lp.resetInvalidateRect();
544 }
Adam Cohen321aa2b2011-03-08 13:33:29 -0800545 Rect childInvalidateRect = lp.getInvalidateRect();
546 if (!childInvalidateRect.isEmpty()) {
547 expandClipRegion = true;
548 stackInvalidateRect.union(childInvalidateRect);
549 }
Adam Cohen44729e32010-07-22 16:00:07 -0700550 }
Adam Cohen321aa2b2011-03-08 13:33:29 -0800551
552 // We only expand the clip bounds if necessary.
553 if (expandClipRegion) {
554 canvas.save(Canvas.CLIP_SAVE_FLAG);
555 canvas.clipRect(stackInvalidateRect, Region.Op.UNION);
556 super.dispatchDraw(canvas);
557 canvas.restore();
558 } else {
559 super.dispatchDraw(canvas);
560 }
Adam Cohen9b073942010-08-19 16:49:52 -0700561 }
Adam Cohen44729e32010-07-22 16:00:07 -0700562
Adam Cohen9b073942010-08-19 16:49:52 -0700563 private void onLayout() {
Adam Cohen44729e32010-07-22 16:00:07 -0700564 if (!mFirstLayoutHappened) {
Adam Cohen44729e32010-07-22 16:00:07 -0700565 mFirstLayoutHappened = true;
Adam Cohen53838d22011-01-26 21:32:33 -0800566 updateChildTransforms();
Adam Cohen44729e32010-07-22 16:00:07 -0700567 }
Adam Cohen36f43902010-12-15 21:17:33 -0800568
Adam Cohenc6a47162011-01-27 18:05:02 -0800569 final int newSlideAmount = Math.round(SLIDE_UP_RATIO * getMeasuredHeight());
570 if (mSlideAmount != newSlideAmount) {
571 mSlideAmount = newSlideAmount;
572 mSwipeThreshold = Math.round(SWIPE_THRESHOLD_RATIO * newSlideAmount);
573 }
574
Adam Cohen36f43902010-12-15 21:17:33 -0800575 if (Float.compare(mPerspectiveShiftY, mNewPerspectiveShiftY) != 0 ||
576 Float.compare(mPerspectiveShiftX, mNewPerspectiveShiftX) != 0) {
Adam Cohen78db1aa2011-01-25 12:24:23 -0800577
Adam Cohen36f43902010-12-15 21:17:33 -0800578 mPerspectiveShiftY = mNewPerspectiveShiftY;
579 mPerspectiveShiftX = mNewPerspectiveShiftX;
Adam Cohen53838d22011-01-26 21:32:33 -0800580 updateChildTransforms();
Adam Cohen36f43902010-12-15 21:17:33 -0800581 }
Adam Cohen44729e32010-07-22 16:00:07 -0700582 }
583
Adam Cohena8a7c922011-02-28 17:20:44 -0800584 @Override
585 public boolean onGenericMotionEvent(MotionEvent event) {
586 if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
587 switch (event.getAction()) {
588 case MotionEvent.ACTION_SCROLL: {
589 final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
590 if (vscroll < 0) {
591 pacedScroll(false);
592 return true;
593 } else if (vscroll > 0) {
594 pacedScroll(true);
595 return true;
596 }
597 }
598 }
599 }
600 return super.onGenericMotionEvent(event);
601 }
602
603 // This ensures that the frequency of stack flips caused by scrolls is capped
604 private void pacedScroll(boolean up) {
605 long timeSinceLastScroll = System.currentTimeMillis() - mLastScrollTime;
606 if (timeSinceLastScroll > MIN_TIME_BETWEEN_SCROLLS) {
607 if (up) {
608 showPrevious();
609 } else {
610 showNext();
611 }
612 mLastScrollTime = System.currentTimeMillis();
613 }
614 }
615
Adam Cohen0e2de6d2011-01-19 17:16:34 -0800616 /**
617 * {@inheritDoc}
618 */
Adam Cohen44729e32010-07-22 16:00:07 -0700619 @Override
620 public boolean onInterceptTouchEvent(MotionEvent ev) {
621 int action = ev.getAction();
622 switch(action & MotionEvent.ACTION_MASK) {
Adam Cohen44729e32010-07-22 16:00:07 -0700623 case MotionEvent.ACTION_DOWN: {
624 if (mActivePointerId == INVALID_POINTER) {
625 mInitialX = ev.getX();
626 mInitialY = ev.getY();
627 mActivePointerId = ev.getPointerId(0);
628 }
629 break;
630 }
631 case MotionEvent.ACTION_MOVE: {
632 int pointerIndex = ev.findPointerIndex(mActivePointerId);
633 if (pointerIndex == INVALID_POINTER) {
634 // no data for our primary pointer, this shouldn't happen, log it
635 Log.d(TAG, "Error: No data for our primary pointer.");
636 return false;
637 }
Adam Cohen44729e32010-07-22 16:00:07 -0700638 float newY = ev.getY(pointerIndex);
639 float deltaY = newY - mInitialY;
640
Adam Cohen32a42f12010-08-11 19:34:30 -0700641 beginGestureIfNeeded(deltaY);
Adam Cohen44729e32010-07-22 16:00:07 -0700642 break;
643 }
644 case MotionEvent.ACTION_POINTER_UP: {
645 onSecondaryPointerUp(ev);
646 break;
647 }
648 case MotionEvent.ACTION_UP:
649 case MotionEvent.ACTION_CANCEL: {
650 mActivePointerId = INVALID_POINTER;
651 mSwipeGestureType = GESTURE_NONE;
Adam Cohen44729e32010-07-22 16:00:07 -0700652 }
653 }
654
655 return mSwipeGestureType != GESTURE_NONE;
656 }
657
Adam Cohen32a42f12010-08-11 19:34:30 -0700658 private void beginGestureIfNeeded(float deltaY) {
659 if ((int) Math.abs(deltaY) > mTouchSlop && mSwipeGestureType == GESTURE_NONE) {
Adam Cohenc99ff732011-01-16 16:11:41 -0800660 final int swipeGestureType = deltaY < 0 ? GESTURE_SLIDE_UP : GESTURE_SLIDE_DOWN;
Adam Cohen32a42f12010-08-11 19:34:30 -0700661 cancelLongPress();
662 requestDisallowInterceptTouchEvent(true);
663
Adam Cohend51bbb52010-10-18 10:59:49 -0700664 if (mAdapter == null) return;
Adam Cohenef17dd42011-01-20 17:20:57 -0800665 final int adapterCount = getCount();
Adam Cohend51bbb52010-10-18 10:59:49 -0700666
Adam Cohen839f4a52010-08-26 17:36:48 -0700667 int activeIndex;
668 if (mStackMode == ITEMS_SLIDE_UP) {
Adam Cohen96d8d562010-10-24 11:12:18 -0700669 activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1;
Adam Cohen839f4a52010-08-26 17:36:48 -0700670 } else {
Adam Cohen96d8d562010-10-24 11:12:18 -0700671 activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 1 : 0;
Adam Cohen839f4a52010-08-26 17:36:48 -0700672 }
Adam Cohen32a42f12010-08-11 19:34:30 -0700673
Adam Cohenc99ff732011-01-16 16:11:41 -0800674 boolean endOfStack = mLoopViews && adapterCount == 1 &&
675 ((mStackMode == ITEMS_SLIDE_UP && swipeGestureType == GESTURE_SLIDE_UP) ||
676 (mStackMode == ITEMS_SLIDE_DOWN && swipeGestureType == GESTURE_SLIDE_DOWN));
677 boolean beginningOfStack = mLoopViews && adapterCount == 1 &&
678 ((mStackMode == ITEMS_SLIDE_DOWN && swipeGestureType == GESTURE_SLIDE_UP) ||
679 (mStackMode == ITEMS_SLIDE_UP && swipeGestureType == GESTURE_SLIDE_DOWN));
680
Adam Cohen3352b682010-10-26 13:54:00 -0700681 int stackMode;
Adam Cohenc99ff732011-01-16 16:11:41 -0800682 if (mLoopViews && !beginningOfStack && !endOfStack) {
Adam Cohen3352b682010-10-26 13:54:00 -0700683 stackMode = StackSlider.NORMAL_MODE;
Adam Cohenc99ff732011-01-16 16:11:41 -0800684 } else if (mCurrentWindowStartUnbounded + activeIndex == -1 || beginningOfStack) {
Adam Cohen96d8d562010-10-24 11:12:18 -0700685 activeIndex++;
Adam Cohen3352b682010-10-26 13:54:00 -0700686 stackMode = StackSlider.BEGINNING_OF_STACK_MODE;
Adam Cohenc99ff732011-01-16 16:11:41 -0800687 } else if (mCurrentWindowStartUnbounded + activeIndex == adapterCount - 1 || endOfStack) {
Adam Cohen3352b682010-10-26 13:54:00 -0700688 stackMode = StackSlider.END_OF_STACK_MODE;
Adam Cohen3d07af02010-08-18 17:46:23 -0700689 } else {
Adam Cohen3352b682010-10-26 13:54:00 -0700690 stackMode = StackSlider.NORMAL_MODE;
Adam Cohen32a42f12010-08-11 19:34:30 -0700691 }
Adam Cohen3d07af02010-08-18 17:46:23 -0700692
Adam Cohen3352b682010-10-26 13:54:00 -0700693 mTransitionIsSetup = stackMode == StackSlider.NORMAL_MODE;
694
Adam Cohen3d07af02010-08-18 17:46:23 -0700695 View v = getViewAtRelativeIndex(activeIndex);
696 if (v == null) return;
697
Adam Cohen3352b682010-10-26 13:54:00 -0700698 setupStackSlider(v, stackMode);
Adam Cohen3d07af02010-08-18 17:46:23 -0700699
700 // We only register this gesture if we've made it this far without a problem
701 mSwipeGestureType = swipeGestureType;
Adam Cohena32edd42010-10-26 10:35:01 -0700702 cancelHandleClick();
Adam Cohen32a42f12010-08-11 19:34:30 -0700703 }
704 }
705
Adam Cohen0e2de6d2011-01-19 17:16:34 -0800706 /**
707 * {@inheritDoc}
708 */
Adam Cohen44729e32010-07-22 16:00:07 -0700709 @Override
710 public boolean onTouchEvent(MotionEvent ev) {
Adam Cohena32edd42010-10-26 10:35:01 -0700711 super.onTouchEvent(ev);
712
Adam Cohen44729e32010-07-22 16:00:07 -0700713 int action = ev.getAction();
714 int pointerIndex = ev.findPointerIndex(mActivePointerId);
715 if (pointerIndex == INVALID_POINTER) {
716 // no data for our primary pointer, this shouldn't happen, log it
717 Log.d(TAG, "Error: No data for our primary pointer.");
718 return false;
719 }
720
721 float newY = ev.getY(pointerIndex);
Adam Cohen32a42f12010-08-11 19:34:30 -0700722 float newX = ev.getX(pointerIndex);
Adam Cohen44729e32010-07-22 16:00:07 -0700723 float deltaY = newY - mInitialY;
Adam Cohen32a42f12010-08-11 19:34:30 -0700724 float deltaX = newX - mInitialX;
Adam Cohen44729e32010-07-22 16:00:07 -0700725 if (mVelocityTracker == null) {
726 mVelocityTracker = VelocityTracker.obtain();
727 }
728 mVelocityTracker.addMovement(ev);
729
730 switch (action & MotionEvent.ACTION_MASK) {
731 case MotionEvent.ACTION_MOVE: {
Adam Cohen32a42f12010-08-11 19:34:30 -0700732 beginGestureIfNeeded(deltaY);
733
Adam Cohendfcdddd2010-09-10 14:38:40 -0700734 float rx = deltaX / (mSlideAmount * 1.0f);
Adam Cohen32a42f12010-08-11 19:34:30 -0700735 if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
Adam Cohendfcdddd2010-09-10 14:38:40 -0700736 float r = (deltaY - mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
Adam Cohen839f4a52010-08-26 17:36:48 -0700737 if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
Adam Cohen32a42f12010-08-11 19:34:30 -0700738 mStackSlider.setYProgress(1 - r);
739 mStackSlider.setXProgress(rx);
740 return true;
741 } else if (mSwipeGestureType == GESTURE_SLIDE_UP) {
Adam Cohendfcdddd2010-09-10 14:38:40 -0700742 float r = -(deltaY + mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
Adam Cohen839f4a52010-08-26 17:36:48 -0700743 if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
Adam Cohen32a42f12010-08-11 19:34:30 -0700744 mStackSlider.setYProgress(r);
745 mStackSlider.setXProgress(rx);
746 return true;
Adam Cohen44729e32010-07-22 16:00:07 -0700747 }
Adam Cohen44729e32010-07-22 16:00:07 -0700748 break;
749 }
750 case MotionEvent.ACTION_UP: {
751 handlePointerUp(ev);
752 break;
753 }
754 case MotionEvent.ACTION_POINTER_UP: {
755 onSecondaryPointerUp(ev);
756 break;
757 }
758 case MotionEvent.ACTION_CANCEL: {
759 mActivePointerId = INVALID_POINTER;
Adam Cohen44729e32010-07-22 16:00:07 -0700760 mSwipeGestureType = GESTURE_NONE;
Adam Cohen44729e32010-07-22 16:00:07 -0700761 break;
762 }
763 }
764 return true;
765 }
766
Adam Cohen44729e32010-07-22 16:00:07 -0700767 private void onSecondaryPointerUp(MotionEvent ev) {
768 final int activePointerIndex = ev.getActionIndex();
769 final int pointerId = ev.getPointerId(activePointerIndex);
770 if (pointerId == mActivePointerId) {
771
Adam Cohen96d8d562010-10-24 11:12:18 -0700772 int activeViewIndex = (mSwipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1;
Adam Cohen44729e32010-07-22 16:00:07 -0700773
774 View v = getViewAtRelativeIndex(activeViewIndex);
775 if (v == null) return;
776
777 // Our primary pointer has gone up -- let's see if we can find
778 // another pointer on the view. If so, then we should replace
779 // our primary pointer with this new pointer and adjust things
780 // so that the view doesn't jump
781 for (int index = 0; index < ev.getPointerCount(); index++) {
782 if (index != activePointerIndex) {
783
784 float x = ev.getX(index);
785 float y = ev.getY(index);
786
Patrick Dubroye80202d2010-11-16 15:41:08 -0800787 mTouchRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
788 if (mTouchRect.contains(Math.round(x), Math.round(y))) {
Adam Cohen44729e32010-07-22 16:00:07 -0700789 float oldX = ev.getX(activePointerIndex);
790 float oldY = ev.getY(activePointerIndex);
791
792 // adjust our frame of reference to avoid a jump
793 mInitialY += (y - oldY);
794 mInitialX += (x - oldX);
795
796 mActivePointerId = ev.getPointerId(index);
797 if (mVelocityTracker != null) {
798 mVelocityTracker.clear();
799 }
800 // ok, we're good, we found a new pointer which is touching the active view
801 return;
802 }
803 }
804 }
805 // if we made it this far, it means we didn't find a satisfactory new pointer :(,
Adam Cohen3d07af02010-08-18 17:46:23 -0700806 // so end the gesture
Adam Cohen44729e32010-07-22 16:00:07 -0700807 handlePointerUp(ev);
808 }
809 }
810
811 private void handlePointerUp(MotionEvent ev) {
812 int pointerIndex = ev.findPointerIndex(mActivePointerId);
813 float newY = ev.getY(pointerIndex);
814 int deltaY = (int) (newY - mInitialY);
Adam Cohen26e30bb2010-12-03 18:16:12 -0800815 mLastInteractionTime = System.currentTimeMillis();
Adam Cohen44729e32010-07-22 16:00:07 -0700816
Adam Cohen3d07af02010-08-18 17:46:23 -0700817 if (mVelocityTracker != null) {
818 mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
819 mYVelocity = (int) mVelocityTracker.getYVelocity(mActivePointerId);
820 }
Adam Cohen44729e32010-07-22 16:00:07 -0700821
822 if (mVelocityTracker != null) {
823 mVelocityTracker.recycle();
824 mVelocityTracker = null;
825 }
826
Adam Cohen3d07af02010-08-18 17:46:23 -0700827 if (deltaY > mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_DOWN
828 && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800829 // We reset the gesture variable, because otherwise we will ignore showPrevious() /
830 // showNext();
831 mSwipeGestureType = GESTURE_NONE;
832
Adam Cohen44729e32010-07-22 16:00:07 -0700833 // Swipe threshold exceeded, swipe down
Adam Cohen839f4a52010-08-26 17:36:48 -0700834 if (mStackMode == ITEMS_SLIDE_UP) {
Adam Cohen839f4a52010-08-26 17:36:48 -0700835 showPrevious();
Adam Cohen96d8d562010-10-24 11:12:18 -0700836 } else {
837 showNext();
Adam Cohen839f4a52010-08-26 17:36:48 -0700838 }
Adam Cohen32a42f12010-08-11 19:34:30 -0700839 mHighlight.bringToFront();
Adam Cohen3d07af02010-08-18 17:46:23 -0700840 } else if (deltaY < -mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_UP
841 && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800842 // We reset the gesture variable, because otherwise we will ignore showPrevious() /
843 // showNext();
844 mSwipeGestureType = GESTURE_NONE;
845
Adam Cohen44729e32010-07-22 16:00:07 -0700846 // Swipe threshold exceeded, swipe up
Adam Cohen839f4a52010-08-26 17:36:48 -0700847 if (mStackMode == ITEMS_SLIDE_UP) {
Adam Cohen839f4a52010-08-26 17:36:48 -0700848 showNext();
Adam Cohen96d8d562010-10-24 11:12:18 -0700849 } else {
850 showPrevious();
Adam Cohen839f4a52010-08-26 17:36:48 -0700851 }
852
Adam Cohen32a42f12010-08-11 19:34:30 -0700853 mHighlight.bringToFront();
Adam Cohen839f4a52010-08-26 17:36:48 -0700854 } else if (mSwipeGestureType == GESTURE_SLIDE_UP ) {
Adam Cohen44729e32010-07-22 16:00:07 -0700855 // Didn't swipe up far enough, snap back down
Adam Cohen839f4a52010-08-26 17:36:48 -0700856 int duration;
857 float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 1 : 0;
858 if (mStackMode == ITEMS_SLIDE_UP || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
859 duration = Math.round(mStackSlider.getDurationForNeutralPosition());
860 } else {
861 duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
862 }
Adam Cohen44729e32010-07-22 16:00:07 -0700863
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700864 StackSlider animationSlider = new StackSlider(mStackSlider);
Chet Haase2794eb32010-10-12 16:29:28 -0700865 PropertyValuesHolder snapBackY = PropertyValuesHolder.ofFloat("YProgress", finalYProgress);
866 PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
867 ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
Adam Cohen839f4a52010-08-26 17:36:48 -0700868 snapBackX, snapBackY);
Chet Haase2794eb32010-10-12 16:29:28 -0700869 pa.setDuration(duration);
Adam Cohen839f4a52010-08-26 17:36:48 -0700870 pa.setInterpolator(new LinearInterpolator());
871 pa.start();
Adam Cohen32a42f12010-08-11 19:34:30 -0700872 } else if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
Adam Cohen44729e32010-07-22 16:00:07 -0700873 // Didn't swipe down far enough, snap back up
Adam Cohen839f4a52010-08-26 17:36:48 -0700874 float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 0 : 1;
875 int duration;
876 if (mStackMode == ITEMS_SLIDE_DOWN || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
877 duration = Math.round(mStackSlider.getDurationForNeutralPosition());
878 } else {
879 duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
880 }
Adam Cohen3d07af02010-08-18 17:46:23 -0700881
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700882 StackSlider animationSlider = new StackSlider(mStackSlider);
Chet Haase2794eb32010-10-12 16:29:28 -0700883 PropertyValuesHolder snapBackY =
884 PropertyValuesHolder.ofFloat("YProgress",finalYProgress);
885 PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
886 ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
Adam Cohen839f4a52010-08-26 17:36:48 -0700887 snapBackX, snapBackY);
Chet Haase2794eb32010-10-12 16:29:28 -0700888 pa.setDuration(duration);
Adam Cohen839f4a52010-08-26 17:36:48 -0700889 pa.start();
Adam Cohen44729e32010-07-22 16:00:07 -0700890 }
891
892 mActivePointerId = INVALID_POINTER;
Adam Cohen44729e32010-07-22 16:00:07 -0700893 mSwipeGestureType = GESTURE_NONE;
Adam Cohen32a42f12010-08-11 19:34:30 -0700894 }
895
896 private class StackSlider {
897 View mView;
898 float mYProgress;
899 float mXProgress;
900
Adam Cohen3d07af02010-08-18 17:46:23 -0700901 static final int NORMAL_MODE = 0;
902 static final int BEGINNING_OF_STACK_MODE = 1;
903 static final int END_OF_STACK_MODE = 2;
904
905 int mMode = NORMAL_MODE;
906
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700907 public StackSlider() {
908 }
909
910 public StackSlider(StackSlider copy) {
911 mView = copy.mView;
912 mYProgress = copy.mYProgress;
913 mXProgress = copy.mXProgress;
Adam Cohen3d07af02010-08-18 17:46:23 -0700914 mMode = copy.mMode;
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700915 }
916
Adam Cohen32a42f12010-08-11 19:34:30 -0700917 private float cubic(float r) {
Adam Cohen839f4a52010-08-26 17:36:48 -0700918 return (float) (Math.pow(2 * r - 1, 3) + 1) / 2.0f;
Adam Cohen32a42f12010-08-11 19:34:30 -0700919 }
920
921 private float highlightAlphaInterpolator(float r) {
922 float pivot = 0.4f;
923 if (r < pivot) {
Adam Cohen839f4a52010-08-26 17:36:48 -0700924 return 0.85f * cubic(r / pivot);
Adam Cohen32a42f12010-08-11 19:34:30 -0700925 } else {
Adam Cohen839f4a52010-08-26 17:36:48 -0700926 return 0.85f * cubic(1 - (r - pivot) / (1 - pivot));
Adam Cohen32a42f12010-08-11 19:34:30 -0700927 }
928 }
929
930 private float viewAlphaInterpolator(float r) {
931 float pivot = 0.3f;
932 if (r > pivot) {
Adam Cohen839f4a52010-08-26 17:36:48 -0700933 return (r - pivot) / (1 - pivot);
Adam Cohen32a42f12010-08-11 19:34:30 -0700934 } else {
935 return 0;
936 }
937 }
938
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700939 private float rotationInterpolator(float r) {
940 float pivot = 0.2f;
941 if (r < pivot) {
942 return 0;
943 } else {
Adam Cohen839f4a52010-08-26 17:36:48 -0700944 return (r - pivot) / (1 - pivot);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700945 }
946 }
947
Adam Cohen32a42f12010-08-11 19:34:30 -0700948 void setView(View v) {
949 mView = v;
950 }
951
952 public void setYProgress(float r) {
953 // enforce r between 0 and 1
954 r = Math.min(1.0f, r);
955 r = Math.max(0, r);
956
957 mYProgress = r;
Adam Cohena02fdf12010-11-03 13:27:40 -0700958 if (mView == null) return;
959
Adam Cohen32a42f12010-08-11 19:34:30 -0700960 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
961 final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
962
Adam Cohen839f4a52010-08-26 17:36:48 -0700963 int stackDirection = (mStackMode == ITEMS_SLIDE_UP) ? 1 : -1;
964
Adam Cohen48867212011-01-09 13:49:40 -0800965 // We need to prevent any clipping issues which may arise by setting a layer type.
966 // This doesn't come for free however, so we only want to enable it when required.
967 if (Float.compare(0f, mYProgress) != 0 && Float.compare(1.0f, mYProgress) != 0) {
968 if (mView.getLayerType() == LAYER_TYPE_NONE) {
969 mView.setLayerType(LAYER_TYPE_HARDWARE, null);
970 }
971 } else {
972 if (mView.getLayerType() != LAYER_TYPE_NONE) {
973 mView.setLayerType(LAYER_TYPE_NONE, null);
974 }
975 }
976
Adam Cohen3d07af02010-08-18 17:46:23 -0700977 switch (mMode) {
978 case NORMAL_MODE:
Adam Cohendfcdddd2010-09-10 14:38:40 -0700979 viewLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
980 highlightLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
Adam Cohen3d07af02010-08-18 17:46:23 -0700981 mHighlight.setAlpha(highlightAlphaInterpolator(r));
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700982
Adam Cohen839f4a52010-08-26 17:36:48 -0700983 float alpha = viewAlphaInterpolator(1 - r);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700984
Adam Cohen3d07af02010-08-18 17:46:23 -0700985 // We make sure that views which can't be seen (have 0 alpha) are also invisible
986 // so that they don't interfere with click events.
987 if (mView.getAlpha() == 0 && alpha != 0 && mView.getVisibility() != VISIBLE) {
988 mView.setVisibility(VISIBLE);
989 } else if (alpha == 0 && mView.getAlpha() != 0
990 && mView.getVisibility() == VISIBLE) {
991 mView.setVisibility(INVISIBLE);
992 }
993
994 mView.setAlpha(alpha);
Adam Cohen839f4a52010-08-26 17:36:48 -0700995 mView.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
996 mHighlight.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
Adam Cohen3d07af02010-08-18 17:46:23 -0700997 break;
Adam Cohen96d8d562010-10-24 11:12:18 -0700998 case END_OF_STACK_MODE:
Adam Cohen839f4a52010-08-26 17:36:48 -0700999 r = r * 0.2f;
Adam Cohendfcdddd2010-09-10 14:38:40 -07001000 viewLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount));
1001 highlightLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount));
Adam Cohen3d07af02010-08-18 17:46:23 -07001002 mHighlight.setAlpha(highlightAlphaInterpolator(r));
1003 break;
Adam Cohen96d8d562010-10-24 11:12:18 -07001004 case BEGINNING_OF_STACK_MODE:
Adam Cohen839f4a52010-08-26 17:36:48 -07001005 r = (1-r) * 0.2f;
Adam Cohendfcdddd2010-09-10 14:38:40 -07001006 viewLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount));
1007 highlightLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount));
Adam Cohen3d07af02010-08-18 17:46:23 -07001008 mHighlight.setAlpha(highlightAlphaInterpolator(r));
1009 break;
Adam Cohenb04f7ad2010-08-15 13:22:42 -07001010 }
Adam Cohen32a42f12010-08-11 19:34:30 -07001011 }
1012
1013 public void setXProgress(float r) {
1014 // enforce r between 0 and 1
Adam Cohen3d07af02010-08-18 17:46:23 -07001015 r = Math.min(2.0f, r);
1016 r = Math.max(-2.0f, r);
Adam Cohen32a42f12010-08-11 19:34:30 -07001017
1018 mXProgress = r;
1019
Adam Cohena02fdf12010-11-03 13:27:40 -07001020 if (mView == null) return;
Adam Cohen32a42f12010-08-11 19:34:30 -07001021 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
1022 final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
1023
Adam Cohen3d07af02010-08-18 17:46:23 -07001024 r *= 0.2f;
Adam Cohendfcdddd2010-09-10 14:38:40 -07001025 viewLp.setHorizontalOffset(Math.round(r * mSlideAmount));
1026 highlightLp.setHorizontalOffset(Math.round(r * mSlideAmount));
Adam Cohen32a42f12010-08-11 19:34:30 -07001027 }
1028
Adam Cohen3d07af02010-08-18 17:46:23 -07001029 void setMode(int mode) {
1030 mMode = mode;
1031 }
1032
1033 float getDurationForNeutralPosition() {
Adam Cohen839f4a52010-08-26 17:36:48 -07001034 return getDuration(false, 0);
Adam Cohen3d07af02010-08-18 17:46:23 -07001035 }
1036
1037 float getDurationForOffscreenPosition() {
Adam Cohen839f4a52010-08-26 17:36:48 -07001038 return getDuration(true, 0);
Adam Cohen3d07af02010-08-18 17:46:23 -07001039 }
1040
Adam Cohen839f4a52010-08-26 17:36:48 -07001041 float getDurationForNeutralPosition(float velocity) {
1042 return getDuration(false, velocity);
1043 }
1044
1045 float getDurationForOffscreenPosition(float velocity) {
1046 return getDuration(true, velocity);
1047 }
1048
1049 private float getDuration(boolean invert, float velocity) {
Adam Cohen3d07af02010-08-18 17:46:23 -07001050 if (mView != null) {
1051 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
1052
Adam Cohen839f4a52010-08-26 17:36:48 -07001053 float d = (float) Math.sqrt(Math.pow(viewLp.horizontalOffset, 2) +
1054 Math.pow(viewLp.verticalOffset, 2));
Adam Cohendfcdddd2010-09-10 14:38:40 -07001055 float maxd = (float) Math.sqrt(Math.pow(mSlideAmount, 2) +
1056 Math.pow(0.4f * mSlideAmount, 2));
Adam Cohen839f4a52010-08-26 17:36:48 -07001057
1058 if (velocity == 0) {
1059 return (invert ? (1 - d / maxd) : d / maxd) * DEFAULT_ANIMATION_DURATION;
1060 } else {
1061 float duration = invert ? d / Math.abs(velocity) :
1062 (maxd - d) / Math.abs(velocity);
1063 if (duration < MINIMUM_ANIMATION_DURATION ||
1064 duration > DEFAULT_ANIMATION_DURATION) {
1065 return getDuration(invert, 0);
1066 } else {
1067 return duration;
1068 }
1069 }
Adam Cohen3d07af02010-08-18 17:46:23 -07001070 }
1071 return 0;
1072 }
1073
Romain Guye5ebcb02010-10-15 13:57:28 -07001074 // Used for animations
1075 @SuppressWarnings({"UnusedDeclaration"})
Adam Cohen839f4a52010-08-26 17:36:48 -07001076 public float getYProgress() {
Adam Cohen32a42f12010-08-11 19:34:30 -07001077 return mYProgress;
1078 }
1079
Romain Guye5ebcb02010-10-15 13:57:28 -07001080 // Used for animations
1081 @SuppressWarnings({"UnusedDeclaration"})
Adam Cohen839f4a52010-08-26 17:36:48 -07001082 public float getXProgress() {
Adam Cohen32a42f12010-08-11 19:34:30 -07001083 return mXProgress;
1084 }
Adam Cohen44729e32010-07-22 16:00:07 -07001085 }
1086
Adam Cohen9b073942010-08-19 16:49:52 -07001087 LayoutParams createOrReuseLayoutParams(View v) {
1088 final ViewGroup.LayoutParams currentLp = v.getLayoutParams();
1089 if (currentLp instanceof LayoutParams) {
1090 LayoutParams lp = (LayoutParams) currentLp;
1091 lp.setHorizontalOffset(0);
1092 lp.setVerticalOffset(0);
Adam Cohen839f4a52010-08-26 17:36:48 -07001093 lp.width = 0;
1094 lp.width = 0;
Adam Cohen9b073942010-08-19 16:49:52 -07001095 return lp;
1096 }
1097 return new LayoutParams(v);
Adam Cohen32a42f12010-08-11 19:34:30 -07001098 }
1099
Adam Cohen9b073942010-08-19 16:49:52 -07001100 @Override
1101 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Adam Cohenef17dd42011-01-20 17:20:57 -08001102 checkForAndHandleDataChanged();
Adam Cohenb04f7ad2010-08-15 13:22:42 -07001103
Adam Cohen9b073942010-08-19 16:49:52 -07001104 final int childCount = getChildCount();
1105 for (int i = 0; i < childCount; i++) {
1106 final View child = getChildAt(i);
Adam Cohen32a42f12010-08-11 19:34:30 -07001107
Adam Cohen9b073942010-08-19 16:49:52 -07001108 int childRight = mPaddingLeft + child.getMeasuredWidth();
1109 int childBottom = mPaddingTop + child.getMeasuredHeight();
1110 LayoutParams lp = (LayoutParams) child.getLayoutParams();
Adam Cohen32a42f12010-08-11 19:34:30 -07001111
Adam Cohen9b073942010-08-19 16:49:52 -07001112 child.layout(mPaddingLeft + lp.horizontalOffset, mPaddingTop + lp.verticalOffset,
1113 childRight + lp.horizontalOffset, childBottom + lp.verticalOffset);
1114
Adam Cohen9b073942010-08-19 16:49:52 -07001115 }
Adam Cohen9b073942010-08-19 16:49:52 -07001116 onLayout();
Adam Cohen32a42f12010-08-11 19:34:30 -07001117 }
1118
Adam Cohen26e30bb2010-12-03 18:16:12 -08001119 @Override
1120 public void advance() {
1121 long timeSinceLastInteraction = System.currentTimeMillis() - mLastInteractionTime;
Adam Cohenc99ff732011-01-16 16:11:41 -08001122
1123 if (mAdapter == null) return;
Adam Cohenef17dd42011-01-20 17:20:57 -08001124 final int adapterCount = getCount();
Adam Cohenc99ff732011-01-16 16:11:41 -08001125 if (adapterCount == 1 && mLoopViews) return;
1126
Adam Cohen26e30bb2010-12-03 18:16:12 -08001127 if (mSwipeGestureType == GESTURE_NONE &&
1128 timeSinceLastInteraction > MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE) {
1129 showNext();
1130 }
1131 }
1132
Adam Cohen839f4a52010-08-26 17:36:48 -07001133 private void measureChildren() {
1134 final int count = getChildCount();
Adam Cohen36f43902010-12-15 21:17:33 -08001135
1136 final int measuredWidth = getMeasuredWidth();
1137 final int measuredHeight = getMeasuredHeight();
1138
1139 final int childWidth = Math.round(measuredWidth*(1-PERSPECTIVE_SHIFT_FACTOR_X))
Adam Cohen026e1212010-12-13 12:29:17 -08001140 - mPaddingLeft - mPaddingRight;
Adam Cohen36f43902010-12-15 21:17:33 -08001141 final int childHeight = Math.round(measuredHeight*(1-PERSPECTIVE_SHIFT_FACTOR_Y))
Adam Cohen839f4a52010-08-26 17:36:48 -07001142 - mPaddingTop - mPaddingBottom;
1143
Adam Cohen36f43902010-12-15 21:17:33 -08001144 int maxWidth = 0;
1145 int maxHeight = 0;
1146
Adam Cohen839f4a52010-08-26 17:36:48 -07001147 for (int i = 0; i < count; i++) {
1148 final View child = getChildAt(i);
Adam Cohen36f43902010-12-15 21:17:33 -08001149 child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST),
1150 MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST));
1151
1152 if (child != mHighlight && child != mClickFeedback) {
1153 final int childMeasuredWidth = child.getMeasuredWidth();
1154 final int childMeasuredHeight = child.getMeasuredHeight();
1155 if (childMeasuredWidth > maxWidth) {
1156 maxWidth = childMeasuredWidth;
1157 }
1158 if (childMeasuredHeight > maxHeight) {
1159 maxHeight = childMeasuredHeight;
1160 }
1161 }
1162 }
1163
1164 mNewPerspectiveShiftX = PERSPECTIVE_SHIFT_FACTOR_X * measuredWidth;
1165 mNewPerspectiveShiftY = PERSPECTIVE_SHIFT_FACTOR_Y * measuredHeight;
Adam Cohen53838d22011-01-26 21:32:33 -08001166
1167 // If we have extra space, we try and spread the items out
Adam Cohen78db1aa2011-01-25 12:24:23 -08001168 if (maxWidth > 0 && count > 0 && maxWidth < childWidth) {
Adam Cohen36f43902010-12-15 21:17:33 -08001169 mNewPerspectiveShiftX = measuredWidth - maxWidth;
1170 }
1171
Adam Cohen78db1aa2011-01-25 12:24:23 -08001172 if (maxHeight > 0 && count > 0 && maxHeight < childHeight) {
Adam Cohen36f43902010-12-15 21:17:33 -08001173 mNewPerspectiveShiftY = measuredHeight - maxHeight;
Adam Cohen839f4a52010-08-26 17:36:48 -07001174 }
1175 }
1176
1177 @Override
1178 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1179 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
1180 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
1181 final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
1182 final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
1183
1184 boolean haveChildRefSize = (mReferenceChildWidth != -1 && mReferenceChildHeight != -1);
1185
1186 // We need to deal with the case where our parent hasn't told us how
1187 // big we should be. In this case we should
Adam Cohen026e1212010-12-13 12:29:17 -08001188 float factorY = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_Y);
Adam Cohen839f4a52010-08-26 17:36:48 -07001189 if (heightSpecMode == MeasureSpec.UNSPECIFIED) {
1190 heightSpecSize = haveChildRefSize ?
Adam Cohen026e1212010-12-13 12:29:17 -08001191 Math.round(mReferenceChildHeight * (1 + factorY)) +
Adam Cohen839f4a52010-08-26 17:36:48 -07001192 mPaddingTop + mPaddingBottom : 0;
1193 } else if (heightSpecMode == MeasureSpec.AT_MOST) {
Dianne Hackborn189ee182010-12-02 21:48:53 -08001194 if (haveChildRefSize) {
Adam Cohen026e1212010-12-13 12:29:17 -08001195 int height = Math.round(mReferenceChildHeight * (1 + factorY))
Dianne Hackborn189ee182010-12-02 21:48:53 -08001196 + mPaddingTop + mPaddingBottom;
1197 if (height <= heightSpecSize) {
1198 heightSpecSize = height;
1199 } else {
1200 heightSpecSize |= MEASURED_STATE_TOO_SMALL;
Adam Cohenef17dd42011-01-20 17:20:57 -08001201
Dianne Hackborn189ee182010-12-02 21:48:53 -08001202 }
1203 } else {
1204 heightSpecSize = 0;
1205 }
Adam Cohen839f4a52010-08-26 17:36:48 -07001206 }
1207
Adam Cohen026e1212010-12-13 12:29:17 -08001208 float factorX = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_X);
Adam Cohen839f4a52010-08-26 17:36:48 -07001209 if (widthSpecMode == MeasureSpec.UNSPECIFIED) {
Adam Cohen026e1212010-12-13 12:29:17 -08001210 widthSpecSize = haveChildRefSize ?
1211 Math.round(mReferenceChildWidth * (1 + factorX)) +
1212 mPaddingLeft + mPaddingRight : 0;
Adam Cohen839f4a52010-08-26 17:36:48 -07001213 } else if (heightSpecMode == MeasureSpec.AT_MOST) {
Dianne Hackborn189ee182010-12-02 21:48:53 -08001214 if (haveChildRefSize) {
1215 int width = mReferenceChildWidth + mPaddingLeft + mPaddingRight;
1216 if (width <= widthSpecSize) {
1217 widthSpecSize = width;
1218 } else {
1219 widthSpecSize |= MEASURED_STATE_TOO_SMALL;
1220 }
1221 } else {
1222 widthSpecSize = 0;
1223 }
Adam Cohen839f4a52010-08-26 17:36:48 -07001224 }
Adam Cohen839f4a52010-08-26 17:36:48 -07001225 setMeasuredDimension(widthSpecSize, heightSpecSize);
1226 measureChildren();
1227 }
1228
Adam Cohen9b073942010-08-19 16:49:52 -07001229 class LayoutParams extends ViewGroup.LayoutParams {
1230 int horizontalOffset;
1231 int verticalOffset;
1232 View mView;
Adam Cohend51bbb52010-10-18 10:59:49 -07001233 private final Rect parentRect = new Rect();
1234 private final Rect invalidateRect = new Rect();
1235 private final RectF invalidateRectf = new RectF();
1236 private final Rect globalInvalidateRect = new Rect();
Adam Cohen32a42f12010-08-11 19:34:30 -07001237
Adam Cohen9b073942010-08-19 16:49:52 -07001238 LayoutParams(View view) {
1239 super(0, 0);
Adam Cohen839f4a52010-08-26 17:36:48 -07001240 width = 0;
1241 height = 0;
Adam Cohen9b073942010-08-19 16:49:52 -07001242 horizontalOffset = 0;
1243 verticalOffset = 0;
1244 mView = view;
1245 }
Adam Cohen32a42f12010-08-11 19:34:30 -07001246
Adam Cohen9b073942010-08-19 16:49:52 -07001247 LayoutParams(Context c, AttributeSet attrs) {
1248 super(c, attrs);
1249 horizontalOffset = 0;
1250 verticalOffset = 0;
Adam Cohen839f4a52010-08-26 17:36:48 -07001251 width = 0;
1252 height = 0;
Adam Cohen9b073942010-08-19 16:49:52 -07001253 }
Adam Cohen32a42f12010-08-11 19:34:30 -07001254
Adam Cohen9b073942010-08-19 16:49:52 -07001255 void invalidateGlobalRegion(View v, Rect r) {
Adam Cohend51bbb52010-10-18 10:59:49 -07001256 // We need to make a new rect here, so as not to modify the one passed
1257 globalInvalidateRect.set(r);
Adam Cohen1ff65d12011-02-16 14:04:09 -08001258 globalInvalidateRect.union(0, 0, getWidth(), getHeight());
Adam Cohen9b073942010-08-19 16:49:52 -07001259 View p = v;
1260 if (!(v.getParent() != null && v.getParent() instanceof View)) return;
1261
Adam Cohen9b073942010-08-19 16:49:52 -07001262 boolean firstPass = true;
1263 parentRect.set(0, 0, 0, 0);
1264 int depth = 0;
Adam Cohenb7f4d032010-09-16 15:25:54 -07001265 while (p.getParent() != null && p.getParent() instanceof View
Adam Cohend51bbb52010-10-18 10:59:49 -07001266 && !parentRect.contains(globalInvalidateRect)) {
Adam Cohen9b073942010-08-19 16:49:52 -07001267 if (!firstPass) {
Adam Cohend51bbb52010-10-18 10:59:49 -07001268 globalInvalidateRect.offset(p.getLeft() - p.getScrollX(), p.getTop()
1269 - p.getScrollY());
Adam Cohen9b073942010-08-19 16:49:52 -07001270 depth++;
1271 }
1272 firstPass = false;
1273 p = (View) p.getParent();
Adam Cohenb7f4d032010-09-16 15:25:54 -07001274 parentRect.set(p.getScrollX(), p.getScrollY(),
Adam Cohen1ff65d12011-02-16 14:04:09 -08001275 p.getWidth() + p.getScrollX(), p.getHeight() + p.getScrollY());
1276 p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top,
1277 globalInvalidateRect.right, globalInvalidateRect.bottom);
Adam Cohen9b073942010-08-19 16:49:52 -07001278 }
1279
Adam Cohend51bbb52010-10-18 10:59:49 -07001280 p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top,
1281 globalInvalidateRect.right, globalInvalidateRect.bottom);
Adam Cohen9b073942010-08-19 16:49:52 -07001282 }
1283
Adam Cohend51bbb52010-10-18 10:59:49 -07001284 Rect getInvalidateRect() {
1285 return invalidateRect;
1286 }
1287
1288 void resetInvalidateRect() {
Adam Cohen0ac116b2010-11-11 11:39:53 -08001289 invalidateRect.set(0, 0, 0, 0);
Adam Cohend51bbb52010-10-18 10:59:49 -07001290 }
1291
Chet Haasea18a86b2010-09-07 13:20:00 -07001292 // This is public so that ObjectAnimator can access it
Adam Cohen9b073942010-08-19 16:49:52 -07001293 public void setVerticalOffset(int newVerticalOffset) {
Adam Cohen0ac116b2010-11-11 11:39:53 -08001294 setOffsets(horizontalOffset, newVerticalOffset);
1295 }
1296
1297 public void setHorizontalOffset(int newHorizontalOffset) {
1298 setOffsets(newHorizontalOffset, verticalOffset);
1299 }
1300
1301 public void setOffsets(int newHorizontalOffset, int newVerticalOffset) {
1302 int horizontalOffsetDelta = newHorizontalOffset - horizontalOffset;
1303 horizontalOffset = newHorizontalOffset;
1304 int verticalOffsetDelta = newVerticalOffset - verticalOffset;
Adam Cohen9b073942010-08-19 16:49:52 -07001305 verticalOffset = newVerticalOffset;
1306
1307 if (mView != null) {
1308 mView.requestLayout();
Adam Cohen0ac116b2010-11-11 11:39:53 -08001309 int left = Math.min(mView.getLeft() + horizontalOffsetDelta, mView.getLeft());
1310 int right = Math.max(mView.getRight() + horizontalOffsetDelta, mView.getRight());
1311 int top = Math.min(mView.getTop() + verticalOffsetDelta, mView.getTop());
1312 int bottom = Math.max(mView.getBottom() + verticalOffsetDelta, mView.getBottom());
Adam Cohen9b073942010-08-19 16:49:52 -07001313
Adam Cohen0ac116b2010-11-11 11:39:53 -08001314 invalidateRectf.set(left, top, right, bottom);
Adam Cohen9b073942010-08-19 16:49:52 -07001315
1316 float xoffset = -invalidateRectf.left;
1317 float yoffset = -invalidateRectf.top;
1318 invalidateRectf.offset(xoffset, yoffset);
1319 mView.getMatrix().mapRect(invalidateRectf);
1320 invalidateRectf.offset(-xoffset, -yoffset);
1321
Adam Cohen0ac116b2010-11-11 11:39:53 -08001322 invalidateRect.set((int) Math.floor(invalidateRectf.left),
Adam Cohen9b073942010-08-19 16:49:52 -07001323 (int) Math.floor(invalidateRectf.top),
1324 (int) Math.ceil(invalidateRectf.right),
1325 (int) Math.ceil(invalidateRectf.bottom));
1326
1327 invalidateGlobalRegion(mView, invalidateRect);
1328 }
1329 }
1330 }
1331
1332 private static class HolographicHelper {
1333 private final Paint mHolographicPaint = new Paint();
1334 private final Paint mErasePaint = new Paint();
Adam Cohen839f4a52010-08-26 17:36:48 -07001335 private final Paint mBlurPaint = new Paint();
Adam Cohen8baf5df2010-11-11 15:23:41 -08001336 private static final int RES_OUT = 0;
1337 private static final int CLICK_FEEDBACK = 1;
1338 private float mDensity;
Patrick Dubroye80202d2010-11-16 15:41:08 -08001339 private BlurMaskFilter mSmallBlurMaskFilter;
1340 private BlurMaskFilter mLargeBlurMaskFilter;
1341 private final Canvas mCanvas = new Canvas();
1342 private final Canvas mMaskCanvas = new Canvas();
1343 private final int[] mTmpXY = new int[2];
1344 private final Matrix mIdentityMatrix = new Matrix();
Adam Cohen9b073942010-08-19 16:49:52 -07001345
Adam Cohendfcdddd2010-09-10 14:38:40 -07001346 HolographicHelper(Context context) {
Adam Cohen8baf5df2010-11-11 15:23:41 -08001347 mDensity = context.getResources().getDisplayMetrics().density;
Adam Cohendfcdddd2010-09-10 14:38:40 -07001348
Adam Cohen9b073942010-08-19 16:49:52 -07001349 mHolographicPaint.setFilterBitmap(true);
Adam Cohen839f4a52010-08-26 17:36:48 -07001350 mHolographicPaint.setMaskFilter(TableMaskFilter.CreateClipTable(0, 30));
Adam Cohen9b073942010-08-19 16:49:52 -07001351 mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
1352 mErasePaint.setFilterBitmap(true);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001353
1354 mSmallBlurMaskFilter = new BlurMaskFilter(2 * mDensity, BlurMaskFilter.Blur.NORMAL);
1355 mLargeBlurMaskFilter = new BlurMaskFilter(4 * mDensity, BlurMaskFilter.Blur.NORMAL);
Adam Cohen9b073942010-08-19 16:49:52 -07001356 }
1357
1358 Bitmap createOutline(View v) {
Adam Cohen8baf5df2010-11-11 15:23:41 -08001359 return createOutline(v, RES_OUT);
1360 }
1361
1362 Bitmap createOutline(View v, int type) {
1363 if (type == RES_OUT) {
1364 mHolographicPaint.setColor(0xff6699ff);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001365 mBlurPaint.setMaskFilter(mSmallBlurMaskFilter);
Adam Cohen8baf5df2010-11-11 15:23:41 -08001366 } else if (type == CLICK_FEEDBACK) {
1367 mHolographicPaint.setColor(0x886699ff);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001368 mBlurPaint.setMaskFilter(mLargeBlurMaskFilter);
Adam Cohen8baf5df2010-11-11 15:23:41 -08001369 }
1370
Adam Cohen9b073942010-08-19 16:49:52 -07001371 if (v.getMeasuredWidth() == 0 || v.getMeasuredHeight() == 0) {
1372 return null;
1373 }
1374
1375 Bitmap bitmap = Bitmap.createBitmap(v.getMeasuredWidth(), v.getMeasuredHeight(),
1376 Bitmap.Config.ARGB_8888);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001377 mCanvas.setBitmap(bitmap);
Adam Cohen9b073942010-08-19 16:49:52 -07001378
1379 float rotationX = v.getRotationX();
Adam Cohen839f4a52010-08-26 17:36:48 -07001380 float rotation = v.getRotation();
1381 float translationY = v.getTranslationY();
Adam Cohen026e1212010-12-13 12:29:17 -08001382 float translationX = v.getTranslationX();
Adam Cohen9b073942010-08-19 16:49:52 -07001383 v.setRotationX(0);
Adam Cohen839f4a52010-08-26 17:36:48 -07001384 v.setRotation(0);
1385 v.setTranslationY(0);
Adam Cohen026e1212010-12-13 12:29:17 -08001386 v.setTranslationX(0);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001387 v.draw(mCanvas);
Adam Cohen9b073942010-08-19 16:49:52 -07001388 v.setRotationX(rotationX);
Adam Cohen839f4a52010-08-26 17:36:48 -07001389 v.setRotation(rotation);
1390 v.setTranslationY(translationY);
Adam Cohen026e1212010-12-13 12:29:17 -08001391 v.setTranslationX(translationX);
Adam Cohen9b073942010-08-19 16:49:52 -07001392
Patrick Dubroye80202d2010-11-16 15:41:08 -08001393 drawOutline(mCanvas, bitmap);
Adam Cohen9b073942010-08-19 16:49:52 -07001394 return bitmap;
1395 }
1396
Adam Cohen9b073942010-08-19 16:49:52 -07001397 void drawOutline(Canvas dest, Bitmap src) {
Patrick Dubroye80202d2010-11-16 15:41:08 -08001398 final int[] xy = mTmpXY;
Adam Cohen839f4a52010-08-26 17:36:48 -07001399 Bitmap mask = src.extractAlpha(mBlurPaint, xy);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001400 mMaskCanvas.setBitmap(mask);
1401 mMaskCanvas.drawBitmap(src, -xy[0], -xy[1], mErasePaint);
Adam Cohen9b073942010-08-19 16:49:52 -07001402 dest.drawColor(0, PorterDuff.Mode.CLEAR);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001403 dest.setMatrix(mIdentityMatrix);
Adam Cohen839f4a52010-08-26 17:36:48 -07001404 dest.drawBitmap(mask, xy[0], xy[1], mHolographicPaint);
Adam Cohen9b073942010-08-19 16:49:52 -07001405 mask.recycle();
1406 }
Adam Cohen32a42f12010-08-11 19:34:30 -07001407 }
Adam Cohenef17dd42011-01-20 17:20:57 -08001408}