blob: 68536604bf676b158af783b1771332da876d2a84 [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) {
171 super(context, attrs, defStyleAttr);
172 TypedArray a = context.obtainStyledAttributes(attrs,
173 com.android.internal.R.styleable.StackView, defStyleAttr, 0);
174
175 mResOutColor = a.getColor(
176 com.android.internal.R.styleable.StackView_resOutColor, 0);
177 mClickColor = a.getColor(
178 com.android.internal.R.styleable.StackView_clickColor, 0);
179
180 a.recycle();
Adam Cohen44729e32010-07-22 16:00:07 -0700181 initStackView();
182 }
183
184 private void initStackView() {
Adam Cohen96d8d562010-10-24 11:12:18 -0700185 configureViewAnimator(NUM_ACTIVE_VIEWS, 1);
Adam Cohen44729e32010-07-22 16:00:07 -0700186 setStaticTransformationsEnabled(true);
187 final ViewConfiguration configuration = ViewConfiguration.get(getContext());
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700188 mTouchSlop = configuration.getScaledTouchSlop();
Adam Cohen44729e32010-07-22 16:00:07 -0700189 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
190 mActivePointerId = INVALID_POINTER;
Adam Cohen32a42f12010-08-11 19:34:30 -0700191
192 mHighlight = new ImageView(getContext());
193 mHighlight.setLayoutParams(new LayoutParams(mHighlight));
194 addViewInLayout(mHighlight, -1, new LayoutParams(mHighlight));
Adam Cohen8baf5df2010-11-11 15:23:41 -0800195
196 mClickFeedback = new ImageView(getContext());
197 mClickFeedback.setLayoutParams(new LayoutParams(mClickFeedback));
198 addViewInLayout(mClickFeedback, -1, new LayoutParams(mClickFeedback));
199 mClickFeedback.setVisibility(INVISIBLE);
200
Adam Cohen32a42f12010-08-11 19:34:30 -0700201 mStackSlider = new StackSlider();
202
Adam Cohen9b073942010-08-19 16:49:52 -0700203 if (sHolographicHelper == null) {
Adam Cohendfcdddd2010-09-10 14:38:40 -0700204 sHolographicHelper = new HolographicHelper(mContext);
Adam Cohen32a42f12010-08-11 19:34:30 -0700205 }
Adam Cohen9b073942010-08-19 16:49:52 -0700206 setClipChildren(false);
207 setClipToPadding(false);
Adam Cohen1480fdd2010-08-25 17:24:53 -0700208
Adam Cohen839f4a52010-08-26 17:36:48 -0700209 // This sets the form of the StackView, which is currently to have the perspective-shifted
210 // views above the active view, and have items slide down when sliding out. The opposite is
211 // available by using ITEMS_SLIDE_UP.
212 mStackMode = ITEMS_SLIDE_DOWN;
213
Adam Cohen1480fdd2010-08-25 17:24:53 -0700214 // This is a flag to indicate the the stack is loading for the first time
215 mWhichChild = -1;
Adam Cohendfcdddd2010-09-10 14:38:40 -0700216
217 // Adjust the frame padding based on the density, since the highlight changes based
218 // on the density
219 final float density = mContext.getResources().getDisplayMetrics().density;
220 mFramePadding = (int) Math.ceil(density * FRAME_PADDING);
Adam Cohen44729e32010-07-22 16:00:07 -0700221 }
222
223 /**
224 * Animate the views between different relative indexes within the {@link AdapterViewAnimator}
225 */
Adam Cohen78db1aa2011-01-25 12:24:23 -0800226 void transformViewForTransition(int fromIndex, int toIndex, final View view, boolean animate) {
Adam Cohen78db1aa2011-01-25 12:24:23 -0800227 if (!animate) {
228 ((StackFrame) view).cancelSliderAnimator();
229 view.setRotationX(0f);
230 LayoutParams lp = (LayoutParams) view.getLayoutParams();
231 lp.setVerticalOffset(0);
232 lp.setHorizontalOffset(0);
Adam Cohen69d66e02011-01-12 14:39:13 -0800233 }
234
Adam Cohen50204582011-01-16 17:28:25 -0800235 if (fromIndex == -1 && toIndex == getNumActiveViews() -1) {
Adam Cohen50204582011-01-16 17:28:25 -0800236 transformViewAtIndex(toIndex, view, false);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700237 view.setVisibility(VISIBLE);
Adam Cohenc798b662011-10-21 12:07:38 -0700238 view.setAlpha(1.0f);
Adam Cohen96d8d562010-10-24 11:12:18 -0700239 } else if (fromIndex == 0 && toIndex == 1) {
Adam Cohen44729e32010-07-22 16:00:07 -0700240 // Slide item in
Adam Cohen78db1aa2011-01-25 12:24:23 -0800241 ((StackFrame) view).cancelSliderAnimator();
Adam Cohen44729e32010-07-22 16:00:07 -0700242 view.setVisibility(VISIBLE);
Adam Cohen32a42f12010-08-11 19:34:30 -0700243
Adam Cohen839f4a52010-08-26 17:36:48 -0700244 int duration = Math.round(mStackSlider.getDurationForNeutralPosition(mYVelocity));
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700245 StackSlider animationSlider = new StackSlider(mStackSlider);
Adam Cohen69d66e02011-01-12 14:39:13 -0800246 animationSlider.setView(view);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800247
248 if (animate) {
249 PropertyValuesHolder slideInY = PropertyValuesHolder.ofFloat("YProgress", 0.0f);
250 PropertyValuesHolder slideInX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
251 ObjectAnimator slideIn = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
252 slideInX, slideInY);
253 slideIn.setDuration(duration);
254 slideIn.setInterpolator(new LinearInterpolator());
255 ((StackFrame) view).setSliderAnimator(slideIn);
256 slideIn.start();
257 } else {
258 animationSlider.setYProgress(0f);
259 animationSlider.setXProgress(0f);
260 }
Adam Cohen96d8d562010-10-24 11:12:18 -0700261 } else if (fromIndex == 1 && toIndex == 0) {
Adam Cohen44729e32010-07-22 16:00:07 -0700262 // Slide item out
Adam Cohen78db1aa2011-01-25 12:24:23 -0800263 ((StackFrame) view).cancelSliderAnimator();
Adam Cohen839f4a52010-08-26 17:36:48 -0700264 int duration = Math.round(mStackSlider.getDurationForOffscreenPosition(mYVelocity));
Adam Cohen44729e32010-07-22 16:00:07 -0700265
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700266 StackSlider animationSlider = new StackSlider(mStackSlider);
Adam Cohen69d66e02011-01-12 14:39:13 -0800267 animationSlider.setView(view);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800268 if (animate) {
269 PropertyValuesHolder slideOutY = PropertyValuesHolder.ofFloat("YProgress", 1.0f);
270 PropertyValuesHolder slideOutX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
271 ObjectAnimator slideOut = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
272 slideOutX, slideOutY);
273 slideOut.setDuration(duration);
274 slideOut.setInterpolator(new LinearInterpolator());
275 ((StackFrame) view).setSliderAnimator(slideOut);
276 slideOut.start();
277 } else {
278 animationSlider.setYProgress(1.0f);
279 animationSlider.setXProgress(0f);
280 }
Adam Cohen69d66e02011-01-12 14:39:13 -0800281 } else if (toIndex == 0) {
Adam Cohen44729e32010-07-22 16:00:07 -0700282 // Make sure this view that is "waiting in the wings" is invisible
283 view.setAlpha(0.0f);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700284 view.setVisibility(INVISIBLE);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800285 } else if ((fromIndex == 0 || fromIndex == 1) && toIndex > 1) {
Adam Cohen69d66e02011-01-12 14:39:13 -0800286 view.setVisibility(VISIBLE);
287 view.setAlpha(1.0f);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800288 view.setRotationX(0f);
289 LayoutParams lp = (LayoutParams) view.getLayoutParams();
290 lp.setVerticalOffset(0);
291 lp.setHorizontalOffset(0);
Adam Cohenc0b53be2010-12-17 19:23:41 -0800292 } else if (fromIndex == -1) {
293 view.setAlpha(1.0f);
294 view.setVisibility(VISIBLE);
Adam Cohen44729e32010-07-22 16:00:07 -0700295 } else if (toIndex == -1) {
Adam Cohen78db1aa2011-01-25 12:24:23 -0800296 if (animate) {
Adam Cohenc798b662011-10-21 12:07:38 -0700297 postDelayed(new Runnable() {
298 public void run() {
299 view.setAlpha(0);
300 }
301 }, STACK_RELAYOUT_DURATION);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800302 } else {
303 view.setAlpha(0f);
304 }
Adam Cohen44729e32010-07-22 16:00:07 -0700305 }
Adam Cohen839f4a52010-08-26 17:36:48 -0700306
307 // Implement the faked perspective
308 if (toIndex != -1) {
Adam Cohen78db1aa2011-01-25 12:24:23 -0800309 transformViewAtIndex(toIndex, view, animate);
Adam Cohenf04e2252010-09-09 18:41:20 -0700310 }
311 }
Adam Cohen839f4a52010-08-26 17:36:48 -0700312
Adam Cohen36f43902010-12-15 21:17:33 -0800313 private void transformViewAtIndex(int index, final View view, boolean animate) {
314 final float maxPerspectiveShiftY = mPerspectiveShiftY;
315 final float maxPerspectiveShiftX = mPerspectiveShiftX;
Adam Cohen026e1212010-12-13 12:29:17 -0800316
Adam Cohenc99ff732011-01-16 16:11:41 -0800317 if (mStackMode == ITEMS_SLIDE_DOWN) {
318 index = mMaxNumActiveViews - index - 1;
319 if (index == mMaxNumActiveViews - 1) index--;
320 } else {
321 index--;
322 if (index < 0) index++;
323 }
Adam Cohen026e1212010-12-13 12:29:17 -0800324
325 float r = (index * 1.0f) / (mMaxNumActiveViews - 2);
326
Adam Cohen36f43902010-12-15 21:17:33 -0800327 final float scale = 1 - PERSPECTIVE_SCALE_FACTOR * (1 - r);
Adam Cohen026e1212010-12-13 12:29:17 -0800328
Adam Cohenc99ff732011-01-16 16:11:41 -0800329 float perspectiveTranslationY = r * maxPerspectiveShiftY;
330 float scaleShiftCorrectionY = (scale - 1) *
Adam Cohen026e1212010-12-13 12:29:17 -0800331 (getMeasuredHeight() * (1 - PERSPECTIVE_SHIFT_FACTOR_Y) / 2.0f);
Adam Cohen36f43902010-12-15 21:17:33 -0800332 final float transY = perspectiveTranslationY + scaleShiftCorrectionY;
Adam Cohen026e1212010-12-13 12:29:17 -0800333
334 float perspectiveTranslationX = (1 - r) * maxPerspectiveShiftX;
335 float scaleShiftCorrectionX = (1 - scale) *
336 (getMeasuredWidth() * (1 - PERSPECTIVE_SHIFT_FACTOR_X) / 2.0f);
Adam Cohen36f43902010-12-15 21:17:33 -0800337 final float transX = perspectiveTranslationX + scaleShiftCorrectionX;
Adam Cohen026e1212010-12-13 12:29:17 -0800338
Adam Cohenc99ff732011-01-16 16:11:41 -0800339 // If this view is currently being animated for a certain position, we need to cancel
Adam Cohen69d66e02011-01-12 14:39:13 -0800340 // this animation so as not to interfere with the new transformation.
Adam Cohen78db1aa2011-01-25 12:24:23 -0800341 if (view instanceof StackFrame) {
342 ((StackFrame) view).cancelTransformAnimator();
Adam Cohen69d66e02011-01-12 14:39:13 -0800343 }
344
Adam Cohen36f43902010-12-15 21:17:33 -0800345 if (animate) {
346 PropertyValuesHolder translationX = PropertyValuesHolder.ofFloat("translationX", transX);
347 PropertyValuesHolder translationY = PropertyValuesHolder.ofFloat("translationY", transY);
348 PropertyValuesHolder scalePropX = PropertyValuesHolder.ofFloat("scaleX", scale);
349 PropertyValuesHolder scalePropY = PropertyValuesHolder.ofFloat("scaleY", scale);
Adam Cohen026e1212010-12-13 12:29:17 -0800350
Adam Cohen36f43902010-12-15 21:17:33 -0800351 ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(view, scalePropX, scalePropY,
352 translationY, translationX);
Adam Cohenc0b53be2010-12-17 19:23:41 -0800353 oa.setDuration(STACK_RELAYOUT_DURATION);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800354 if (view instanceof StackFrame) {
355 ((StackFrame) view).setTransformAnimator(oa);
356 }
Adam Cohen36f43902010-12-15 21:17:33 -0800357 oa.start();
358 } else {
Adam Cohen36f43902010-12-15 21:17:33 -0800359 view.setTranslationX(transX);
360 view.setTranslationY(transY);
361 view.setScaleX(scale);
362 view.setScaleY(scale);
363 }
Adam Cohen026e1212010-12-13 12:29:17 -0800364 }
365
Adam Cohen3352b682010-10-26 13:54:00 -0700366 private void setupStackSlider(View v, int mode) {
367 mStackSlider.setMode(mode);
368 if (v != null) {
Adam Cohen26f072c2011-04-01 16:23:18 -0700369 mHighlight.setImageBitmap(sHolographicHelper.createResOutline(v, mResOutColor));
Adam Cohen3352b682010-10-26 13:54:00 -0700370 mHighlight.setRotation(v.getRotation());
371 mHighlight.setTranslationY(v.getTranslationY());
Adam Cohen026e1212010-12-13 12:29:17 -0800372 mHighlight.setTranslationX(v.getTranslationX());
Adam Cohen3352b682010-10-26 13:54:00 -0700373 mHighlight.bringToFront();
374 v.bringToFront();
375 mStackSlider.setView(v);
376
377 v.setVisibility(VISIBLE);
378 }
379 }
380
Adam Cohen0e2de6d2011-01-19 17:16:34 -0800381 /**
382 * {@inheritDoc}
383 */
Adam Cohen3352b682010-10-26 13:54:00 -0700384 @Override
385 @android.view.RemotableViewMethod
386 public void showNext() {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800387 if (mSwipeGestureType != GESTURE_NONE) return;
Adam Cohen3352b682010-10-26 13:54:00 -0700388 if (!mTransitionIsSetup) {
389 View v = getViewAtRelativeIndex(1);
390 if (v != null) {
391 setupStackSlider(v, StackSlider.NORMAL_MODE);
392 mStackSlider.setYProgress(0);
393 mStackSlider.setXProgress(0);
394 }
395 }
396 super.showNext();
397 }
398
Adam Cohen0e2de6d2011-01-19 17:16:34 -0800399 /**
400 * {@inheritDoc}
401 */
Adam Cohen3352b682010-10-26 13:54:00 -0700402 @Override
403 @android.view.RemotableViewMethod
404 public void showPrevious() {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800405 if (mSwipeGestureType != GESTURE_NONE) return;
Adam Cohen3352b682010-10-26 13:54:00 -0700406 if (!mTransitionIsSetup) {
407 View v = getViewAtRelativeIndex(0);
408 if (v != null) {
409 setupStackSlider(v, StackSlider.NORMAL_MODE);
410 mStackSlider.setYProgress(1);
411 mStackSlider.setXProgress(0);
412 }
413 }
414 super.showPrevious();
415 }
416
Adam Cohen96d8d562010-10-24 11:12:18 -0700417 @Override
Adam Cohenef17dd42011-01-20 17:20:57 -0800418 void showOnly(int childIndex, boolean animate) {
419 super.showOnly(childIndex, animate);
Adam Cohen96d8d562010-10-24 11:12:18 -0700420
421 // Here we need to make sure that the z-order of the children is correct
Adam Cohen3352b682010-10-26 13:54:00 -0700422 for (int i = mCurrentWindowEnd; i >= mCurrentWindowStart; i--) {
Adam Cohen96d8d562010-10-24 11:12:18 -0700423 int index = modulo(i, getWindowSize());
Adam Cohend38a0ce2011-04-06 13:20:42 -0700424 ViewAndMetaData vm = mViewsMap.get(index);
425 if (vm != null) {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800426 View v = mViewsMap.get(index).view;
427 if (v != null) v.bringToFront();
428 }
Adam Cohen96d8d562010-10-24 11:12:18 -0700429 }
Adam Cohen78db1aa2011-01-25 12:24:23 -0800430 if (mHighlight != null) {
431 mHighlight.bringToFront();
432 }
Adam Cohen3352b682010-10-26 13:54:00 -0700433 mTransitionIsSetup = false;
Adam Cohen8baf5df2010-11-11 15:23:41 -0800434 mClickFeedbackIsValid = false;
435 }
436
437 void updateClickFeedback() {
438 if (!mClickFeedbackIsValid) {
Adam Cohen9c295482010-11-18 15:19:48 -0800439 View v = getViewAtRelativeIndex(1);
Adam Cohen8baf5df2010-11-11 15:23:41 -0800440 if (v != null) {
Adam Cohen26f072c2011-04-01 16:23:18 -0700441 mClickFeedback.setImageBitmap(
442 sHolographicHelper.createClickOutline(v, mClickColor));
Adam Cohen8baf5df2010-11-11 15:23:41 -0800443 mClickFeedback.setTranslationX(v.getTranslationX());
444 mClickFeedback.setTranslationY(v.getTranslationY());
445 }
446 mClickFeedbackIsValid = true;
447 }
448 }
449
450 @Override
451 void showTapFeedback(View v) {
452 updateClickFeedback();
453 mClickFeedback.setVisibility(VISIBLE);
454 mClickFeedback.bringToFront();
455 invalidate();
456 }
457
458 @Override
459 void hideTapFeedback(View v) {
460 mClickFeedback.setVisibility(INVISIBLE);
461 invalidate();
Adam Cohen96d8d562010-10-24 11:12:18 -0700462 }
463
Adam Cohenf04e2252010-09-09 18:41:20 -0700464 private void updateChildTransforms() {
Adam Cohen96d8d562010-10-24 11:12:18 -0700465 for (int i = 0; i < getNumActiveViews(); i++) {
Adam Cohenf04e2252010-09-09 18:41:20 -0700466 View v = getViewAtRelativeIndex(i);
467 if (v != null) {
Adam Cohen36f43902010-12-15 21:17:33 -0800468 transformViewAtIndex(i, v, false);
Adam Cohenf04e2252010-09-09 18:41:20 -0700469 }
Adam Cohen839f4a52010-08-26 17:36:48 -0700470 }
Adam Cohen44729e32010-07-22 16:00:07 -0700471 }
472
Adam Cohen78db1aa2011-01-25 12:24:23 -0800473 private static class StackFrame extends FrameLayout {
Adam Cohen78db1aa2011-01-25 12:24:23 -0800474 WeakReference<ObjectAnimator> transformAnimator;
475 WeakReference<ObjectAnimator> sliderAnimator;
476
477 public StackFrame(Context context) {
478 super(context);
479 }
480
Adam Cohen78db1aa2011-01-25 12:24:23 -0800481 void setTransformAnimator(ObjectAnimator oa) {
482 transformAnimator = new WeakReference<ObjectAnimator>(oa);
483 }
484
485 void setSliderAnimator(ObjectAnimator oa) {
486 sliderAnimator = new WeakReference<ObjectAnimator>(oa);
487 }
488
Adam Cohen78db1aa2011-01-25 12:24:23 -0800489 boolean cancelTransformAnimator() {
490 if (transformAnimator != null) {
491 ObjectAnimator oa = transformAnimator.get();
492 if (oa != null) {
493 oa.cancel();
494 return true;
495 }
496 }
497 return false;
498 }
499
500 boolean cancelSliderAnimator() {
501 if (sliderAnimator != null) {
502 ObjectAnimator oa = sliderAnimator.get();
503 if (oa != null) {
504 oa.cancel();
505 return true;
506 }
507 }
508 return false;
509 }
510 }
511
Adam Cohendfcdddd2010-09-10 14:38:40 -0700512 @Override
513 FrameLayout getFrameForChild() {
Adam Cohen78db1aa2011-01-25 12:24:23 -0800514 StackFrame fl = new StackFrame(mContext);
Adam Cohendfcdddd2010-09-10 14:38:40 -0700515 fl.setPadding(mFramePadding, mFramePadding, mFramePadding, mFramePadding);
516 return fl;
517 }
518
Adam Cohen44729e32010-07-22 16:00:07 -0700519 /**
520 * Apply any necessary tranforms for the child that is being added.
521 */
522 void applyTransformForChildAtIndex(View child, int relativeIndex) {
Adam Cohen44729e32010-07-22 16:00:07 -0700523 }
524
525 @Override
Adam Cohen9b073942010-08-19 16:49:52 -0700526 protected void dispatchDraw(Canvas canvas) {
Adam Cohen321aa2b2011-03-08 13:33:29 -0800527 boolean expandClipRegion = false;
528
Adam Cohen0ac116b2010-11-11 11:39:53 -0800529 canvas.getClipBounds(stackInvalidateRect);
Adam Cohend51bbb52010-10-18 10:59:49 -0700530 final int childCount = getChildCount();
531 for (int i = 0; i < childCount; i++) {
Adam Cohene86ff4d2011-01-21 17:46:11 -0800532 final View child = getChildAt(i);
533 LayoutParams lp = (LayoutParams) child.getLayoutParams();
534 if ((lp.horizontalOffset == 0 && lp.verticalOffset == 0) ||
535 child.getAlpha() == 0f || child.getVisibility() != VISIBLE) {
536 lp.resetInvalidateRect();
537 }
Adam Cohen321aa2b2011-03-08 13:33:29 -0800538 Rect childInvalidateRect = lp.getInvalidateRect();
539 if (!childInvalidateRect.isEmpty()) {
540 expandClipRegion = true;
541 stackInvalidateRect.union(childInvalidateRect);
542 }
Adam Cohen44729e32010-07-22 16:00:07 -0700543 }
Adam Cohen321aa2b2011-03-08 13:33:29 -0800544
545 // We only expand the clip bounds if necessary.
546 if (expandClipRegion) {
547 canvas.save(Canvas.CLIP_SAVE_FLAG);
548 canvas.clipRect(stackInvalidateRect, Region.Op.UNION);
549 super.dispatchDraw(canvas);
550 canvas.restore();
551 } else {
552 super.dispatchDraw(canvas);
553 }
Adam Cohen9b073942010-08-19 16:49:52 -0700554 }
Adam Cohen44729e32010-07-22 16:00:07 -0700555
Adam Cohen9b073942010-08-19 16:49:52 -0700556 private void onLayout() {
Adam Cohen44729e32010-07-22 16:00:07 -0700557 if (!mFirstLayoutHappened) {
Adam Cohen44729e32010-07-22 16:00:07 -0700558 mFirstLayoutHappened = true;
Adam Cohen53838d22011-01-26 21:32:33 -0800559 updateChildTransforms();
Adam Cohen44729e32010-07-22 16:00:07 -0700560 }
Adam Cohen36f43902010-12-15 21:17:33 -0800561
Adam Cohenc6a47162011-01-27 18:05:02 -0800562 final int newSlideAmount = Math.round(SLIDE_UP_RATIO * getMeasuredHeight());
563 if (mSlideAmount != newSlideAmount) {
564 mSlideAmount = newSlideAmount;
565 mSwipeThreshold = Math.round(SWIPE_THRESHOLD_RATIO * newSlideAmount);
566 }
567
Adam Cohen36f43902010-12-15 21:17:33 -0800568 if (Float.compare(mPerspectiveShiftY, mNewPerspectiveShiftY) != 0 ||
569 Float.compare(mPerspectiveShiftX, mNewPerspectiveShiftX) != 0) {
Adam Cohen78db1aa2011-01-25 12:24:23 -0800570
Adam Cohen36f43902010-12-15 21:17:33 -0800571 mPerspectiveShiftY = mNewPerspectiveShiftY;
572 mPerspectiveShiftX = mNewPerspectiveShiftX;
Adam Cohen53838d22011-01-26 21:32:33 -0800573 updateChildTransforms();
Adam Cohen36f43902010-12-15 21:17:33 -0800574 }
Adam Cohen44729e32010-07-22 16:00:07 -0700575 }
576
Adam Cohena8a7c922011-02-28 17:20:44 -0800577 @Override
578 public boolean onGenericMotionEvent(MotionEvent event) {
579 if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
580 switch (event.getAction()) {
581 case MotionEvent.ACTION_SCROLL: {
582 final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
583 if (vscroll < 0) {
584 pacedScroll(false);
585 return true;
586 } else if (vscroll > 0) {
587 pacedScroll(true);
588 return true;
589 }
590 }
591 }
592 }
593 return super.onGenericMotionEvent(event);
594 }
595
596 // This ensures that the frequency of stack flips caused by scrolls is capped
597 private void pacedScroll(boolean up) {
598 long timeSinceLastScroll = System.currentTimeMillis() - mLastScrollTime;
599 if (timeSinceLastScroll > MIN_TIME_BETWEEN_SCROLLS) {
600 if (up) {
601 showPrevious();
602 } else {
603 showNext();
604 }
605 mLastScrollTime = System.currentTimeMillis();
606 }
607 }
608
Adam Cohen0e2de6d2011-01-19 17:16:34 -0800609 /**
610 * {@inheritDoc}
611 */
Adam Cohen44729e32010-07-22 16:00:07 -0700612 @Override
613 public boolean onInterceptTouchEvent(MotionEvent ev) {
614 int action = ev.getAction();
615 switch(action & MotionEvent.ACTION_MASK) {
Adam Cohen44729e32010-07-22 16:00:07 -0700616 case MotionEvent.ACTION_DOWN: {
617 if (mActivePointerId == INVALID_POINTER) {
618 mInitialX = ev.getX();
619 mInitialY = ev.getY();
620 mActivePointerId = ev.getPointerId(0);
621 }
622 break;
623 }
624 case MotionEvent.ACTION_MOVE: {
625 int pointerIndex = ev.findPointerIndex(mActivePointerId);
626 if (pointerIndex == INVALID_POINTER) {
627 // no data for our primary pointer, this shouldn't happen, log it
628 Log.d(TAG, "Error: No data for our primary pointer.");
629 return false;
630 }
Adam Cohen44729e32010-07-22 16:00:07 -0700631 float newY = ev.getY(pointerIndex);
632 float deltaY = newY - mInitialY;
633
Adam Cohen32a42f12010-08-11 19:34:30 -0700634 beginGestureIfNeeded(deltaY);
Adam Cohen44729e32010-07-22 16:00:07 -0700635 break;
636 }
637 case MotionEvent.ACTION_POINTER_UP: {
638 onSecondaryPointerUp(ev);
639 break;
640 }
641 case MotionEvent.ACTION_UP:
642 case MotionEvent.ACTION_CANCEL: {
643 mActivePointerId = INVALID_POINTER;
644 mSwipeGestureType = GESTURE_NONE;
Adam Cohen44729e32010-07-22 16:00:07 -0700645 }
646 }
647
648 return mSwipeGestureType != GESTURE_NONE;
649 }
650
Adam Cohen32a42f12010-08-11 19:34:30 -0700651 private void beginGestureIfNeeded(float deltaY) {
652 if ((int) Math.abs(deltaY) > mTouchSlop && mSwipeGestureType == GESTURE_NONE) {
Adam Cohenc99ff732011-01-16 16:11:41 -0800653 final int swipeGestureType = deltaY < 0 ? GESTURE_SLIDE_UP : GESTURE_SLIDE_DOWN;
Adam Cohen32a42f12010-08-11 19:34:30 -0700654 cancelLongPress();
655 requestDisallowInterceptTouchEvent(true);
656
Adam Cohend51bbb52010-10-18 10:59:49 -0700657 if (mAdapter == null) return;
Adam Cohenef17dd42011-01-20 17:20:57 -0800658 final int adapterCount = getCount();
Adam Cohend51bbb52010-10-18 10:59:49 -0700659
Adam Cohen839f4a52010-08-26 17:36:48 -0700660 int activeIndex;
661 if (mStackMode == ITEMS_SLIDE_UP) {
Adam Cohen96d8d562010-10-24 11:12:18 -0700662 activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1;
Adam Cohen839f4a52010-08-26 17:36:48 -0700663 } else {
Adam Cohen96d8d562010-10-24 11:12:18 -0700664 activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 1 : 0;
Adam Cohen839f4a52010-08-26 17:36:48 -0700665 }
Adam Cohen32a42f12010-08-11 19:34:30 -0700666
Adam Cohenc99ff732011-01-16 16:11:41 -0800667 boolean endOfStack = mLoopViews && adapterCount == 1 &&
668 ((mStackMode == ITEMS_SLIDE_UP && swipeGestureType == GESTURE_SLIDE_UP) ||
669 (mStackMode == ITEMS_SLIDE_DOWN && swipeGestureType == GESTURE_SLIDE_DOWN));
670 boolean beginningOfStack = mLoopViews && adapterCount == 1 &&
671 ((mStackMode == ITEMS_SLIDE_DOWN && swipeGestureType == GESTURE_SLIDE_UP) ||
672 (mStackMode == ITEMS_SLIDE_UP && swipeGestureType == GESTURE_SLIDE_DOWN));
673
Adam Cohen3352b682010-10-26 13:54:00 -0700674 int stackMode;
Adam Cohenc99ff732011-01-16 16:11:41 -0800675 if (mLoopViews && !beginningOfStack && !endOfStack) {
Adam Cohen3352b682010-10-26 13:54:00 -0700676 stackMode = StackSlider.NORMAL_MODE;
Adam Cohenc99ff732011-01-16 16:11:41 -0800677 } else if (mCurrentWindowStartUnbounded + activeIndex == -1 || beginningOfStack) {
Adam Cohen96d8d562010-10-24 11:12:18 -0700678 activeIndex++;
Adam Cohen3352b682010-10-26 13:54:00 -0700679 stackMode = StackSlider.BEGINNING_OF_STACK_MODE;
Adam Cohenc99ff732011-01-16 16:11:41 -0800680 } else if (mCurrentWindowStartUnbounded + activeIndex == adapterCount - 1 || endOfStack) {
Adam Cohen3352b682010-10-26 13:54:00 -0700681 stackMode = StackSlider.END_OF_STACK_MODE;
Adam Cohen3d07af02010-08-18 17:46:23 -0700682 } else {
Adam Cohen3352b682010-10-26 13:54:00 -0700683 stackMode = StackSlider.NORMAL_MODE;
Adam Cohen32a42f12010-08-11 19:34:30 -0700684 }
Adam Cohen3d07af02010-08-18 17:46:23 -0700685
Adam Cohen3352b682010-10-26 13:54:00 -0700686 mTransitionIsSetup = stackMode == StackSlider.NORMAL_MODE;
687
Adam Cohen3d07af02010-08-18 17:46:23 -0700688 View v = getViewAtRelativeIndex(activeIndex);
689 if (v == null) return;
690
Adam Cohen3352b682010-10-26 13:54:00 -0700691 setupStackSlider(v, stackMode);
Adam Cohen3d07af02010-08-18 17:46:23 -0700692
693 // We only register this gesture if we've made it this far without a problem
694 mSwipeGestureType = swipeGestureType;
Adam Cohena32edd42010-10-26 10:35:01 -0700695 cancelHandleClick();
Adam Cohen32a42f12010-08-11 19:34:30 -0700696 }
697 }
698
Adam Cohen0e2de6d2011-01-19 17:16:34 -0800699 /**
700 * {@inheritDoc}
701 */
Adam Cohen44729e32010-07-22 16:00:07 -0700702 @Override
703 public boolean onTouchEvent(MotionEvent ev) {
Adam Cohena32edd42010-10-26 10:35:01 -0700704 super.onTouchEvent(ev);
705
Adam Cohen44729e32010-07-22 16:00:07 -0700706 int action = ev.getAction();
707 int pointerIndex = ev.findPointerIndex(mActivePointerId);
708 if (pointerIndex == INVALID_POINTER) {
709 // no data for our primary pointer, this shouldn't happen, log it
710 Log.d(TAG, "Error: No data for our primary pointer.");
711 return false;
712 }
713
714 float newY = ev.getY(pointerIndex);
Adam Cohen32a42f12010-08-11 19:34:30 -0700715 float newX = ev.getX(pointerIndex);
Adam Cohen44729e32010-07-22 16:00:07 -0700716 float deltaY = newY - mInitialY;
Adam Cohen32a42f12010-08-11 19:34:30 -0700717 float deltaX = newX - mInitialX;
Adam Cohen44729e32010-07-22 16:00:07 -0700718 if (mVelocityTracker == null) {
719 mVelocityTracker = VelocityTracker.obtain();
720 }
721 mVelocityTracker.addMovement(ev);
722
723 switch (action & MotionEvent.ACTION_MASK) {
724 case MotionEvent.ACTION_MOVE: {
Adam Cohen32a42f12010-08-11 19:34:30 -0700725 beginGestureIfNeeded(deltaY);
726
Adam Cohendfcdddd2010-09-10 14:38:40 -0700727 float rx = deltaX / (mSlideAmount * 1.0f);
Adam Cohen32a42f12010-08-11 19:34:30 -0700728 if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
Adam Cohendfcdddd2010-09-10 14:38:40 -0700729 float r = (deltaY - mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
Adam Cohen839f4a52010-08-26 17:36:48 -0700730 if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
Adam Cohen32a42f12010-08-11 19:34:30 -0700731 mStackSlider.setYProgress(1 - r);
732 mStackSlider.setXProgress(rx);
733 return true;
734 } else if (mSwipeGestureType == GESTURE_SLIDE_UP) {
Adam Cohendfcdddd2010-09-10 14:38:40 -0700735 float r = -(deltaY + mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
Adam Cohen839f4a52010-08-26 17:36:48 -0700736 if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
Adam Cohen32a42f12010-08-11 19:34:30 -0700737 mStackSlider.setYProgress(r);
738 mStackSlider.setXProgress(rx);
739 return true;
Adam Cohen44729e32010-07-22 16:00:07 -0700740 }
Adam Cohen44729e32010-07-22 16:00:07 -0700741 break;
742 }
743 case MotionEvent.ACTION_UP: {
744 handlePointerUp(ev);
745 break;
746 }
747 case MotionEvent.ACTION_POINTER_UP: {
748 onSecondaryPointerUp(ev);
749 break;
750 }
751 case MotionEvent.ACTION_CANCEL: {
752 mActivePointerId = INVALID_POINTER;
Adam Cohen44729e32010-07-22 16:00:07 -0700753 mSwipeGestureType = GESTURE_NONE;
Adam Cohen44729e32010-07-22 16:00:07 -0700754 break;
755 }
756 }
757 return true;
758 }
759
Adam Cohen44729e32010-07-22 16:00:07 -0700760 private void onSecondaryPointerUp(MotionEvent ev) {
761 final int activePointerIndex = ev.getActionIndex();
762 final int pointerId = ev.getPointerId(activePointerIndex);
763 if (pointerId == mActivePointerId) {
764
Adam Cohen96d8d562010-10-24 11:12:18 -0700765 int activeViewIndex = (mSwipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1;
Adam Cohen44729e32010-07-22 16:00:07 -0700766
767 View v = getViewAtRelativeIndex(activeViewIndex);
768 if (v == null) return;
769
770 // Our primary pointer has gone up -- let's see if we can find
771 // another pointer on the view. If so, then we should replace
772 // our primary pointer with this new pointer and adjust things
773 // so that the view doesn't jump
774 for (int index = 0; index < ev.getPointerCount(); index++) {
775 if (index != activePointerIndex) {
776
777 float x = ev.getX(index);
778 float y = ev.getY(index);
779
Patrick Dubroye80202d2010-11-16 15:41:08 -0800780 mTouchRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
781 if (mTouchRect.contains(Math.round(x), Math.round(y))) {
Adam Cohen44729e32010-07-22 16:00:07 -0700782 float oldX = ev.getX(activePointerIndex);
783 float oldY = ev.getY(activePointerIndex);
784
785 // adjust our frame of reference to avoid a jump
786 mInitialY += (y - oldY);
787 mInitialX += (x - oldX);
788
789 mActivePointerId = ev.getPointerId(index);
790 if (mVelocityTracker != null) {
791 mVelocityTracker.clear();
792 }
793 // ok, we're good, we found a new pointer which is touching the active view
794 return;
795 }
796 }
797 }
798 // if we made it this far, it means we didn't find a satisfactory new pointer :(,
Adam Cohen3d07af02010-08-18 17:46:23 -0700799 // so end the gesture
Adam Cohen44729e32010-07-22 16:00:07 -0700800 handlePointerUp(ev);
801 }
802 }
803
804 private void handlePointerUp(MotionEvent ev) {
805 int pointerIndex = ev.findPointerIndex(mActivePointerId);
806 float newY = ev.getY(pointerIndex);
807 int deltaY = (int) (newY - mInitialY);
Adam Cohen26e30bb2010-12-03 18:16:12 -0800808 mLastInteractionTime = System.currentTimeMillis();
Adam Cohen44729e32010-07-22 16:00:07 -0700809
Adam Cohen3d07af02010-08-18 17:46:23 -0700810 if (mVelocityTracker != null) {
811 mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
812 mYVelocity = (int) mVelocityTracker.getYVelocity(mActivePointerId);
813 }
Adam Cohen44729e32010-07-22 16:00:07 -0700814
815 if (mVelocityTracker != null) {
816 mVelocityTracker.recycle();
817 mVelocityTracker = null;
818 }
819
Adam Cohen3d07af02010-08-18 17:46:23 -0700820 if (deltaY > mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_DOWN
821 && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800822 // We reset the gesture variable, because otherwise we will ignore showPrevious() /
823 // showNext();
824 mSwipeGestureType = GESTURE_NONE;
825
Adam Cohen44729e32010-07-22 16:00:07 -0700826 // Swipe threshold exceeded, swipe down
Adam Cohen839f4a52010-08-26 17:36:48 -0700827 if (mStackMode == ITEMS_SLIDE_UP) {
Adam Cohen839f4a52010-08-26 17:36:48 -0700828 showPrevious();
Adam Cohen96d8d562010-10-24 11:12:18 -0700829 } else {
830 showNext();
Adam Cohen839f4a52010-08-26 17:36:48 -0700831 }
Adam Cohen32a42f12010-08-11 19:34:30 -0700832 mHighlight.bringToFront();
Adam Cohen3d07af02010-08-18 17:46:23 -0700833 } else if (deltaY < -mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_UP
834 && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800835 // We reset the gesture variable, because otherwise we will ignore showPrevious() /
836 // showNext();
837 mSwipeGestureType = GESTURE_NONE;
838
Adam Cohen44729e32010-07-22 16:00:07 -0700839 // Swipe threshold exceeded, swipe up
Adam Cohen839f4a52010-08-26 17:36:48 -0700840 if (mStackMode == ITEMS_SLIDE_UP) {
Adam Cohen839f4a52010-08-26 17:36:48 -0700841 showNext();
Adam Cohen96d8d562010-10-24 11:12:18 -0700842 } else {
843 showPrevious();
Adam Cohen839f4a52010-08-26 17:36:48 -0700844 }
845
Adam Cohen32a42f12010-08-11 19:34:30 -0700846 mHighlight.bringToFront();
Adam Cohen839f4a52010-08-26 17:36:48 -0700847 } else if (mSwipeGestureType == GESTURE_SLIDE_UP ) {
Adam Cohen44729e32010-07-22 16:00:07 -0700848 // Didn't swipe up far enough, snap back down
Adam Cohen839f4a52010-08-26 17:36:48 -0700849 int duration;
850 float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 1 : 0;
851 if (mStackMode == ITEMS_SLIDE_UP || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
852 duration = Math.round(mStackSlider.getDurationForNeutralPosition());
853 } else {
854 duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
855 }
Adam Cohen44729e32010-07-22 16:00:07 -0700856
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700857 StackSlider animationSlider = new StackSlider(mStackSlider);
Chet Haase2794eb32010-10-12 16:29:28 -0700858 PropertyValuesHolder snapBackY = PropertyValuesHolder.ofFloat("YProgress", finalYProgress);
859 PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
860 ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
Adam Cohen839f4a52010-08-26 17:36:48 -0700861 snapBackX, snapBackY);
Chet Haase2794eb32010-10-12 16:29:28 -0700862 pa.setDuration(duration);
Adam Cohen839f4a52010-08-26 17:36:48 -0700863 pa.setInterpolator(new LinearInterpolator());
864 pa.start();
Adam Cohen32a42f12010-08-11 19:34:30 -0700865 } else if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
Adam Cohen44729e32010-07-22 16:00:07 -0700866 // Didn't swipe down far enough, snap back up
Adam Cohen839f4a52010-08-26 17:36:48 -0700867 float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 0 : 1;
868 int duration;
869 if (mStackMode == ITEMS_SLIDE_DOWN || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
870 duration = Math.round(mStackSlider.getDurationForNeutralPosition());
871 } else {
872 duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
873 }
Adam Cohen3d07af02010-08-18 17:46:23 -0700874
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700875 StackSlider animationSlider = new StackSlider(mStackSlider);
Chet Haase2794eb32010-10-12 16:29:28 -0700876 PropertyValuesHolder snapBackY =
877 PropertyValuesHolder.ofFloat("YProgress",finalYProgress);
878 PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
879 ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
Adam Cohen839f4a52010-08-26 17:36:48 -0700880 snapBackX, snapBackY);
Chet Haase2794eb32010-10-12 16:29:28 -0700881 pa.setDuration(duration);
Adam Cohen839f4a52010-08-26 17:36:48 -0700882 pa.start();
Adam Cohen44729e32010-07-22 16:00:07 -0700883 }
884
885 mActivePointerId = INVALID_POINTER;
Adam Cohen44729e32010-07-22 16:00:07 -0700886 mSwipeGestureType = GESTURE_NONE;
Adam Cohen32a42f12010-08-11 19:34:30 -0700887 }
888
889 private class StackSlider {
890 View mView;
891 float mYProgress;
892 float mXProgress;
893
Adam Cohen3d07af02010-08-18 17:46:23 -0700894 static final int NORMAL_MODE = 0;
895 static final int BEGINNING_OF_STACK_MODE = 1;
896 static final int END_OF_STACK_MODE = 2;
897
898 int mMode = NORMAL_MODE;
899
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700900 public StackSlider() {
901 }
902
903 public StackSlider(StackSlider copy) {
904 mView = copy.mView;
905 mYProgress = copy.mYProgress;
906 mXProgress = copy.mXProgress;
Adam Cohen3d07af02010-08-18 17:46:23 -0700907 mMode = copy.mMode;
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700908 }
909
Adam Cohen32a42f12010-08-11 19:34:30 -0700910 private float cubic(float r) {
Adam Cohen839f4a52010-08-26 17:36:48 -0700911 return (float) (Math.pow(2 * r - 1, 3) + 1) / 2.0f;
Adam Cohen32a42f12010-08-11 19:34:30 -0700912 }
913
914 private float highlightAlphaInterpolator(float r) {
915 float pivot = 0.4f;
916 if (r < pivot) {
Adam Cohen839f4a52010-08-26 17:36:48 -0700917 return 0.85f * cubic(r / pivot);
Adam Cohen32a42f12010-08-11 19:34:30 -0700918 } else {
Adam Cohen839f4a52010-08-26 17:36:48 -0700919 return 0.85f * cubic(1 - (r - pivot) / (1 - pivot));
Adam Cohen32a42f12010-08-11 19:34:30 -0700920 }
921 }
922
923 private float viewAlphaInterpolator(float r) {
924 float pivot = 0.3f;
925 if (r > pivot) {
Adam Cohen839f4a52010-08-26 17:36:48 -0700926 return (r - pivot) / (1 - pivot);
Adam Cohen32a42f12010-08-11 19:34:30 -0700927 } else {
928 return 0;
929 }
930 }
931
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700932 private float rotationInterpolator(float r) {
933 float pivot = 0.2f;
934 if (r < pivot) {
935 return 0;
936 } else {
Adam Cohen839f4a52010-08-26 17:36:48 -0700937 return (r - pivot) / (1 - pivot);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700938 }
939 }
940
Adam Cohen32a42f12010-08-11 19:34:30 -0700941 void setView(View v) {
942 mView = v;
943 }
944
945 public void setYProgress(float r) {
946 // enforce r between 0 and 1
947 r = Math.min(1.0f, r);
948 r = Math.max(0, r);
949
950 mYProgress = r;
Adam Cohena02fdf12010-11-03 13:27:40 -0700951 if (mView == null) return;
952
Adam Cohen32a42f12010-08-11 19:34:30 -0700953 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
954 final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
955
Adam Cohen839f4a52010-08-26 17:36:48 -0700956 int stackDirection = (mStackMode == ITEMS_SLIDE_UP) ? 1 : -1;
957
Adam Cohen48867212011-01-09 13:49:40 -0800958 // We need to prevent any clipping issues which may arise by setting a layer type.
959 // This doesn't come for free however, so we only want to enable it when required.
960 if (Float.compare(0f, mYProgress) != 0 && Float.compare(1.0f, mYProgress) != 0) {
961 if (mView.getLayerType() == LAYER_TYPE_NONE) {
962 mView.setLayerType(LAYER_TYPE_HARDWARE, null);
963 }
964 } else {
965 if (mView.getLayerType() != LAYER_TYPE_NONE) {
966 mView.setLayerType(LAYER_TYPE_NONE, null);
967 }
968 }
969
Adam Cohen3d07af02010-08-18 17:46:23 -0700970 switch (mMode) {
971 case NORMAL_MODE:
Adam Cohendfcdddd2010-09-10 14:38:40 -0700972 viewLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
973 highlightLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
Adam Cohen3d07af02010-08-18 17:46:23 -0700974 mHighlight.setAlpha(highlightAlphaInterpolator(r));
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700975
Adam Cohen839f4a52010-08-26 17:36:48 -0700976 float alpha = viewAlphaInterpolator(1 - r);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700977
Adam Cohen3d07af02010-08-18 17:46:23 -0700978 // We make sure that views which can't be seen (have 0 alpha) are also invisible
979 // so that they don't interfere with click events.
980 if (mView.getAlpha() == 0 && alpha != 0 && mView.getVisibility() != VISIBLE) {
981 mView.setVisibility(VISIBLE);
982 } else if (alpha == 0 && mView.getAlpha() != 0
983 && mView.getVisibility() == VISIBLE) {
984 mView.setVisibility(INVISIBLE);
985 }
986
987 mView.setAlpha(alpha);
Adam Cohen839f4a52010-08-26 17:36:48 -0700988 mView.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
989 mHighlight.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
Adam Cohen3d07af02010-08-18 17:46:23 -0700990 break;
Adam Cohen96d8d562010-10-24 11:12:18 -0700991 case END_OF_STACK_MODE:
Adam Cohen839f4a52010-08-26 17:36:48 -0700992 r = r * 0.2f;
Adam Cohendfcdddd2010-09-10 14:38:40 -0700993 viewLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount));
994 highlightLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount));
Adam Cohen3d07af02010-08-18 17:46:23 -0700995 mHighlight.setAlpha(highlightAlphaInterpolator(r));
996 break;
Adam Cohen96d8d562010-10-24 11:12:18 -0700997 case BEGINNING_OF_STACK_MODE:
Adam Cohen839f4a52010-08-26 17:36:48 -0700998 r = (1-r) * 0.2f;
Adam Cohendfcdddd2010-09-10 14:38:40 -0700999 viewLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount));
1000 highlightLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount));
Adam Cohen3d07af02010-08-18 17:46:23 -07001001 mHighlight.setAlpha(highlightAlphaInterpolator(r));
1002 break;
Adam Cohenb04f7ad2010-08-15 13:22:42 -07001003 }
Adam Cohen32a42f12010-08-11 19:34:30 -07001004 }
1005
1006 public void setXProgress(float r) {
1007 // enforce r between 0 and 1
Adam Cohen3d07af02010-08-18 17:46:23 -07001008 r = Math.min(2.0f, r);
1009 r = Math.max(-2.0f, r);
Adam Cohen32a42f12010-08-11 19:34:30 -07001010
1011 mXProgress = r;
1012
Adam Cohena02fdf12010-11-03 13:27:40 -07001013 if (mView == null) return;
Adam Cohen32a42f12010-08-11 19:34:30 -07001014 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
1015 final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
1016
Adam Cohen3d07af02010-08-18 17:46:23 -07001017 r *= 0.2f;
Adam Cohendfcdddd2010-09-10 14:38:40 -07001018 viewLp.setHorizontalOffset(Math.round(r * mSlideAmount));
1019 highlightLp.setHorizontalOffset(Math.round(r * mSlideAmount));
Adam Cohen32a42f12010-08-11 19:34:30 -07001020 }
1021
Adam Cohen3d07af02010-08-18 17:46:23 -07001022 void setMode(int mode) {
1023 mMode = mode;
1024 }
1025
1026 float getDurationForNeutralPosition() {
Adam Cohen839f4a52010-08-26 17:36:48 -07001027 return getDuration(false, 0);
Adam Cohen3d07af02010-08-18 17:46:23 -07001028 }
1029
1030 float getDurationForOffscreenPosition() {
Adam Cohen839f4a52010-08-26 17:36:48 -07001031 return getDuration(true, 0);
Adam Cohen3d07af02010-08-18 17:46:23 -07001032 }
1033
Adam Cohen839f4a52010-08-26 17:36:48 -07001034 float getDurationForNeutralPosition(float velocity) {
1035 return getDuration(false, velocity);
1036 }
1037
1038 float getDurationForOffscreenPosition(float velocity) {
1039 return getDuration(true, velocity);
1040 }
1041
1042 private float getDuration(boolean invert, float velocity) {
Adam Cohen3d07af02010-08-18 17:46:23 -07001043 if (mView != null) {
1044 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
1045
Adam Cohen839f4a52010-08-26 17:36:48 -07001046 float d = (float) Math.sqrt(Math.pow(viewLp.horizontalOffset, 2) +
1047 Math.pow(viewLp.verticalOffset, 2));
Adam Cohendfcdddd2010-09-10 14:38:40 -07001048 float maxd = (float) Math.sqrt(Math.pow(mSlideAmount, 2) +
1049 Math.pow(0.4f * mSlideAmount, 2));
Adam Cohen839f4a52010-08-26 17:36:48 -07001050
1051 if (velocity == 0) {
1052 return (invert ? (1 - d / maxd) : d / maxd) * DEFAULT_ANIMATION_DURATION;
1053 } else {
1054 float duration = invert ? d / Math.abs(velocity) :
1055 (maxd - d) / Math.abs(velocity);
1056 if (duration < MINIMUM_ANIMATION_DURATION ||
1057 duration > DEFAULT_ANIMATION_DURATION) {
1058 return getDuration(invert, 0);
1059 } else {
1060 return duration;
1061 }
1062 }
Adam Cohen3d07af02010-08-18 17:46:23 -07001063 }
1064 return 0;
1065 }
1066
Romain Guye5ebcb02010-10-15 13:57:28 -07001067 // Used for animations
1068 @SuppressWarnings({"UnusedDeclaration"})
Adam Cohen839f4a52010-08-26 17:36:48 -07001069 public float getYProgress() {
Adam Cohen32a42f12010-08-11 19:34:30 -07001070 return mYProgress;
1071 }
1072
Romain Guye5ebcb02010-10-15 13:57:28 -07001073 // Used for animations
1074 @SuppressWarnings({"UnusedDeclaration"})
Adam Cohen839f4a52010-08-26 17:36:48 -07001075 public float getXProgress() {
Adam Cohen32a42f12010-08-11 19:34:30 -07001076 return mXProgress;
1077 }
Adam Cohen44729e32010-07-22 16:00:07 -07001078 }
1079
Adam Cohen9b073942010-08-19 16:49:52 -07001080 LayoutParams createOrReuseLayoutParams(View v) {
1081 final ViewGroup.LayoutParams currentLp = v.getLayoutParams();
1082 if (currentLp instanceof LayoutParams) {
1083 LayoutParams lp = (LayoutParams) currentLp;
1084 lp.setHorizontalOffset(0);
1085 lp.setVerticalOffset(0);
Adam Cohen839f4a52010-08-26 17:36:48 -07001086 lp.width = 0;
1087 lp.width = 0;
Adam Cohen9b073942010-08-19 16:49:52 -07001088 return lp;
1089 }
1090 return new LayoutParams(v);
Adam Cohen32a42f12010-08-11 19:34:30 -07001091 }
1092
Adam Cohen9b073942010-08-19 16:49:52 -07001093 @Override
1094 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Adam Cohenef17dd42011-01-20 17:20:57 -08001095 checkForAndHandleDataChanged();
Adam Cohenb04f7ad2010-08-15 13:22:42 -07001096
Adam Cohen9b073942010-08-19 16:49:52 -07001097 final int childCount = getChildCount();
1098 for (int i = 0; i < childCount; i++) {
1099 final View child = getChildAt(i);
Adam Cohen32a42f12010-08-11 19:34:30 -07001100
Adam Cohen9b073942010-08-19 16:49:52 -07001101 int childRight = mPaddingLeft + child.getMeasuredWidth();
1102 int childBottom = mPaddingTop + child.getMeasuredHeight();
1103 LayoutParams lp = (LayoutParams) child.getLayoutParams();
Adam Cohen32a42f12010-08-11 19:34:30 -07001104
Adam Cohen9b073942010-08-19 16:49:52 -07001105 child.layout(mPaddingLeft + lp.horizontalOffset, mPaddingTop + lp.verticalOffset,
1106 childRight + lp.horizontalOffset, childBottom + lp.verticalOffset);
1107
Adam Cohen9b073942010-08-19 16:49:52 -07001108 }
Adam Cohen9b073942010-08-19 16:49:52 -07001109 onLayout();
Adam Cohen32a42f12010-08-11 19:34:30 -07001110 }
1111
Adam Cohen26e30bb2010-12-03 18:16:12 -08001112 @Override
1113 public void advance() {
1114 long timeSinceLastInteraction = System.currentTimeMillis() - mLastInteractionTime;
Adam Cohenc99ff732011-01-16 16:11:41 -08001115
1116 if (mAdapter == null) return;
Adam Cohenef17dd42011-01-20 17:20:57 -08001117 final int adapterCount = getCount();
Adam Cohenc99ff732011-01-16 16:11:41 -08001118 if (adapterCount == 1 && mLoopViews) return;
1119
Adam Cohen26e30bb2010-12-03 18:16:12 -08001120 if (mSwipeGestureType == GESTURE_NONE &&
1121 timeSinceLastInteraction > MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE) {
1122 showNext();
1123 }
1124 }
1125
Adam Cohen839f4a52010-08-26 17:36:48 -07001126 private void measureChildren() {
1127 final int count = getChildCount();
Adam Cohen36f43902010-12-15 21:17:33 -08001128
1129 final int measuredWidth = getMeasuredWidth();
1130 final int measuredHeight = getMeasuredHeight();
1131
1132 final int childWidth = Math.round(measuredWidth*(1-PERSPECTIVE_SHIFT_FACTOR_X))
Adam Cohen026e1212010-12-13 12:29:17 -08001133 - mPaddingLeft - mPaddingRight;
Adam Cohen36f43902010-12-15 21:17:33 -08001134 final int childHeight = Math.round(measuredHeight*(1-PERSPECTIVE_SHIFT_FACTOR_Y))
Adam Cohen839f4a52010-08-26 17:36:48 -07001135 - mPaddingTop - mPaddingBottom;
1136
Adam Cohen36f43902010-12-15 21:17:33 -08001137 int maxWidth = 0;
1138 int maxHeight = 0;
1139
Adam Cohen839f4a52010-08-26 17:36:48 -07001140 for (int i = 0; i < count; i++) {
1141 final View child = getChildAt(i);
Adam Cohen36f43902010-12-15 21:17:33 -08001142 child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST),
1143 MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST));
1144
1145 if (child != mHighlight && child != mClickFeedback) {
1146 final int childMeasuredWidth = child.getMeasuredWidth();
1147 final int childMeasuredHeight = child.getMeasuredHeight();
1148 if (childMeasuredWidth > maxWidth) {
1149 maxWidth = childMeasuredWidth;
1150 }
1151 if (childMeasuredHeight > maxHeight) {
1152 maxHeight = childMeasuredHeight;
1153 }
1154 }
1155 }
1156
1157 mNewPerspectiveShiftX = PERSPECTIVE_SHIFT_FACTOR_X * measuredWidth;
1158 mNewPerspectiveShiftY = PERSPECTIVE_SHIFT_FACTOR_Y * measuredHeight;
Adam Cohen53838d22011-01-26 21:32:33 -08001159
1160 // If we have extra space, we try and spread the items out
Adam Cohen78db1aa2011-01-25 12:24:23 -08001161 if (maxWidth > 0 && count > 0 && maxWidth < childWidth) {
Adam Cohen36f43902010-12-15 21:17:33 -08001162 mNewPerspectiveShiftX = measuredWidth - maxWidth;
1163 }
1164
Adam Cohen78db1aa2011-01-25 12:24:23 -08001165 if (maxHeight > 0 && count > 0 && maxHeight < childHeight) {
Adam Cohen36f43902010-12-15 21:17:33 -08001166 mNewPerspectiveShiftY = measuredHeight - maxHeight;
Adam Cohen839f4a52010-08-26 17:36:48 -07001167 }
1168 }
1169
1170 @Override
1171 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1172 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
1173 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
1174 final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
1175 final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
1176
1177 boolean haveChildRefSize = (mReferenceChildWidth != -1 && mReferenceChildHeight != -1);
1178
1179 // We need to deal with the case where our parent hasn't told us how
1180 // big we should be. In this case we should
Adam Cohen026e1212010-12-13 12:29:17 -08001181 float factorY = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_Y);
Adam Cohen839f4a52010-08-26 17:36:48 -07001182 if (heightSpecMode == MeasureSpec.UNSPECIFIED) {
1183 heightSpecSize = haveChildRefSize ?
Adam Cohen026e1212010-12-13 12:29:17 -08001184 Math.round(mReferenceChildHeight * (1 + factorY)) +
Adam Cohen839f4a52010-08-26 17:36:48 -07001185 mPaddingTop + mPaddingBottom : 0;
1186 } else if (heightSpecMode == MeasureSpec.AT_MOST) {
Dianne Hackborn189ee182010-12-02 21:48:53 -08001187 if (haveChildRefSize) {
Adam Cohen026e1212010-12-13 12:29:17 -08001188 int height = Math.round(mReferenceChildHeight * (1 + factorY))
Dianne Hackborn189ee182010-12-02 21:48:53 -08001189 + mPaddingTop + mPaddingBottom;
1190 if (height <= heightSpecSize) {
1191 heightSpecSize = height;
1192 } else {
1193 heightSpecSize |= MEASURED_STATE_TOO_SMALL;
Adam Cohenef17dd42011-01-20 17:20:57 -08001194
Dianne Hackborn189ee182010-12-02 21:48:53 -08001195 }
1196 } else {
1197 heightSpecSize = 0;
1198 }
Adam Cohen839f4a52010-08-26 17:36:48 -07001199 }
1200
Adam Cohen026e1212010-12-13 12:29:17 -08001201 float factorX = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_X);
Adam Cohen839f4a52010-08-26 17:36:48 -07001202 if (widthSpecMode == MeasureSpec.UNSPECIFIED) {
Adam Cohen026e1212010-12-13 12:29:17 -08001203 widthSpecSize = haveChildRefSize ?
1204 Math.round(mReferenceChildWidth * (1 + factorX)) +
1205 mPaddingLeft + mPaddingRight : 0;
Adam Cohen839f4a52010-08-26 17:36:48 -07001206 } else if (heightSpecMode == MeasureSpec.AT_MOST) {
Dianne Hackborn189ee182010-12-02 21:48:53 -08001207 if (haveChildRefSize) {
1208 int width = mReferenceChildWidth + mPaddingLeft + mPaddingRight;
1209 if (width <= widthSpecSize) {
1210 widthSpecSize = width;
1211 } else {
1212 widthSpecSize |= MEASURED_STATE_TOO_SMALL;
1213 }
1214 } else {
1215 widthSpecSize = 0;
1216 }
Adam Cohen839f4a52010-08-26 17:36:48 -07001217 }
Adam Cohen839f4a52010-08-26 17:36:48 -07001218 setMeasuredDimension(widthSpecSize, heightSpecSize);
1219 measureChildren();
1220 }
1221
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001222 @Override
1223 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
1224 super.onInitializeAccessibilityEvent(event);
1225 event.setClassName(StackView.class.getName());
1226 }
1227
1228 @Override
1229 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
1230 super.onInitializeAccessibilityNodeInfo(info);
1231 info.setClassName(StackView.class.getName());
Svetoslav Ganov48d15862012-05-15 10:10:00 -07001232 info.setScrollable(getChildCount() > 1);
Svetoslav Ganovfb1e80a2012-05-16 17:33:19 -07001233 if (isEnabled()) {
1234 if (getDisplayedChild() < getChildCount() - 1) {
1235 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
1236 }
1237 if (getDisplayedChild() > 0) {
1238 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
1239 }
Svetoslav Ganov48d15862012-05-15 10:10:00 -07001240 }
1241 }
1242
1243 @Override
1244 public boolean performAccessibilityAction(int action, Bundle arguments) {
1245 if (super.performAccessibilityAction(action, arguments)) {
1246 return true;
1247 }
Svetoslav Ganovfb1e80a2012-05-16 17:33:19 -07001248 if (!isEnabled()) {
1249 return false;
1250 }
Svetoslav Ganov48d15862012-05-15 10:10:00 -07001251 switch (action) {
1252 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
1253 if (getDisplayedChild() < getChildCount() - 1) {
1254 showNext();
1255 return true;
1256 }
1257 } return false;
1258 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
1259 if (getDisplayedChild() > 0) {
1260 showPrevious();
1261 return true;
1262 }
1263 } return false;
1264 }
1265 return false;
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001266 }
1267
Adam Cohen9b073942010-08-19 16:49:52 -07001268 class LayoutParams extends ViewGroup.LayoutParams {
1269 int horizontalOffset;
1270 int verticalOffset;
1271 View mView;
Adam Cohend51bbb52010-10-18 10:59:49 -07001272 private final Rect parentRect = new Rect();
1273 private final Rect invalidateRect = new Rect();
1274 private final RectF invalidateRectf = new RectF();
1275 private final Rect globalInvalidateRect = new Rect();
Adam Cohen32a42f12010-08-11 19:34:30 -07001276
Adam Cohen9b073942010-08-19 16:49:52 -07001277 LayoutParams(View view) {
1278 super(0, 0);
Adam Cohen839f4a52010-08-26 17:36:48 -07001279 width = 0;
1280 height = 0;
Adam Cohen9b073942010-08-19 16:49:52 -07001281 horizontalOffset = 0;
1282 verticalOffset = 0;
1283 mView = view;
1284 }
Adam Cohen32a42f12010-08-11 19:34:30 -07001285
Adam Cohen9b073942010-08-19 16:49:52 -07001286 LayoutParams(Context c, AttributeSet attrs) {
1287 super(c, attrs);
1288 horizontalOffset = 0;
1289 verticalOffset = 0;
Adam Cohen839f4a52010-08-26 17:36:48 -07001290 width = 0;
1291 height = 0;
Adam Cohen9b073942010-08-19 16:49:52 -07001292 }
Adam Cohen32a42f12010-08-11 19:34:30 -07001293
Adam Cohen9b073942010-08-19 16:49:52 -07001294 void invalidateGlobalRegion(View v, Rect r) {
Adam Cohend51bbb52010-10-18 10:59:49 -07001295 // We need to make a new rect here, so as not to modify the one passed
1296 globalInvalidateRect.set(r);
Adam Cohen1ff65d12011-02-16 14:04:09 -08001297 globalInvalidateRect.union(0, 0, getWidth(), getHeight());
Adam Cohen9b073942010-08-19 16:49:52 -07001298 View p = v;
1299 if (!(v.getParent() != null && v.getParent() instanceof View)) return;
1300
Adam Cohen9b073942010-08-19 16:49:52 -07001301 boolean firstPass = true;
1302 parentRect.set(0, 0, 0, 0);
Adam Cohenb7f4d032010-09-16 15:25:54 -07001303 while (p.getParent() != null && p.getParent() instanceof View
Adam Cohend51bbb52010-10-18 10:59:49 -07001304 && !parentRect.contains(globalInvalidateRect)) {
Adam Cohen9b073942010-08-19 16:49:52 -07001305 if (!firstPass) {
Adam Cohend51bbb52010-10-18 10:59:49 -07001306 globalInvalidateRect.offset(p.getLeft() - p.getScrollX(), p.getTop()
1307 - p.getScrollY());
Adam Cohen9b073942010-08-19 16:49:52 -07001308 }
1309 firstPass = false;
1310 p = (View) p.getParent();
Adam Cohenb7f4d032010-09-16 15:25:54 -07001311 parentRect.set(p.getScrollX(), p.getScrollY(),
Adam Cohen1ff65d12011-02-16 14:04:09 -08001312 p.getWidth() + p.getScrollX(), p.getHeight() + p.getScrollY());
1313 p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top,
1314 globalInvalidateRect.right, globalInvalidateRect.bottom);
Adam Cohen9b073942010-08-19 16:49:52 -07001315 }
1316
Adam Cohend51bbb52010-10-18 10:59:49 -07001317 p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top,
1318 globalInvalidateRect.right, globalInvalidateRect.bottom);
Adam Cohen9b073942010-08-19 16:49:52 -07001319 }
1320
Adam Cohend51bbb52010-10-18 10:59:49 -07001321 Rect getInvalidateRect() {
1322 return invalidateRect;
1323 }
1324
1325 void resetInvalidateRect() {
Adam Cohen0ac116b2010-11-11 11:39:53 -08001326 invalidateRect.set(0, 0, 0, 0);
Adam Cohend51bbb52010-10-18 10:59:49 -07001327 }
1328
Chet Haasea18a86b2010-09-07 13:20:00 -07001329 // This is public so that ObjectAnimator can access it
Adam Cohen9b073942010-08-19 16:49:52 -07001330 public void setVerticalOffset(int newVerticalOffset) {
Adam Cohen0ac116b2010-11-11 11:39:53 -08001331 setOffsets(horizontalOffset, newVerticalOffset);
1332 }
1333
1334 public void setHorizontalOffset(int newHorizontalOffset) {
1335 setOffsets(newHorizontalOffset, verticalOffset);
1336 }
1337
1338 public void setOffsets(int newHorizontalOffset, int newVerticalOffset) {
1339 int horizontalOffsetDelta = newHorizontalOffset - horizontalOffset;
1340 horizontalOffset = newHorizontalOffset;
1341 int verticalOffsetDelta = newVerticalOffset - verticalOffset;
Adam Cohen9b073942010-08-19 16:49:52 -07001342 verticalOffset = newVerticalOffset;
1343
1344 if (mView != null) {
1345 mView.requestLayout();
Adam Cohen0ac116b2010-11-11 11:39:53 -08001346 int left = Math.min(mView.getLeft() + horizontalOffsetDelta, mView.getLeft());
1347 int right = Math.max(mView.getRight() + horizontalOffsetDelta, mView.getRight());
1348 int top = Math.min(mView.getTop() + verticalOffsetDelta, mView.getTop());
1349 int bottom = Math.max(mView.getBottom() + verticalOffsetDelta, mView.getBottom());
Adam Cohen9b073942010-08-19 16:49:52 -07001350
Adam Cohen0ac116b2010-11-11 11:39:53 -08001351 invalidateRectf.set(left, top, right, bottom);
Adam Cohen9b073942010-08-19 16:49:52 -07001352
1353 float xoffset = -invalidateRectf.left;
1354 float yoffset = -invalidateRectf.top;
1355 invalidateRectf.offset(xoffset, yoffset);
1356 mView.getMatrix().mapRect(invalidateRectf);
1357 invalidateRectf.offset(-xoffset, -yoffset);
1358
Adam Cohen0ac116b2010-11-11 11:39:53 -08001359 invalidateRect.set((int) Math.floor(invalidateRectf.left),
Adam Cohen9b073942010-08-19 16:49:52 -07001360 (int) Math.floor(invalidateRectf.top),
1361 (int) Math.ceil(invalidateRectf.right),
1362 (int) Math.ceil(invalidateRectf.bottom));
1363
1364 invalidateGlobalRegion(mView, invalidateRect);
1365 }
1366 }
1367 }
1368
1369 private static class HolographicHelper {
1370 private final Paint mHolographicPaint = new Paint();
1371 private final Paint mErasePaint = new Paint();
Adam Cohen839f4a52010-08-26 17:36:48 -07001372 private final Paint mBlurPaint = new Paint();
Adam Cohen8baf5df2010-11-11 15:23:41 -08001373 private static final int RES_OUT = 0;
1374 private static final int CLICK_FEEDBACK = 1;
1375 private float mDensity;
Patrick Dubroye80202d2010-11-16 15:41:08 -08001376 private BlurMaskFilter mSmallBlurMaskFilter;
1377 private BlurMaskFilter mLargeBlurMaskFilter;
1378 private final Canvas mCanvas = new Canvas();
1379 private final Canvas mMaskCanvas = new Canvas();
1380 private final int[] mTmpXY = new int[2];
1381 private final Matrix mIdentityMatrix = new Matrix();
Adam Cohen9b073942010-08-19 16:49:52 -07001382
Adam Cohendfcdddd2010-09-10 14:38:40 -07001383 HolographicHelper(Context context) {
Adam Cohen8baf5df2010-11-11 15:23:41 -08001384 mDensity = context.getResources().getDisplayMetrics().density;
Adam Cohendfcdddd2010-09-10 14:38:40 -07001385
Adam Cohen9b073942010-08-19 16:49:52 -07001386 mHolographicPaint.setFilterBitmap(true);
Adam Cohen839f4a52010-08-26 17:36:48 -07001387 mHolographicPaint.setMaskFilter(TableMaskFilter.CreateClipTable(0, 30));
Adam Cohen9b073942010-08-19 16:49:52 -07001388 mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
1389 mErasePaint.setFilterBitmap(true);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001390
1391 mSmallBlurMaskFilter = new BlurMaskFilter(2 * mDensity, BlurMaskFilter.Blur.NORMAL);
1392 mLargeBlurMaskFilter = new BlurMaskFilter(4 * mDensity, BlurMaskFilter.Blur.NORMAL);
Adam Cohen9b073942010-08-19 16:49:52 -07001393 }
1394
Adam Cohen26f072c2011-04-01 16:23:18 -07001395 Bitmap createClickOutline(View v, int color) {
1396 return createOutline(v, CLICK_FEEDBACK, color);
Adam Cohen8baf5df2010-11-11 15:23:41 -08001397 }
1398
Adam Cohen26f072c2011-04-01 16:23:18 -07001399 Bitmap createResOutline(View v, int color) {
1400 return createOutline(v, RES_OUT, color);
1401 }
1402
1403 Bitmap createOutline(View v, int type, int color) {
1404 mHolographicPaint.setColor(color);
Adam Cohen8baf5df2010-11-11 15:23:41 -08001405 if (type == RES_OUT) {
Patrick Dubroye80202d2010-11-16 15:41:08 -08001406 mBlurPaint.setMaskFilter(mSmallBlurMaskFilter);
Adam Cohen8baf5df2010-11-11 15:23:41 -08001407 } else if (type == CLICK_FEEDBACK) {
Patrick Dubroye80202d2010-11-16 15:41:08 -08001408 mBlurPaint.setMaskFilter(mLargeBlurMaskFilter);
Adam Cohen8baf5df2010-11-11 15:23:41 -08001409 }
1410
Adam Cohen9b073942010-08-19 16:49:52 -07001411 if (v.getMeasuredWidth() == 0 || v.getMeasuredHeight() == 0) {
1412 return null;
1413 }
1414
Dianne Hackborndde331c2012-08-03 14:01:57 -07001415 Bitmap bitmap = Bitmap.createBitmap(v.getResources().getDisplayMetrics(),
1416 v.getMeasuredWidth(), v.getMeasuredHeight(), Bitmap.Config.ARGB_8888);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001417 mCanvas.setBitmap(bitmap);
Adam Cohen9b073942010-08-19 16:49:52 -07001418
1419 float rotationX = v.getRotationX();
Adam Cohen839f4a52010-08-26 17:36:48 -07001420 float rotation = v.getRotation();
1421 float translationY = v.getTranslationY();
Adam Cohen026e1212010-12-13 12:29:17 -08001422 float translationX = v.getTranslationX();
Adam Cohen9b073942010-08-19 16:49:52 -07001423 v.setRotationX(0);
Adam Cohen839f4a52010-08-26 17:36:48 -07001424 v.setRotation(0);
1425 v.setTranslationY(0);
Adam Cohen026e1212010-12-13 12:29:17 -08001426 v.setTranslationX(0);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001427 v.draw(mCanvas);
Adam Cohen9b073942010-08-19 16:49:52 -07001428 v.setRotationX(rotationX);
Adam Cohen839f4a52010-08-26 17:36:48 -07001429 v.setRotation(rotation);
1430 v.setTranslationY(translationY);
Adam Cohen026e1212010-12-13 12:29:17 -08001431 v.setTranslationX(translationX);
Adam Cohen9b073942010-08-19 16:49:52 -07001432
Patrick Dubroye80202d2010-11-16 15:41:08 -08001433 drawOutline(mCanvas, bitmap);
Dianne Hackborn6311d0a2011-08-02 16:37:58 -07001434 mCanvas.setBitmap(null);
Adam Cohen9b073942010-08-19 16:49:52 -07001435 return bitmap;
1436 }
1437
Adam Cohen9b073942010-08-19 16:49:52 -07001438 void drawOutline(Canvas dest, Bitmap src) {
Patrick Dubroye80202d2010-11-16 15:41:08 -08001439 final int[] xy = mTmpXY;
Adam Cohen839f4a52010-08-26 17:36:48 -07001440 Bitmap mask = src.extractAlpha(mBlurPaint, xy);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001441 mMaskCanvas.setBitmap(mask);
1442 mMaskCanvas.drawBitmap(src, -xy[0], -xy[1], mErasePaint);
Adam Cohen9b073942010-08-19 16:49:52 -07001443 dest.drawColor(0, PorterDuff.Mode.CLEAR);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001444 dest.setMatrix(mIdentityMatrix);
Adam Cohen839f4a52010-08-26 17:36:48 -07001445 dest.drawBitmap(mask, xy[0], xy[1], mHolographicPaint);
Dianne Hackborn6311d0a2011-08-02 16:37:58 -07001446 mMaskCanvas.setBitmap(null);
Adam Cohen9b073942010-08-19 16:49:52 -07001447 mask.recycle();
1448 }
Adam Cohen32a42f12010-08-11 19:34:30 -07001449 }
Adam Cohenef17dd42011-01-20 17:20:57 -08001450}