blob: d2e718c7e12e3be0fa5129ffc468b6e60fee0542 [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 Cohen26f072c2011-04-01 16:23:18 -070023import android.content.res.TypedArray;
Adam Cohen32a42f12010-08-11 19:34:30 -070024import android.graphics.Bitmap;
Adam Cohen839f4a52010-08-26 17:36:48 -070025import android.graphics.BlurMaskFilter;
Adam Cohen32a42f12010-08-11 19:34:30 -070026import android.graphics.Canvas;
27import android.graphics.Matrix;
28import android.graphics.Paint;
29import android.graphics.PorterDuff;
30import android.graphics.PorterDuffXfermode;
Adam Cohen44729e32010-07-22 16:00:07 -070031import android.graphics.Rect;
Adam Cohen9b073942010-08-19 16:49:52 -070032import android.graphics.RectF;
Adam Cohend51bbb52010-10-18 10:59:49 -070033import android.graphics.Region;
Adam Cohen839f4a52010-08-26 17:36:48 -070034import android.graphics.TableMaskFilter;
Svetoslav Ganov48d15862012-05-15 10:10:00 -070035import android.os.Bundle;
Adam Cohen44729e32010-07-22 16:00:07 -070036import android.util.AttributeSet;
37import android.util.Log;
Adam Cohena8a7c922011-02-28 17:20:44 -080038import android.view.InputDevice;
Adam Cohen44729e32010-07-22 16:00:07 -070039import android.view.MotionEvent;
40import android.view.VelocityTracker;
41import android.view.View;
42import android.view.ViewConfiguration;
43import android.view.ViewGroup;
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -080044import android.view.accessibility.AccessibilityEvent;
45import android.view.accessibility.AccessibilityNodeInfo;
Adam Cohenb04f7ad2010-08-15 13:22:42 -070046import android.view.animation.LinearInterpolator;
Adam Cohen44729e32010-07-22 16:00:07 -070047import android.widget.RemoteViews.RemoteView;
48
49@RemoteView
50/**
51 * A view that displays its children in a stack and allows users to discretely swipe
52 * through the children.
53 */
54public class StackView extends AdapterViewAnimator {
55 private final String TAG = "StackView";
56
57 /**
58 * Default animation parameters
59 */
Romain Guye5ebcb02010-10-15 13:57:28 -070060 private static final int DEFAULT_ANIMATION_DURATION = 400;
61 private static final int MINIMUM_ANIMATION_DURATION = 50;
Adam Cohenc0b53be2010-12-17 19:23:41 -080062 private static final int STACK_RELAYOUT_DURATION = 100;
Adam Cohen44729e32010-07-22 16:00:07 -070063
64 /**
Adam Cohen839f4a52010-08-26 17:36:48 -070065 * Parameters effecting the perspective visuals
66 */
Adam Cohen026e1212010-12-13 12:29:17 -080067 private static final float PERSPECTIVE_SHIFT_FACTOR_Y = 0.1f;
68 private static final float PERSPECTIVE_SHIFT_FACTOR_X = 0.1f;
69
Adam Cohen36f43902010-12-15 21:17:33 -080070 private float mPerspectiveShiftX;
71 private float mPerspectiveShiftY;
72 private float mNewPerspectiveShiftX;
73 private float mNewPerspectiveShiftY;
74
Romain Guye5ebcb02010-10-15 13:57:28 -070075 @SuppressWarnings({"FieldCanBeLocal"})
Adam Cohen78db1aa2011-01-25 12:24:23 -080076 private static final float PERSPECTIVE_SCALE_FACTOR = 0f;
Adam Cohen839f4a52010-08-26 17:36:48 -070077
78 /**
79 * Represent the two possible stack modes, one where items slide up, and the other
80 * where items slide down. The perspective is also inverted between these two modes.
81 */
82 private static final int ITEMS_SLIDE_UP = 0;
83 private static final int ITEMS_SLIDE_DOWN = 1;
84
85 /**
Adam Cohen44729e32010-07-22 16:00:07 -070086 * These specify the different gesture states
87 */
Romain Guy5b53f912010-08-16 18:24:33 -070088 private static final int GESTURE_NONE = 0;
89 private static final int GESTURE_SLIDE_UP = 1;
90 private static final int GESTURE_SLIDE_DOWN = 2;
Adam Cohen44729e32010-07-22 16:00:07 -070091
92 /**
93 * Specifies how far you need to swipe (up or down) before it
94 * will be consider a completed gesture when you lift your finger
95 */
Adam Cohena9238c82010-10-25 14:01:29 -070096 private static final float SWIPE_THRESHOLD_RATIO = 0.2f;
97
98 /**
99 * Specifies the total distance, relative to the size of the stack,
100 * that views will be slid, either up or down
101 */
Romain Guy5b53f912010-08-16 18:24:33 -0700102 private static final float SLIDE_UP_RATIO = 0.7f;
Adam Cohen44729e32010-07-22 16:00:07 -0700103
Adam Cohen44729e32010-07-22 16:00:07 -0700104 /**
105 * Sentinel value for no current active pointer.
106 * Used by {@link #mActivePointerId}.
107 */
108 private static final int INVALID_POINTER = -1;
109
110 /**
Adam Cohen839f4a52010-08-26 17:36:48 -0700111 * Number of active views in the stack. One fewer view is actually visible, as one is hidden.
112 */
113 private static final int NUM_ACTIVE_VIEWS = 5;
114
Adam Cohendfcdddd2010-09-10 14:38:40 -0700115 private static final int FRAME_PADDING = 4;
Adam Cohen839f4a52010-08-26 17:36:48 -0700116
Patrick Dubroye80202d2010-11-16 15:41:08 -0800117 private final Rect mTouchRect = new Rect();
118
Adam Cohen26e30bb2010-12-03 18:16:12 -0800119 private static final int MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE = 5000;
120
Romain Guy4725a902011-05-25 10:54:29 -0700121 private static final long MIN_TIME_BETWEEN_SCROLLS = 100;
Adam Cohena8a7c922011-02-28 17:20:44 -0800122
Adam Cohen839f4a52010-08-26 17:36:48 -0700123 /**
Adam Cohen44729e32010-07-22 16:00:07 -0700124 * These variables are all related to the current state of touch interaction
125 * with the stack
126 */
Adam Cohen44729e32010-07-22 16:00:07 -0700127 private float mInitialY;
128 private float mInitialX;
129 private int mActivePointerId;
Adam Cohen44729e32010-07-22 16:00:07 -0700130 private int mYVelocity = 0;
131 private int mSwipeGestureType = GESTURE_NONE;
Adam Cohendfcdddd2010-09-10 14:38:40 -0700132 private int mSlideAmount;
Adam Cohen44729e32010-07-22 16:00:07 -0700133 private int mSwipeThreshold;
134 private int mTouchSlop;
135 private int mMaximumVelocity;
136 private VelocityTracker mVelocityTracker;
Adam Cohen3352b682010-10-26 13:54:00 -0700137 private boolean mTransitionIsSetup = false;
Adam Cohen26f072c2011-04-01 16:23:18 -0700138 private int mResOutColor;
139 private int mClickColor;
Adam Cohen44729e32010-07-22 16:00:07 -0700140
Adam Cohen9b073942010-08-19 16:49:52 -0700141 private static HolographicHelper sHolographicHelper;
Adam Cohen32a42f12010-08-11 19:34:30 -0700142 private ImageView mHighlight;
Adam Cohen8baf5df2010-11-11 15:23:41 -0800143 private ImageView mClickFeedback;
144 private boolean mClickFeedbackIsValid = false;
Adam Cohen32a42f12010-08-11 19:34:30 -0700145 private StackSlider mStackSlider;
Adam Cohen44729e32010-07-22 16:00:07 -0700146 private boolean mFirstLayoutHappened = false;
Adam Cohen26e30bb2010-12-03 18:16:12 -0800147 private long mLastInteractionTime = 0;
Adam Cohena8a7c922011-02-28 17:20:44 -0800148 private long mLastScrollTime;
Adam Cohen839f4a52010-08-26 17:36:48 -0700149 private int mStackMode;
Adam Cohendfcdddd2010-09-10 14:38:40 -0700150 private int mFramePadding;
Adam Cohen0ac116b2010-11-11 11:39:53 -0800151 private final Rect stackInvalidateRect = new Rect();
Adam Cohen44729e32010-07-22 16:00:07 -0700152
Winson Chungabc8b50a2011-09-06 18:38:50 -0700153 /**
154 * {@inheritDoc}
155 */
Adam Cohen44729e32010-07-22 16:00:07 -0700156 public StackView(Context context) {
Adam Cohen26f072c2011-04-01 16:23:18 -0700157 this(context, null);
Adam Cohen44729e32010-07-22 16:00:07 -0700158 }
159
Winson Chungabc8b50a2011-09-06 18:38:50 -0700160 /**
161 * {@inheritDoc}
162 */
Adam Cohen44729e32010-07-22 16:00:07 -0700163 public StackView(Context context, AttributeSet attrs) {
Adam Cohen26f072c2011-04-01 16:23:18 -0700164 this(context, attrs, com.android.internal.R.attr.stackViewStyle);
165 }
166
Winson Chungabc8b50a2011-09-06 18:38:50 -0700167 /**
168 * {@inheritDoc}
169 */
Adam Cohen26f072c2011-04-01 16:23:18 -0700170 public StackView(Context context, AttributeSet attrs, int defStyleAttr) {
Alan Viverette617feb92013-09-09 18:09:13 -0700171 this(context, attrs, defStyleAttr, 0);
172 }
173
174 /**
175 * {@inheritDoc}
176 */
177 public StackView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
178 super(context, attrs, defStyleAttr, defStyleRes);
179 final TypedArray a = context.obtainStyledAttributes(
180 attrs, com.android.internal.R.styleable.StackView, defStyleAttr, defStyleRes);
Adam Cohen26f072c2011-04-01 16:23:18 -0700181
182 mResOutColor = a.getColor(
183 com.android.internal.R.styleable.StackView_resOutColor, 0);
184 mClickColor = a.getColor(
185 com.android.internal.R.styleable.StackView_clickColor, 0);
186
187 a.recycle();
Adam Cohen44729e32010-07-22 16:00:07 -0700188 initStackView();
189 }
190
191 private void initStackView() {
Adam Cohen96d8d562010-10-24 11:12:18 -0700192 configureViewAnimator(NUM_ACTIVE_VIEWS, 1);
Adam Cohen44729e32010-07-22 16:00:07 -0700193 setStaticTransformationsEnabled(true);
194 final ViewConfiguration configuration = ViewConfiguration.get(getContext());
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700195 mTouchSlop = configuration.getScaledTouchSlop();
Adam Cohen44729e32010-07-22 16:00:07 -0700196 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
197 mActivePointerId = INVALID_POINTER;
Adam Cohen32a42f12010-08-11 19:34:30 -0700198
199 mHighlight = new ImageView(getContext());
200 mHighlight.setLayoutParams(new LayoutParams(mHighlight));
201 addViewInLayout(mHighlight, -1, new LayoutParams(mHighlight));
Adam Cohen8baf5df2010-11-11 15:23:41 -0800202
203 mClickFeedback = new ImageView(getContext());
204 mClickFeedback.setLayoutParams(new LayoutParams(mClickFeedback));
205 addViewInLayout(mClickFeedback, -1, new LayoutParams(mClickFeedback));
206 mClickFeedback.setVisibility(INVISIBLE);
207
Adam Cohen32a42f12010-08-11 19:34:30 -0700208 mStackSlider = new StackSlider();
209
Adam Cohen9b073942010-08-19 16:49:52 -0700210 if (sHolographicHelper == null) {
Adam Cohendfcdddd2010-09-10 14:38:40 -0700211 sHolographicHelper = new HolographicHelper(mContext);
Adam Cohen32a42f12010-08-11 19:34:30 -0700212 }
Adam Cohen9b073942010-08-19 16:49:52 -0700213 setClipChildren(false);
214 setClipToPadding(false);
Adam Cohen1480fdd2010-08-25 17:24:53 -0700215
Adam Cohen839f4a52010-08-26 17:36:48 -0700216 // This sets the form of the StackView, which is currently to have the perspective-shifted
217 // views above the active view, and have items slide down when sliding out. The opposite is
218 // available by using ITEMS_SLIDE_UP.
219 mStackMode = ITEMS_SLIDE_DOWN;
220
Adam Cohen1480fdd2010-08-25 17:24:53 -0700221 // This is a flag to indicate the the stack is loading for the first time
222 mWhichChild = -1;
Adam Cohendfcdddd2010-09-10 14:38:40 -0700223
224 // Adjust the frame padding based on the density, since the highlight changes based
225 // on the density
226 final float density = mContext.getResources().getDisplayMetrics().density;
227 mFramePadding = (int) Math.ceil(density * FRAME_PADDING);
Adam Cohen44729e32010-07-22 16:00:07 -0700228 }
229
230 /**
231 * Animate the views between different relative indexes within the {@link AdapterViewAnimator}
232 */
Adam Cohen78db1aa2011-01-25 12:24:23 -0800233 void transformViewForTransition(int fromIndex, int toIndex, final View view, boolean animate) {
Adam Cohen78db1aa2011-01-25 12:24:23 -0800234 if (!animate) {
235 ((StackFrame) view).cancelSliderAnimator();
236 view.setRotationX(0f);
237 LayoutParams lp = (LayoutParams) view.getLayoutParams();
238 lp.setVerticalOffset(0);
239 lp.setHorizontalOffset(0);
Adam Cohen69d66e02011-01-12 14:39:13 -0800240 }
241
Adam Cohen50204582011-01-16 17:28:25 -0800242 if (fromIndex == -1 && toIndex == getNumActiveViews() -1) {
Adam Cohen50204582011-01-16 17:28:25 -0800243 transformViewAtIndex(toIndex, view, false);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700244 view.setVisibility(VISIBLE);
Adam Cohenc798b662011-10-21 12:07:38 -0700245 view.setAlpha(1.0f);
Adam Cohen96d8d562010-10-24 11:12:18 -0700246 } else if (fromIndex == 0 && toIndex == 1) {
Adam Cohen44729e32010-07-22 16:00:07 -0700247 // Slide item in
Adam Cohen78db1aa2011-01-25 12:24:23 -0800248 ((StackFrame) view).cancelSliderAnimator();
Adam Cohen44729e32010-07-22 16:00:07 -0700249 view.setVisibility(VISIBLE);
Adam Cohen32a42f12010-08-11 19:34:30 -0700250
Adam Cohen839f4a52010-08-26 17:36:48 -0700251 int duration = Math.round(mStackSlider.getDurationForNeutralPosition(mYVelocity));
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700252 StackSlider animationSlider = new StackSlider(mStackSlider);
Adam Cohen69d66e02011-01-12 14:39:13 -0800253 animationSlider.setView(view);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800254
255 if (animate) {
256 PropertyValuesHolder slideInY = PropertyValuesHolder.ofFloat("YProgress", 0.0f);
257 PropertyValuesHolder slideInX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
258 ObjectAnimator slideIn = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
259 slideInX, slideInY);
260 slideIn.setDuration(duration);
261 slideIn.setInterpolator(new LinearInterpolator());
262 ((StackFrame) view).setSliderAnimator(slideIn);
263 slideIn.start();
264 } else {
265 animationSlider.setYProgress(0f);
266 animationSlider.setXProgress(0f);
267 }
Adam Cohen96d8d562010-10-24 11:12:18 -0700268 } else if (fromIndex == 1 && toIndex == 0) {
Adam Cohen44729e32010-07-22 16:00:07 -0700269 // Slide item out
Adam Cohen78db1aa2011-01-25 12:24:23 -0800270 ((StackFrame) view).cancelSliderAnimator();
Adam Cohen839f4a52010-08-26 17:36:48 -0700271 int duration = Math.round(mStackSlider.getDurationForOffscreenPosition(mYVelocity));
Adam Cohen44729e32010-07-22 16:00:07 -0700272
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700273 StackSlider animationSlider = new StackSlider(mStackSlider);
Adam Cohen69d66e02011-01-12 14:39:13 -0800274 animationSlider.setView(view);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800275 if (animate) {
276 PropertyValuesHolder slideOutY = PropertyValuesHolder.ofFloat("YProgress", 1.0f);
277 PropertyValuesHolder slideOutX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
278 ObjectAnimator slideOut = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
279 slideOutX, slideOutY);
280 slideOut.setDuration(duration);
281 slideOut.setInterpolator(new LinearInterpolator());
282 ((StackFrame) view).setSliderAnimator(slideOut);
283 slideOut.start();
284 } else {
285 animationSlider.setYProgress(1.0f);
286 animationSlider.setXProgress(0f);
287 }
Adam Cohen69d66e02011-01-12 14:39:13 -0800288 } else if (toIndex == 0) {
Adam Cohen44729e32010-07-22 16:00:07 -0700289 // Make sure this view that is "waiting in the wings" is invisible
290 view.setAlpha(0.0f);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700291 view.setVisibility(INVISIBLE);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800292 } else if ((fromIndex == 0 || fromIndex == 1) && toIndex > 1) {
Adam Cohen69d66e02011-01-12 14:39:13 -0800293 view.setVisibility(VISIBLE);
294 view.setAlpha(1.0f);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800295 view.setRotationX(0f);
296 LayoutParams lp = (LayoutParams) view.getLayoutParams();
297 lp.setVerticalOffset(0);
298 lp.setHorizontalOffset(0);
Adam Cohenc0b53be2010-12-17 19:23:41 -0800299 } else if (fromIndex == -1) {
300 view.setAlpha(1.0f);
301 view.setVisibility(VISIBLE);
Adam Cohen44729e32010-07-22 16:00:07 -0700302 } else if (toIndex == -1) {
Adam Cohen78db1aa2011-01-25 12:24:23 -0800303 if (animate) {
Adam Cohenc798b662011-10-21 12:07:38 -0700304 postDelayed(new Runnable() {
305 public void run() {
306 view.setAlpha(0);
307 }
308 }, STACK_RELAYOUT_DURATION);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800309 } else {
310 view.setAlpha(0f);
311 }
Adam Cohen44729e32010-07-22 16:00:07 -0700312 }
Adam Cohen839f4a52010-08-26 17:36:48 -0700313
314 // Implement the faked perspective
315 if (toIndex != -1) {
Adam Cohen78db1aa2011-01-25 12:24:23 -0800316 transformViewAtIndex(toIndex, view, animate);
Adam Cohenf04e2252010-09-09 18:41:20 -0700317 }
318 }
Adam Cohen839f4a52010-08-26 17:36:48 -0700319
Adam Cohen36f43902010-12-15 21:17:33 -0800320 private void transformViewAtIndex(int index, final View view, boolean animate) {
321 final float maxPerspectiveShiftY = mPerspectiveShiftY;
322 final float maxPerspectiveShiftX = mPerspectiveShiftX;
Adam Cohen026e1212010-12-13 12:29:17 -0800323
Adam Cohenc99ff732011-01-16 16:11:41 -0800324 if (mStackMode == ITEMS_SLIDE_DOWN) {
325 index = mMaxNumActiveViews - index - 1;
326 if (index == mMaxNumActiveViews - 1) index--;
327 } else {
328 index--;
329 if (index < 0) index++;
330 }
Adam Cohen026e1212010-12-13 12:29:17 -0800331
332 float r = (index * 1.0f) / (mMaxNumActiveViews - 2);
333
Adam Cohen36f43902010-12-15 21:17:33 -0800334 final float scale = 1 - PERSPECTIVE_SCALE_FACTOR * (1 - r);
Adam Cohen026e1212010-12-13 12:29:17 -0800335
Adam Cohenc99ff732011-01-16 16:11:41 -0800336 float perspectiveTranslationY = r * maxPerspectiveShiftY;
337 float scaleShiftCorrectionY = (scale - 1) *
Adam Cohen026e1212010-12-13 12:29:17 -0800338 (getMeasuredHeight() * (1 - PERSPECTIVE_SHIFT_FACTOR_Y) / 2.0f);
Adam Cohen36f43902010-12-15 21:17:33 -0800339 final float transY = perspectiveTranslationY + scaleShiftCorrectionY;
Adam Cohen026e1212010-12-13 12:29:17 -0800340
341 float perspectiveTranslationX = (1 - r) * maxPerspectiveShiftX;
342 float scaleShiftCorrectionX = (1 - scale) *
343 (getMeasuredWidth() * (1 - PERSPECTIVE_SHIFT_FACTOR_X) / 2.0f);
Adam Cohen36f43902010-12-15 21:17:33 -0800344 final float transX = perspectiveTranslationX + scaleShiftCorrectionX;
Adam Cohen026e1212010-12-13 12:29:17 -0800345
Adam Cohenc99ff732011-01-16 16:11:41 -0800346 // If this view is currently being animated for a certain position, we need to cancel
Adam Cohen69d66e02011-01-12 14:39:13 -0800347 // this animation so as not to interfere with the new transformation.
Adam Cohen78db1aa2011-01-25 12:24:23 -0800348 if (view instanceof StackFrame) {
349 ((StackFrame) view).cancelTransformAnimator();
Adam Cohen69d66e02011-01-12 14:39:13 -0800350 }
351
Adam Cohen36f43902010-12-15 21:17:33 -0800352 if (animate) {
353 PropertyValuesHolder translationX = PropertyValuesHolder.ofFloat("translationX", transX);
354 PropertyValuesHolder translationY = PropertyValuesHolder.ofFloat("translationY", transY);
355 PropertyValuesHolder scalePropX = PropertyValuesHolder.ofFloat("scaleX", scale);
356 PropertyValuesHolder scalePropY = PropertyValuesHolder.ofFloat("scaleY", scale);
Adam Cohen026e1212010-12-13 12:29:17 -0800357
Adam Cohen36f43902010-12-15 21:17:33 -0800358 ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(view, scalePropX, scalePropY,
359 translationY, translationX);
Adam Cohenc0b53be2010-12-17 19:23:41 -0800360 oa.setDuration(STACK_RELAYOUT_DURATION);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800361 if (view instanceof StackFrame) {
362 ((StackFrame) view).setTransformAnimator(oa);
363 }
Adam Cohen36f43902010-12-15 21:17:33 -0800364 oa.start();
365 } else {
Adam Cohen36f43902010-12-15 21:17:33 -0800366 view.setTranslationX(transX);
367 view.setTranslationY(transY);
368 view.setScaleX(scale);
369 view.setScaleY(scale);
370 }
Adam Cohen026e1212010-12-13 12:29:17 -0800371 }
372
Adam Cohen3352b682010-10-26 13:54:00 -0700373 private void setupStackSlider(View v, int mode) {
374 mStackSlider.setMode(mode);
375 if (v != null) {
Adam Cohen26f072c2011-04-01 16:23:18 -0700376 mHighlight.setImageBitmap(sHolographicHelper.createResOutline(v, mResOutColor));
Adam Cohen3352b682010-10-26 13:54:00 -0700377 mHighlight.setRotation(v.getRotation());
378 mHighlight.setTranslationY(v.getTranslationY());
Adam Cohen026e1212010-12-13 12:29:17 -0800379 mHighlight.setTranslationX(v.getTranslationX());
Adam Cohen3352b682010-10-26 13:54:00 -0700380 mHighlight.bringToFront();
381 v.bringToFront();
382 mStackSlider.setView(v);
383
384 v.setVisibility(VISIBLE);
385 }
386 }
387
Adam Cohen0e2de6d2011-01-19 17:16:34 -0800388 /**
389 * {@inheritDoc}
390 */
Adam Cohen3352b682010-10-26 13:54:00 -0700391 @Override
392 @android.view.RemotableViewMethod
393 public void showNext() {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800394 if (mSwipeGestureType != GESTURE_NONE) return;
Adam Cohen3352b682010-10-26 13:54:00 -0700395 if (!mTransitionIsSetup) {
396 View v = getViewAtRelativeIndex(1);
397 if (v != null) {
398 setupStackSlider(v, StackSlider.NORMAL_MODE);
399 mStackSlider.setYProgress(0);
400 mStackSlider.setXProgress(0);
401 }
402 }
403 super.showNext();
404 }
405
Adam Cohen0e2de6d2011-01-19 17:16:34 -0800406 /**
407 * {@inheritDoc}
408 */
Adam Cohen3352b682010-10-26 13:54:00 -0700409 @Override
410 @android.view.RemotableViewMethod
411 public void showPrevious() {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800412 if (mSwipeGestureType != GESTURE_NONE) return;
Adam Cohen3352b682010-10-26 13:54:00 -0700413 if (!mTransitionIsSetup) {
414 View v = getViewAtRelativeIndex(0);
415 if (v != null) {
416 setupStackSlider(v, StackSlider.NORMAL_MODE);
417 mStackSlider.setYProgress(1);
418 mStackSlider.setXProgress(0);
419 }
420 }
421 super.showPrevious();
422 }
423
Adam Cohen96d8d562010-10-24 11:12:18 -0700424 @Override
Adam Cohenef17dd42011-01-20 17:20:57 -0800425 void showOnly(int childIndex, boolean animate) {
426 super.showOnly(childIndex, animate);
Adam Cohen96d8d562010-10-24 11:12:18 -0700427
428 // Here we need to make sure that the z-order of the children is correct
Adam Cohen3352b682010-10-26 13:54:00 -0700429 for (int i = mCurrentWindowEnd; i >= mCurrentWindowStart; i--) {
Adam Cohen96d8d562010-10-24 11:12:18 -0700430 int index = modulo(i, getWindowSize());
Adam Cohend38a0ce2011-04-06 13:20:42 -0700431 ViewAndMetaData vm = mViewsMap.get(index);
432 if (vm != null) {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800433 View v = mViewsMap.get(index).view;
434 if (v != null) v.bringToFront();
435 }
Adam Cohen96d8d562010-10-24 11:12:18 -0700436 }
Adam Cohen78db1aa2011-01-25 12:24:23 -0800437 if (mHighlight != null) {
438 mHighlight.bringToFront();
439 }
Adam Cohen3352b682010-10-26 13:54:00 -0700440 mTransitionIsSetup = false;
Adam Cohen8baf5df2010-11-11 15:23:41 -0800441 mClickFeedbackIsValid = false;
442 }
443
444 void updateClickFeedback() {
445 if (!mClickFeedbackIsValid) {
Adam Cohen9c295482010-11-18 15:19:48 -0800446 View v = getViewAtRelativeIndex(1);
Adam Cohen8baf5df2010-11-11 15:23:41 -0800447 if (v != null) {
Adam Cohen26f072c2011-04-01 16:23:18 -0700448 mClickFeedback.setImageBitmap(
449 sHolographicHelper.createClickOutline(v, mClickColor));
Adam Cohen8baf5df2010-11-11 15:23:41 -0800450 mClickFeedback.setTranslationX(v.getTranslationX());
451 mClickFeedback.setTranslationY(v.getTranslationY());
452 }
453 mClickFeedbackIsValid = true;
454 }
455 }
456
457 @Override
458 void showTapFeedback(View v) {
459 updateClickFeedback();
460 mClickFeedback.setVisibility(VISIBLE);
461 mClickFeedback.bringToFront();
462 invalidate();
463 }
464
465 @Override
466 void hideTapFeedback(View v) {
467 mClickFeedback.setVisibility(INVISIBLE);
468 invalidate();
Adam Cohen96d8d562010-10-24 11:12:18 -0700469 }
470
Adam Cohenf04e2252010-09-09 18:41:20 -0700471 private void updateChildTransforms() {
Adam Cohen96d8d562010-10-24 11:12:18 -0700472 for (int i = 0; i < getNumActiveViews(); i++) {
Adam Cohenf04e2252010-09-09 18:41:20 -0700473 View v = getViewAtRelativeIndex(i);
474 if (v != null) {
Adam Cohen36f43902010-12-15 21:17:33 -0800475 transformViewAtIndex(i, v, false);
Adam Cohenf04e2252010-09-09 18:41:20 -0700476 }
Adam Cohen839f4a52010-08-26 17:36:48 -0700477 }
Adam Cohen44729e32010-07-22 16:00:07 -0700478 }
479
Adam Cohen78db1aa2011-01-25 12:24:23 -0800480 private static class StackFrame extends FrameLayout {
Adam Cohen78db1aa2011-01-25 12:24:23 -0800481 WeakReference<ObjectAnimator> transformAnimator;
482 WeakReference<ObjectAnimator> sliderAnimator;
483
484 public StackFrame(Context context) {
485 super(context);
486 }
487
Adam Cohen78db1aa2011-01-25 12:24:23 -0800488 void setTransformAnimator(ObjectAnimator oa) {
489 transformAnimator = new WeakReference<ObjectAnimator>(oa);
490 }
491
492 void setSliderAnimator(ObjectAnimator oa) {
493 sliderAnimator = new WeakReference<ObjectAnimator>(oa);
494 }
495
Adam Cohen78db1aa2011-01-25 12:24:23 -0800496 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
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001229 @Override
1230 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
1231 super.onInitializeAccessibilityEvent(event);
1232 event.setClassName(StackView.class.getName());
1233 }
1234
1235 @Override
1236 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
1237 super.onInitializeAccessibilityNodeInfo(info);
1238 info.setClassName(StackView.class.getName());
Svetoslav Ganov48d15862012-05-15 10:10:00 -07001239 info.setScrollable(getChildCount() > 1);
Svetoslav Ganovfb1e80a2012-05-16 17:33:19 -07001240 if (isEnabled()) {
1241 if (getDisplayedChild() < getChildCount() - 1) {
1242 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
1243 }
1244 if (getDisplayedChild() > 0) {
1245 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
1246 }
Svetoslav Ganov48d15862012-05-15 10:10:00 -07001247 }
1248 }
1249
1250 @Override
1251 public boolean performAccessibilityAction(int action, Bundle arguments) {
1252 if (super.performAccessibilityAction(action, arguments)) {
1253 return true;
1254 }
Svetoslav Ganovfb1e80a2012-05-16 17:33:19 -07001255 if (!isEnabled()) {
1256 return false;
1257 }
Svetoslav Ganov48d15862012-05-15 10:10:00 -07001258 switch (action) {
1259 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
1260 if (getDisplayedChild() < getChildCount() - 1) {
1261 showNext();
1262 return true;
1263 }
1264 } return false;
1265 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
1266 if (getDisplayedChild() > 0) {
1267 showPrevious();
1268 return true;
1269 }
1270 } return false;
1271 }
1272 return false;
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001273 }
1274
Adam Cohen9b073942010-08-19 16:49:52 -07001275 class LayoutParams extends ViewGroup.LayoutParams {
1276 int horizontalOffset;
1277 int verticalOffset;
1278 View mView;
Adam Cohend51bbb52010-10-18 10:59:49 -07001279 private final Rect parentRect = new Rect();
1280 private final Rect invalidateRect = new Rect();
1281 private final RectF invalidateRectf = new RectF();
1282 private final Rect globalInvalidateRect = new Rect();
Adam Cohen32a42f12010-08-11 19:34:30 -07001283
Adam Cohen9b073942010-08-19 16:49:52 -07001284 LayoutParams(View view) {
1285 super(0, 0);
Adam Cohen839f4a52010-08-26 17:36:48 -07001286 width = 0;
1287 height = 0;
Adam Cohen9b073942010-08-19 16:49:52 -07001288 horizontalOffset = 0;
1289 verticalOffset = 0;
1290 mView = view;
1291 }
Adam Cohen32a42f12010-08-11 19:34:30 -07001292
Adam Cohen9b073942010-08-19 16:49:52 -07001293 LayoutParams(Context c, AttributeSet attrs) {
1294 super(c, attrs);
1295 horizontalOffset = 0;
1296 verticalOffset = 0;
Adam Cohen839f4a52010-08-26 17:36:48 -07001297 width = 0;
1298 height = 0;
Adam Cohen9b073942010-08-19 16:49:52 -07001299 }
Adam Cohen32a42f12010-08-11 19:34:30 -07001300
Adam Cohen9b073942010-08-19 16:49:52 -07001301 void invalidateGlobalRegion(View v, Rect r) {
Adam Cohend51bbb52010-10-18 10:59:49 -07001302 // We need to make a new rect here, so as not to modify the one passed
1303 globalInvalidateRect.set(r);
Adam Cohen1ff65d12011-02-16 14:04:09 -08001304 globalInvalidateRect.union(0, 0, getWidth(), getHeight());
Adam Cohen9b073942010-08-19 16:49:52 -07001305 View p = v;
1306 if (!(v.getParent() != null && v.getParent() instanceof View)) return;
1307
Adam Cohen9b073942010-08-19 16:49:52 -07001308 boolean firstPass = true;
1309 parentRect.set(0, 0, 0, 0);
Adam Cohenb7f4d032010-09-16 15:25:54 -07001310 while (p.getParent() != null && p.getParent() instanceof View
Adam Cohend51bbb52010-10-18 10:59:49 -07001311 && !parentRect.contains(globalInvalidateRect)) {
Adam Cohen9b073942010-08-19 16:49:52 -07001312 if (!firstPass) {
Adam Cohend51bbb52010-10-18 10:59:49 -07001313 globalInvalidateRect.offset(p.getLeft() - p.getScrollX(), p.getTop()
1314 - p.getScrollY());
Adam Cohen9b073942010-08-19 16:49:52 -07001315 }
1316 firstPass = false;
1317 p = (View) p.getParent();
Adam Cohenb7f4d032010-09-16 15:25:54 -07001318 parentRect.set(p.getScrollX(), p.getScrollY(),
Adam Cohen1ff65d12011-02-16 14:04:09 -08001319 p.getWidth() + p.getScrollX(), p.getHeight() + p.getScrollY());
1320 p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top,
1321 globalInvalidateRect.right, globalInvalidateRect.bottom);
Adam Cohen9b073942010-08-19 16:49:52 -07001322 }
1323
Adam Cohend51bbb52010-10-18 10:59:49 -07001324 p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top,
1325 globalInvalidateRect.right, globalInvalidateRect.bottom);
Adam Cohen9b073942010-08-19 16:49:52 -07001326 }
1327
Adam Cohend51bbb52010-10-18 10:59:49 -07001328 Rect getInvalidateRect() {
1329 return invalidateRect;
1330 }
1331
1332 void resetInvalidateRect() {
Adam Cohen0ac116b2010-11-11 11:39:53 -08001333 invalidateRect.set(0, 0, 0, 0);
Adam Cohend51bbb52010-10-18 10:59:49 -07001334 }
1335
Chet Haasea18a86b2010-09-07 13:20:00 -07001336 // This is public so that ObjectAnimator can access it
Adam Cohen9b073942010-08-19 16:49:52 -07001337 public void setVerticalOffset(int newVerticalOffset) {
Adam Cohen0ac116b2010-11-11 11:39:53 -08001338 setOffsets(horizontalOffset, newVerticalOffset);
1339 }
1340
1341 public void setHorizontalOffset(int newHorizontalOffset) {
1342 setOffsets(newHorizontalOffset, verticalOffset);
1343 }
1344
1345 public void setOffsets(int newHorizontalOffset, int newVerticalOffset) {
1346 int horizontalOffsetDelta = newHorizontalOffset - horizontalOffset;
1347 horizontalOffset = newHorizontalOffset;
1348 int verticalOffsetDelta = newVerticalOffset - verticalOffset;
Adam Cohen9b073942010-08-19 16:49:52 -07001349 verticalOffset = newVerticalOffset;
1350
1351 if (mView != null) {
1352 mView.requestLayout();
Adam Cohen0ac116b2010-11-11 11:39:53 -08001353 int left = Math.min(mView.getLeft() + horizontalOffsetDelta, mView.getLeft());
1354 int right = Math.max(mView.getRight() + horizontalOffsetDelta, mView.getRight());
1355 int top = Math.min(mView.getTop() + verticalOffsetDelta, mView.getTop());
1356 int bottom = Math.max(mView.getBottom() + verticalOffsetDelta, mView.getBottom());
Adam Cohen9b073942010-08-19 16:49:52 -07001357
Adam Cohen0ac116b2010-11-11 11:39:53 -08001358 invalidateRectf.set(left, top, right, bottom);
Adam Cohen9b073942010-08-19 16:49:52 -07001359
1360 float xoffset = -invalidateRectf.left;
1361 float yoffset = -invalidateRectf.top;
1362 invalidateRectf.offset(xoffset, yoffset);
1363 mView.getMatrix().mapRect(invalidateRectf);
1364 invalidateRectf.offset(-xoffset, -yoffset);
1365
Adam Cohen0ac116b2010-11-11 11:39:53 -08001366 invalidateRect.set((int) Math.floor(invalidateRectf.left),
Adam Cohen9b073942010-08-19 16:49:52 -07001367 (int) Math.floor(invalidateRectf.top),
1368 (int) Math.ceil(invalidateRectf.right),
1369 (int) Math.ceil(invalidateRectf.bottom));
1370
1371 invalidateGlobalRegion(mView, invalidateRect);
1372 }
1373 }
1374 }
1375
1376 private static class HolographicHelper {
1377 private final Paint mHolographicPaint = new Paint();
1378 private final Paint mErasePaint = new Paint();
Adam Cohen839f4a52010-08-26 17:36:48 -07001379 private final Paint mBlurPaint = new Paint();
Adam Cohen8baf5df2010-11-11 15:23:41 -08001380 private static final int RES_OUT = 0;
1381 private static final int CLICK_FEEDBACK = 1;
1382 private float mDensity;
Patrick Dubroye80202d2010-11-16 15:41:08 -08001383 private BlurMaskFilter mSmallBlurMaskFilter;
1384 private BlurMaskFilter mLargeBlurMaskFilter;
1385 private final Canvas mCanvas = new Canvas();
1386 private final Canvas mMaskCanvas = new Canvas();
1387 private final int[] mTmpXY = new int[2];
1388 private final Matrix mIdentityMatrix = new Matrix();
Adam Cohen9b073942010-08-19 16:49:52 -07001389
Adam Cohendfcdddd2010-09-10 14:38:40 -07001390 HolographicHelper(Context context) {
Adam Cohen8baf5df2010-11-11 15:23:41 -08001391 mDensity = context.getResources().getDisplayMetrics().density;
Adam Cohendfcdddd2010-09-10 14:38:40 -07001392
Adam Cohen9b073942010-08-19 16:49:52 -07001393 mHolographicPaint.setFilterBitmap(true);
Adam Cohen839f4a52010-08-26 17:36:48 -07001394 mHolographicPaint.setMaskFilter(TableMaskFilter.CreateClipTable(0, 30));
Adam Cohen9b073942010-08-19 16:49:52 -07001395 mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
1396 mErasePaint.setFilterBitmap(true);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001397
1398 mSmallBlurMaskFilter = new BlurMaskFilter(2 * mDensity, BlurMaskFilter.Blur.NORMAL);
1399 mLargeBlurMaskFilter = new BlurMaskFilter(4 * mDensity, BlurMaskFilter.Blur.NORMAL);
Adam Cohen9b073942010-08-19 16:49:52 -07001400 }
1401
Adam Cohen26f072c2011-04-01 16:23:18 -07001402 Bitmap createClickOutline(View v, int color) {
1403 return createOutline(v, CLICK_FEEDBACK, color);
Adam Cohen8baf5df2010-11-11 15:23:41 -08001404 }
1405
Adam Cohen26f072c2011-04-01 16:23:18 -07001406 Bitmap createResOutline(View v, int color) {
1407 return createOutline(v, RES_OUT, color);
1408 }
1409
1410 Bitmap createOutline(View v, int type, int color) {
1411 mHolographicPaint.setColor(color);
Adam Cohen8baf5df2010-11-11 15:23:41 -08001412 if (type == RES_OUT) {
Patrick Dubroye80202d2010-11-16 15:41:08 -08001413 mBlurPaint.setMaskFilter(mSmallBlurMaskFilter);
Adam Cohen8baf5df2010-11-11 15:23:41 -08001414 } else if (type == CLICK_FEEDBACK) {
Patrick Dubroye80202d2010-11-16 15:41:08 -08001415 mBlurPaint.setMaskFilter(mLargeBlurMaskFilter);
Adam Cohen8baf5df2010-11-11 15:23:41 -08001416 }
1417
Adam Cohen9b073942010-08-19 16:49:52 -07001418 if (v.getMeasuredWidth() == 0 || v.getMeasuredHeight() == 0) {
1419 return null;
1420 }
1421
Dianne Hackborndde331c2012-08-03 14:01:57 -07001422 Bitmap bitmap = Bitmap.createBitmap(v.getResources().getDisplayMetrics(),
1423 v.getMeasuredWidth(), v.getMeasuredHeight(), Bitmap.Config.ARGB_8888);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001424 mCanvas.setBitmap(bitmap);
Adam Cohen9b073942010-08-19 16:49:52 -07001425
1426 float rotationX = v.getRotationX();
Adam Cohen839f4a52010-08-26 17:36:48 -07001427 float rotation = v.getRotation();
1428 float translationY = v.getTranslationY();
Adam Cohen026e1212010-12-13 12:29:17 -08001429 float translationX = v.getTranslationX();
Adam Cohen9b073942010-08-19 16:49:52 -07001430 v.setRotationX(0);
Adam Cohen839f4a52010-08-26 17:36:48 -07001431 v.setRotation(0);
1432 v.setTranslationY(0);
Adam Cohen026e1212010-12-13 12:29:17 -08001433 v.setTranslationX(0);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001434 v.draw(mCanvas);
Adam Cohen9b073942010-08-19 16:49:52 -07001435 v.setRotationX(rotationX);
Adam Cohen839f4a52010-08-26 17:36:48 -07001436 v.setRotation(rotation);
1437 v.setTranslationY(translationY);
Adam Cohen026e1212010-12-13 12:29:17 -08001438 v.setTranslationX(translationX);
Adam Cohen9b073942010-08-19 16:49:52 -07001439
Patrick Dubroye80202d2010-11-16 15:41:08 -08001440 drawOutline(mCanvas, bitmap);
Dianne Hackborn6311d0a2011-08-02 16:37:58 -07001441 mCanvas.setBitmap(null);
Adam Cohen9b073942010-08-19 16:49:52 -07001442 return bitmap;
1443 }
1444
Adam Cohen9b073942010-08-19 16:49:52 -07001445 void drawOutline(Canvas dest, Bitmap src) {
Patrick Dubroye80202d2010-11-16 15:41:08 -08001446 final int[] xy = mTmpXY;
Adam Cohen839f4a52010-08-26 17:36:48 -07001447 Bitmap mask = src.extractAlpha(mBlurPaint, xy);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001448 mMaskCanvas.setBitmap(mask);
1449 mMaskCanvas.drawBitmap(src, -xy[0], -xy[1], mErasePaint);
Adam Cohen9b073942010-08-19 16:49:52 -07001450 dest.drawColor(0, PorterDuff.Mode.CLEAR);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001451 dest.setMatrix(mIdentityMatrix);
Adam Cohen839f4a52010-08-26 17:36:48 -07001452 dest.drawBitmap(mask, xy[0], xy[1], mHolographicPaint);
Dianne Hackborn6311d0a2011-08-02 16:37:58 -07001453 mMaskCanvas.setBitmap(null);
Adam Cohen9b073942010-08-19 16:49:52 -07001454 mask.recycle();
1455 }
Adam Cohen32a42f12010-08-11 19:34:30 -07001456 }
Adam Cohenef17dd42011-01-20 17:20:57 -08001457}