blob: 6cc86b9b871cdcb0b491fe35ffc8a3b5a00dbbb2 [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
Chet Haasea18a86b2010-09-07 13:20:00 -070018import android.animation.ObjectAnimator;
Romain Guye5ebcb02010-10-15 13:57:28 -070019import android.animation.PropertyValuesHolder;
Adam Cohen44729e32010-07-22 16:00:07 -070020import android.content.Context;
Adam Cohen26f072c2011-04-01 16:23:18 -070021import android.content.res.TypedArray;
Adam Cohen32a42f12010-08-11 19:34:30 -070022import android.graphics.Bitmap;
Adam Cohen839f4a52010-08-26 17:36:48 -070023import android.graphics.BlurMaskFilter;
Adam Cohen32a42f12010-08-11 19:34:30 -070024import android.graphics.Canvas;
25import android.graphics.Matrix;
26import android.graphics.Paint;
27import android.graphics.PorterDuff;
28import android.graphics.PorterDuffXfermode;
Adam Cohen44729e32010-07-22 16:00:07 -070029import android.graphics.Rect;
Adam Cohen9b073942010-08-19 16:49:52 -070030import android.graphics.RectF;
Adam Cohen839f4a52010-08-26 17:36:48 -070031import android.graphics.TableMaskFilter;
Svetoslav Ganov48d15862012-05-15 10:10:00 -070032import android.os.Bundle;
Adam Cohen44729e32010-07-22 16:00:07 -070033import android.util.AttributeSet;
34import android.util.Log;
Adam Cohena8a7c922011-02-28 17:20:44 -080035import android.view.InputDevice;
Adam Cohen44729e32010-07-22 16:00:07 -070036import android.view.MotionEvent;
37import android.view.VelocityTracker;
38import android.view.View;
39import android.view.ViewConfiguration;
40import android.view.ViewGroup;
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -080041import android.view.accessibility.AccessibilityNodeInfo;
Adam Cohenb04f7ad2010-08-15 13:22:42 -070042import android.view.animation.LinearInterpolator;
Adam Cohen44729e32010-07-22 16:00:07 -070043import android.widget.RemoteViews.RemoteView;
44
Aurimas Liutikas99441c52016-10-11 16:48:32 -070045import java.lang.ref.WeakReference;
46
Adam Cohen44729e32010-07-22 16:00:07 -070047@RemoteView
48/**
49 * A view that displays its children in a stack and allows users to discretely swipe
50 * through the children.
51 */
52public class StackView extends AdapterViewAnimator {
53 private final String TAG = "StackView";
54
55 /**
56 * Default animation parameters
57 */
Romain Guye5ebcb02010-10-15 13:57:28 -070058 private static final int DEFAULT_ANIMATION_DURATION = 400;
59 private static final int MINIMUM_ANIMATION_DURATION = 50;
Adam Cohenc0b53be2010-12-17 19:23:41 -080060 private static final int STACK_RELAYOUT_DURATION = 100;
Adam Cohen44729e32010-07-22 16:00:07 -070061
62 /**
Adam Cohen839f4a52010-08-26 17:36:48 -070063 * Parameters effecting the perspective visuals
64 */
Adam Cohen026e1212010-12-13 12:29:17 -080065 private static final float PERSPECTIVE_SHIFT_FACTOR_Y = 0.1f;
66 private static final float PERSPECTIVE_SHIFT_FACTOR_X = 0.1f;
67
Adam Cohen36f43902010-12-15 21:17:33 -080068 private float mPerspectiveShiftX;
69 private float mPerspectiveShiftY;
70 private float mNewPerspectiveShiftX;
71 private float mNewPerspectiveShiftY;
72
Romain Guye5ebcb02010-10-15 13:57:28 -070073 @SuppressWarnings({"FieldCanBeLocal"})
Adam Cohen78db1aa2011-01-25 12:24:23 -080074 private static final float PERSPECTIVE_SCALE_FACTOR = 0f;
Adam Cohen839f4a52010-08-26 17:36:48 -070075
76 /**
77 * Represent the two possible stack modes, one where items slide up, and the other
78 * where items slide down. The perspective is also inverted between these two modes.
79 */
80 private static final int ITEMS_SLIDE_UP = 0;
81 private static final int ITEMS_SLIDE_DOWN = 1;
82
83 /**
Adam Cohen44729e32010-07-22 16:00:07 -070084 * These specify the different gesture states
85 */
Romain Guy5b53f912010-08-16 18:24:33 -070086 private static final int GESTURE_NONE = 0;
87 private static final int GESTURE_SLIDE_UP = 1;
88 private static final int GESTURE_SLIDE_DOWN = 2;
Adam Cohen44729e32010-07-22 16:00:07 -070089
90 /**
91 * Specifies how far you need to swipe (up or down) before it
92 * will be consider a completed gesture when you lift your finger
93 */
Adam Cohena9238c82010-10-25 14:01:29 -070094 private static final float SWIPE_THRESHOLD_RATIO = 0.2f;
95
96 /**
97 * Specifies the total distance, relative to the size of the stack,
98 * that views will be slid, either up or down
99 */
Romain Guy5b53f912010-08-16 18:24:33 -0700100 private static final float SLIDE_UP_RATIO = 0.7f;
Adam Cohen44729e32010-07-22 16:00:07 -0700101
Adam Cohen44729e32010-07-22 16:00:07 -0700102 /**
103 * Sentinel value for no current active pointer.
104 * Used by {@link #mActivePointerId}.
105 */
106 private static final int INVALID_POINTER = -1;
107
108 /**
Adam Cohen839f4a52010-08-26 17:36:48 -0700109 * Number of active views in the stack. One fewer view is actually visible, as one is hidden.
110 */
111 private static final int NUM_ACTIVE_VIEWS = 5;
112
Adam Cohendfcdddd2010-09-10 14:38:40 -0700113 private static final int FRAME_PADDING = 4;
Adam Cohen839f4a52010-08-26 17:36:48 -0700114
Patrick Dubroye80202d2010-11-16 15:41:08 -0800115 private final Rect mTouchRect = new Rect();
116
Adam Cohen26e30bb2010-12-03 18:16:12 -0800117 private static final int MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE = 5000;
118
Romain Guy4725a902011-05-25 10:54:29 -0700119 private static final long MIN_TIME_BETWEEN_SCROLLS = 100;
Adam Cohena8a7c922011-02-28 17:20:44 -0800120
Adam Cohen839f4a52010-08-26 17:36:48 -0700121 /**
Adam Cohen44729e32010-07-22 16:00:07 -0700122 * These variables are all related to the current state of touch interaction
123 * with the stack
124 */
Adam Cohen44729e32010-07-22 16:00:07 -0700125 private float mInitialY;
126 private float mInitialX;
127 private int mActivePointerId;
Adam Cohen44729e32010-07-22 16:00:07 -0700128 private int mYVelocity = 0;
129 private int mSwipeGestureType = GESTURE_NONE;
Adam Cohendfcdddd2010-09-10 14:38:40 -0700130 private int mSlideAmount;
Adam Cohen44729e32010-07-22 16:00:07 -0700131 private int mSwipeThreshold;
132 private int mTouchSlop;
133 private int mMaximumVelocity;
134 private VelocityTracker mVelocityTracker;
Adam Cohen3352b682010-10-26 13:54:00 -0700135 private boolean mTransitionIsSetup = false;
Adam Cohen26f072c2011-04-01 16:23:18 -0700136 private int mResOutColor;
137 private int mClickColor;
Adam Cohen44729e32010-07-22 16:00:07 -0700138
Adam Cohen9b073942010-08-19 16:49:52 -0700139 private static HolographicHelper sHolographicHelper;
Adam Cohen32a42f12010-08-11 19:34:30 -0700140 private ImageView mHighlight;
Adam Cohen8baf5df2010-11-11 15:23:41 -0800141 private ImageView mClickFeedback;
142 private boolean mClickFeedbackIsValid = false;
Adam Cohen32a42f12010-08-11 19:34:30 -0700143 private StackSlider mStackSlider;
Adam Cohen44729e32010-07-22 16:00:07 -0700144 private boolean mFirstLayoutHappened = false;
Adam Cohen26e30bb2010-12-03 18:16:12 -0800145 private long mLastInteractionTime = 0;
Adam Cohena8a7c922011-02-28 17:20:44 -0800146 private long mLastScrollTime;
Adam Cohen839f4a52010-08-26 17:36:48 -0700147 private int mStackMode;
Adam Cohendfcdddd2010-09-10 14:38:40 -0700148 private int mFramePadding;
Adam Cohen0ac116b2010-11-11 11:39:53 -0800149 private final Rect stackInvalidateRect = new Rect();
Adam Cohen44729e32010-07-22 16:00:07 -0700150
Winson Chungabc8b50a2011-09-06 18:38:50 -0700151 /**
152 * {@inheritDoc}
153 */
Adam Cohen44729e32010-07-22 16:00:07 -0700154 public StackView(Context context) {
Adam Cohen26f072c2011-04-01 16:23:18 -0700155 this(context, null);
Adam Cohen44729e32010-07-22 16:00:07 -0700156 }
157
Winson Chungabc8b50a2011-09-06 18:38:50 -0700158 /**
159 * {@inheritDoc}
160 */
Adam Cohen44729e32010-07-22 16:00:07 -0700161 public StackView(Context context, AttributeSet attrs) {
Adam Cohen26f072c2011-04-01 16:23:18 -0700162 this(context, attrs, com.android.internal.R.attr.stackViewStyle);
163 }
164
Winson Chungabc8b50a2011-09-06 18:38:50 -0700165 /**
166 * {@inheritDoc}
167 */
Adam Cohen26f072c2011-04-01 16:23:18 -0700168 public StackView(Context context, AttributeSet attrs, int defStyleAttr) {
Alan Viverette617feb92013-09-09 18:09:13 -0700169 this(context, attrs, defStyleAttr, 0);
170 }
171
172 /**
173 * {@inheritDoc}
174 */
175 public StackView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
176 super(context, attrs, defStyleAttr, defStyleRes);
177 final TypedArray a = context.obtainStyledAttributes(
178 attrs, com.android.internal.R.styleable.StackView, defStyleAttr, defStyleRes);
Adam Cohen26f072c2011-04-01 16:23:18 -0700179
180 mResOutColor = a.getColor(
181 com.android.internal.R.styleable.StackView_resOutColor, 0);
182 mClickColor = a.getColor(
183 com.android.internal.R.styleable.StackView_clickColor, 0);
184
185 a.recycle();
Adam Cohen44729e32010-07-22 16:00:07 -0700186 initStackView();
187 }
188
189 private void initStackView() {
Adam Cohen96d8d562010-10-24 11:12:18 -0700190 configureViewAnimator(NUM_ACTIVE_VIEWS, 1);
Adam Cohen44729e32010-07-22 16:00:07 -0700191 setStaticTransformationsEnabled(true);
192 final ViewConfiguration configuration = ViewConfiguration.get(getContext());
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700193 mTouchSlop = configuration.getScaledTouchSlop();
Adam Cohen44729e32010-07-22 16:00:07 -0700194 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
195 mActivePointerId = INVALID_POINTER;
Adam Cohen32a42f12010-08-11 19:34:30 -0700196
197 mHighlight = new ImageView(getContext());
198 mHighlight.setLayoutParams(new LayoutParams(mHighlight));
199 addViewInLayout(mHighlight, -1, new LayoutParams(mHighlight));
Adam Cohen8baf5df2010-11-11 15:23:41 -0800200
201 mClickFeedback = new ImageView(getContext());
202 mClickFeedback.setLayoutParams(new LayoutParams(mClickFeedback));
203 addViewInLayout(mClickFeedback, -1, new LayoutParams(mClickFeedback));
204 mClickFeedback.setVisibility(INVISIBLE);
205
Adam Cohen32a42f12010-08-11 19:34:30 -0700206 mStackSlider = new StackSlider();
207
Adam Cohen9b073942010-08-19 16:49:52 -0700208 if (sHolographicHelper == null) {
Adam Cohendfcdddd2010-09-10 14:38:40 -0700209 sHolographicHelper = new HolographicHelper(mContext);
Adam Cohen32a42f12010-08-11 19:34:30 -0700210 }
Adam Cohen9b073942010-08-19 16:49:52 -0700211 setClipChildren(false);
212 setClipToPadding(false);
Adam Cohen1480fdd2010-08-25 17:24:53 -0700213
Adam Cohen839f4a52010-08-26 17:36:48 -0700214 // This sets the form of the StackView, which is currently to have the perspective-shifted
215 // views above the active view, and have items slide down when sliding out. The opposite is
216 // available by using ITEMS_SLIDE_UP.
217 mStackMode = ITEMS_SLIDE_DOWN;
218
Adam Cohen1480fdd2010-08-25 17:24:53 -0700219 // This is a flag to indicate the the stack is loading for the first time
220 mWhichChild = -1;
Adam Cohendfcdddd2010-09-10 14:38:40 -0700221
222 // Adjust the frame padding based on the density, since the highlight changes based
223 // on the density
224 final float density = mContext.getResources().getDisplayMetrics().density;
225 mFramePadding = (int) Math.ceil(density * FRAME_PADDING);
Adam Cohen44729e32010-07-22 16:00:07 -0700226 }
227
228 /**
229 * Animate the views between different relative indexes within the {@link AdapterViewAnimator}
230 */
Adam Cohen78db1aa2011-01-25 12:24:23 -0800231 void transformViewForTransition(int fromIndex, int toIndex, final View view, boolean animate) {
Adam Cohen78db1aa2011-01-25 12:24:23 -0800232 if (!animate) {
233 ((StackFrame) view).cancelSliderAnimator();
234 view.setRotationX(0f);
235 LayoutParams lp = (LayoutParams) view.getLayoutParams();
236 lp.setVerticalOffset(0);
237 lp.setHorizontalOffset(0);
Adam Cohen69d66e02011-01-12 14:39:13 -0800238 }
239
Adam Cohen50204582011-01-16 17:28:25 -0800240 if (fromIndex == -1 && toIndex == getNumActiveViews() -1) {
Adam Cohen50204582011-01-16 17:28:25 -0800241 transformViewAtIndex(toIndex, view, false);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700242 view.setVisibility(VISIBLE);
Adam Cohenc798b662011-10-21 12:07:38 -0700243 view.setAlpha(1.0f);
Adam Cohen96d8d562010-10-24 11:12:18 -0700244 } else if (fromIndex == 0 && toIndex == 1) {
Adam Cohen44729e32010-07-22 16:00:07 -0700245 // Slide item in
Adam Cohen78db1aa2011-01-25 12:24:23 -0800246 ((StackFrame) view).cancelSliderAnimator();
Adam Cohen44729e32010-07-22 16:00:07 -0700247 view.setVisibility(VISIBLE);
Adam Cohen32a42f12010-08-11 19:34:30 -0700248
Adam Cohen839f4a52010-08-26 17:36:48 -0700249 int duration = Math.round(mStackSlider.getDurationForNeutralPosition(mYVelocity));
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700250 StackSlider animationSlider = new StackSlider(mStackSlider);
Adam Cohen69d66e02011-01-12 14:39:13 -0800251 animationSlider.setView(view);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800252
253 if (animate) {
254 PropertyValuesHolder slideInY = PropertyValuesHolder.ofFloat("YProgress", 0.0f);
255 PropertyValuesHolder slideInX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
256 ObjectAnimator slideIn = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
257 slideInX, slideInY);
258 slideIn.setDuration(duration);
259 slideIn.setInterpolator(new LinearInterpolator());
260 ((StackFrame) view).setSliderAnimator(slideIn);
261 slideIn.start();
262 } else {
263 animationSlider.setYProgress(0f);
264 animationSlider.setXProgress(0f);
265 }
Adam Cohen96d8d562010-10-24 11:12:18 -0700266 } else if (fromIndex == 1 && toIndex == 0) {
Adam Cohen44729e32010-07-22 16:00:07 -0700267 // Slide item out
Adam Cohen78db1aa2011-01-25 12:24:23 -0800268 ((StackFrame) view).cancelSliderAnimator();
Adam Cohen839f4a52010-08-26 17:36:48 -0700269 int duration = Math.round(mStackSlider.getDurationForOffscreenPosition(mYVelocity));
Adam Cohen44729e32010-07-22 16:00:07 -0700270
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700271 StackSlider animationSlider = new StackSlider(mStackSlider);
Adam Cohen69d66e02011-01-12 14:39:13 -0800272 animationSlider.setView(view);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800273 if (animate) {
274 PropertyValuesHolder slideOutY = PropertyValuesHolder.ofFloat("YProgress", 1.0f);
275 PropertyValuesHolder slideOutX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
276 ObjectAnimator slideOut = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
277 slideOutX, slideOutY);
278 slideOut.setDuration(duration);
279 slideOut.setInterpolator(new LinearInterpolator());
280 ((StackFrame) view).setSliderAnimator(slideOut);
281 slideOut.start();
282 } else {
283 animationSlider.setYProgress(1.0f);
284 animationSlider.setXProgress(0f);
285 }
Adam Cohen69d66e02011-01-12 14:39:13 -0800286 } else if (toIndex == 0) {
Adam Cohen44729e32010-07-22 16:00:07 -0700287 // Make sure this view that is "waiting in the wings" is invisible
288 view.setAlpha(0.0f);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700289 view.setVisibility(INVISIBLE);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800290 } else if ((fromIndex == 0 || fromIndex == 1) && toIndex > 1) {
Adam Cohen69d66e02011-01-12 14:39:13 -0800291 view.setVisibility(VISIBLE);
292 view.setAlpha(1.0f);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800293 view.setRotationX(0f);
294 LayoutParams lp = (LayoutParams) view.getLayoutParams();
295 lp.setVerticalOffset(0);
296 lp.setHorizontalOffset(0);
Adam Cohenc0b53be2010-12-17 19:23:41 -0800297 } else if (fromIndex == -1) {
298 view.setAlpha(1.0f);
299 view.setVisibility(VISIBLE);
Adam Cohen44729e32010-07-22 16:00:07 -0700300 } else if (toIndex == -1) {
Adam Cohen78db1aa2011-01-25 12:24:23 -0800301 if (animate) {
Adam Cohenc798b662011-10-21 12:07:38 -0700302 postDelayed(new Runnable() {
303 public void run() {
304 view.setAlpha(0);
305 }
306 }, STACK_RELAYOUT_DURATION);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800307 } else {
308 view.setAlpha(0f);
309 }
Adam Cohen44729e32010-07-22 16:00:07 -0700310 }
Adam Cohen839f4a52010-08-26 17:36:48 -0700311
312 // Implement the faked perspective
313 if (toIndex != -1) {
Adam Cohen78db1aa2011-01-25 12:24:23 -0800314 transformViewAtIndex(toIndex, view, animate);
Adam Cohenf04e2252010-09-09 18:41:20 -0700315 }
316 }
Adam Cohen839f4a52010-08-26 17:36:48 -0700317
Adam Cohen36f43902010-12-15 21:17:33 -0800318 private void transformViewAtIndex(int index, final View view, boolean animate) {
319 final float maxPerspectiveShiftY = mPerspectiveShiftY;
320 final float maxPerspectiveShiftX = mPerspectiveShiftX;
Adam Cohen026e1212010-12-13 12:29:17 -0800321
Adam Cohenc99ff732011-01-16 16:11:41 -0800322 if (mStackMode == ITEMS_SLIDE_DOWN) {
323 index = mMaxNumActiveViews - index - 1;
324 if (index == mMaxNumActiveViews - 1) index--;
325 } else {
326 index--;
327 if (index < 0) index++;
328 }
Adam Cohen026e1212010-12-13 12:29:17 -0800329
330 float r = (index * 1.0f) / (mMaxNumActiveViews - 2);
331
Adam Cohen36f43902010-12-15 21:17:33 -0800332 final float scale = 1 - PERSPECTIVE_SCALE_FACTOR * (1 - r);
Adam Cohen026e1212010-12-13 12:29:17 -0800333
Adam Cohenc99ff732011-01-16 16:11:41 -0800334 float perspectiveTranslationY = r * maxPerspectiveShiftY;
335 float scaleShiftCorrectionY = (scale - 1) *
Adam Cohen026e1212010-12-13 12:29:17 -0800336 (getMeasuredHeight() * (1 - PERSPECTIVE_SHIFT_FACTOR_Y) / 2.0f);
Adam Cohen36f43902010-12-15 21:17:33 -0800337 final float transY = perspectiveTranslationY + scaleShiftCorrectionY;
Adam Cohen026e1212010-12-13 12:29:17 -0800338
339 float perspectiveTranslationX = (1 - r) * maxPerspectiveShiftX;
340 float scaleShiftCorrectionX = (1 - scale) *
341 (getMeasuredWidth() * (1 - PERSPECTIVE_SHIFT_FACTOR_X) / 2.0f);
Adam Cohen36f43902010-12-15 21:17:33 -0800342 final float transX = perspectiveTranslationX + scaleShiftCorrectionX;
Adam Cohen026e1212010-12-13 12:29:17 -0800343
Adam Cohenc99ff732011-01-16 16:11:41 -0800344 // If this view is currently being animated for a certain position, we need to cancel
Adam Cohen69d66e02011-01-12 14:39:13 -0800345 // this animation so as not to interfere with the new transformation.
Adam Cohen78db1aa2011-01-25 12:24:23 -0800346 if (view instanceof StackFrame) {
347 ((StackFrame) view).cancelTransformAnimator();
Adam Cohen69d66e02011-01-12 14:39:13 -0800348 }
349
Adam Cohen36f43902010-12-15 21:17:33 -0800350 if (animate) {
351 PropertyValuesHolder translationX = PropertyValuesHolder.ofFloat("translationX", transX);
352 PropertyValuesHolder translationY = PropertyValuesHolder.ofFloat("translationY", transY);
353 PropertyValuesHolder scalePropX = PropertyValuesHolder.ofFloat("scaleX", scale);
354 PropertyValuesHolder scalePropY = PropertyValuesHolder.ofFloat("scaleY", scale);
Adam Cohen026e1212010-12-13 12:29:17 -0800355
Adam Cohen36f43902010-12-15 21:17:33 -0800356 ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(view, scalePropX, scalePropY,
357 translationY, translationX);
Adam Cohenc0b53be2010-12-17 19:23:41 -0800358 oa.setDuration(STACK_RELAYOUT_DURATION);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800359 if (view instanceof StackFrame) {
360 ((StackFrame) view).setTransformAnimator(oa);
361 }
Adam Cohen36f43902010-12-15 21:17:33 -0800362 oa.start();
363 } else {
Adam Cohen36f43902010-12-15 21:17:33 -0800364 view.setTranslationX(transX);
365 view.setTranslationY(transY);
366 view.setScaleX(scale);
367 view.setScaleY(scale);
368 }
Adam Cohen026e1212010-12-13 12:29:17 -0800369 }
370
Adam Cohen3352b682010-10-26 13:54:00 -0700371 private void setupStackSlider(View v, int mode) {
372 mStackSlider.setMode(mode);
373 if (v != null) {
Adam Cohen26f072c2011-04-01 16:23:18 -0700374 mHighlight.setImageBitmap(sHolographicHelper.createResOutline(v, mResOutColor));
Adam Cohen3352b682010-10-26 13:54:00 -0700375 mHighlight.setRotation(v.getRotation());
376 mHighlight.setTranslationY(v.getTranslationY());
Adam Cohen026e1212010-12-13 12:29:17 -0800377 mHighlight.setTranslationX(v.getTranslationX());
Adam Cohen3352b682010-10-26 13:54:00 -0700378 mHighlight.bringToFront();
379 v.bringToFront();
380 mStackSlider.setView(v);
381
382 v.setVisibility(VISIBLE);
383 }
384 }
385
Adam Cohen0e2de6d2011-01-19 17:16:34 -0800386 /**
387 * {@inheritDoc}
388 */
Adam Cohen3352b682010-10-26 13:54:00 -0700389 @Override
390 @android.view.RemotableViewMethod
391 public void showNext() {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800392 if (mSwipeGestureType != GESTURE_NONE) return;
Adam Cohen3352b682010-10-26 13:54:00 -0700393 if (!mTransitionIsSetup) {
394 View v = getViewAtRelativeIndex(1);
395 if (v != null) {
396 setupStackSlider(v, StackSlider.NORMAL_MODE);
397 mStackSlider.setYProgress(0);
398 mStackSlider.setXProgress(0);
399 }
400 }
401 super.showNext();
402 }
403
Adam Cohen0e2de6d2011-01-19 17:16:34 -0800404 /**
405 * {@inheritDoc}
406 */
Adam Cohen3352b682010-10-26 13:54:00 -0700407 @Override
408 @android.view.RemotableViewMethod
409 public void showPrevious() {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800410 if (mSwipeGestureType != GESTURE_NONE) return;
Adam Cohen3352b682010-10-26 13:54:00 -0700411 if (!mTransitionIsSetup) {
412 View v = getViewAtRelativeIndex(0);
413 if (v != null) {
414 setupStackSlider(v, StackSlider.NORMAL_MODE);
415 mStackSlider.setYProgress(1);
416 mStackSlider.setXProgress(0);
417 }
418 }
419 super.showPrevious();
420 }
421
Adam Cohen96d8d562010-10-24 11:12:18 -0700422 @Override
Adam Cohenef17dd42011-01-20 17:20:57 -0800423 void showOnly(int childIndex, boolean animate) {
424 super.showOnly(childIndex, animate);
Adam Cohen96d8d562010-10-24 11:12:18 -0700425
426 // Here we need to make sure that the z-order of the children is correct
Adam Cohen3352b682010-10-26 13:54:00 -0700427 for (int i = mCurrentWindowEnd; i >= mCurrentWindowStart; i--) {
Adam Cohen96d8d562010-10-24 11:12:18 -0700428 int index = modulo(i, getWindowSize());
Adam Cohend38a0ce2011-04-06 13:20:42 -0700429 ViewAndMetaData vm = mViewsMap.get(index);
430 if (vm != null) {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800431 View v = mViewsMap.get(index).view;
432 if (v != null) v.bringToFront();
433 }
Adam Cohen96d8d562010-10-24 11:12:18 -0700434 }
Adam Cohen78db1aa2011-01-25 12:24:23 -0800435 if (mHighlight != null) {
436 mHighlight.bringToFront();
437 }
Adam Cohen3352b682010-10-26 13:54:00 -0700438 mTransitionIsSetup = false;
Adam Cohen8baf5df2010-11-11 15:23:41 -0800439 mClickFeedbackIsValid = false;
440 }
441
442 void updateClickFeedback() {
443 if (!mClickFeedbackIsValid) {
Adam Cohen9c295482010-11-18 15:19:48 -0800444 View v = getViewAtRelativeIndex(1);
Adam Cohen8baf5df2010-11-11 15:23:41 -0800445 if (v != null) {
Adam Cohen26f072c2011-04-01 16:23:18 -0700446 mClickFeedback.setImageBitmap(
447 sHolographicHelper.createClickOutline(v, mClickColor));
Adam Cohen8baf5df2010-11-11 15:23:41 -0800448 mClickFeedback.setTranslationX(v.getTranslationX());
449 mClickFeedback.setTranslationY(v.getTranslationY());
450 }
451 mClickFeedbackIsValid = true;
452 }
453 }
454
455 @Override
456 void showTapFeedback(View v) {
457 updateClickFeedback();
458 mClickFeedback.setVisibility(VISIBLE);
459 mClickFeedback.bringToFront();
460 invalidate();
461 }
462
463 @Override
464 void hideTapFeedback(View v) {
465 mClickFeedback.setVisibility(INVISIBLE);
466 invalidate();
Adam Cohen96d8d562010-10-24 11:12:18 -0700467 }
468
Adam Cohenf04e2252010-09-09 18:41:20 -0700469 private void updateChildTransforms() {
Adam Cohen96d8d562010-10-24 11:12:18 -0700470 for (int i = 0; i < getNumActiveViews(); i++) {
Adam Cohenf04e2252010-09-09 18:41:20 -0700471 View v = getViewAtRelativeIndex(i);
472 if (v != null) {
Adam Cohen36f43902010-12-15 21:17:33 -0800473 transformViewAtIndex(i, v, false);
Adam Cohenf04e2252010-09-09 18:41:20 -0700474 }
Adam Cohen839f4a52010-08-26 17:36:48 -0700475 }
Adam Cohen44729e32010-07-22 16:00:07 -0700476 }
477
Adam Cohen78db1aa2011-01-25 12:24:23 -0800478 private static class StackFrame extends FrameLayout {
Adam Cohen78db1aa2011-01-25 12:24:23 -0800479 WeakReference<ObjectAnimator> transformAnimator;
480 WeakReference<ObjectAnimator> sliderAnimator;
481
482 public StackFrame(Context context) {
483 super(context);
484 }
485
Adam Cohen78db1aa2011-01-25 12:24:23 -0800486 void setTransformAnimator(ObjectAnimator oa) {
487 transformAnimator = new WeakReference<ObjectAnimator>(oa);
488 }
489
490 void setSliderAnimator(ObjectAnimator oa) {
491 sliderAnimator = new WeakReference<ObjectAnimator>(oa);
492 }
493
Adam Cohen78db1aa2011-01-25 12:24:23 -0800494 boolean cancelTransformAnimator() {
495 if (transformAnimator != null) {
496 ObjectAnimator oa = transformAnimator.get();
497 if (oa != null) {
498 oa.cancel();
499 return true;
500 }
501 }
502 return false;
503 }
504
505 boolean cancelSliderAnimator() {
506 if (sliderAnimator != null) {
507 ObjectAnimator oa = sliderAnimator.get();
508 if (oa != null) {
509 oa.cancel();
510 return true;
511 }
512 }
513 return false;
514 }
515 }
516
Adam Cohendfcdddd2010-09-10 14:38:40 -0700517 @Override
518 FrameLayout getFrameForChild() {
Adam Cohen78db1aa2011-01-25 12:24:23 -0800519 StackFrame fl = new StackFrame(mContext);
Adam Cohendfcdddd2010-09-10 14:38:40 -0700520 fl.setPadding(mFramePadding, mFramePadding, mFramePadding, mFramePadding);
521 return fl;
522 }
523
Adam Cohen44729e32010-07-22 16:00:07 -0700524 /**
525 * Apply any necessary tranforms for the child that is being added.
526 */
527 void applyTransformForChildAtIndex(View child, int relativeIndex) {
Adam Cohen44729e32010-07-22 16:00:07 -0700528 }
529
530 @Override
Adam Cohen9b073942010-08-19 16:49:52 -0700531 protected void dispatchDraw(Canvas canvas) {
Adam Cohen321aa2b2011-03-08 13:33:29 -0800532 boolean expandClipRegion = false;
533
Adam Cohen0ac116b2010-11-11 11:39:53 -0800534 canvas.getClipBounds(stackInvalidateRect);
Adam Cohend51bbb52010-10-18 10:59:49 -0700535 final int childCount = getChildCount();
536 for (int i = 0; i < childCount; i++) {
Adam Cohene86ff4d2011-01-21 17:46:11 -0800537 final View child = getChildAt(i);
538 LayoutParams lp = (LayoutParams) child.getLayoutParams();
539 if ((lp.horizontalOffset == 0 && lp.verticalOffset == 0) ||
540 child.getAlpha() == 0f || child.getVisibility() != VISIBLE) {
541 lp.resetInvalidateRect();
542 }
Adam Cohen321aa2b2011-03-08 13:33:29 -0800543 Rect childInvalidateRect = lp.getInvalidateRect();
544 if (!childInvalidateRect.isEmpty()) {
545 expandClipRegion = true;
546 stackInvalidateRect.union(childInvalidateRect);
547 }
Adam Cohen44729e32010-07-22 16:00:07 -0700548 }
Adam Cohen321aa2b2011-03-08 13:33:29 -0800549
550 // We only expand the clip bounds if necessary.
551 if (expandClipRegion) {
Derek Sollenberger2ad19e52018-04-18 15:03:09 -0400552 canvas.save();
553 canvas.clipRectUnion(stackInvalidateRect);
Adam Cohen321aa2b2011-03-08 13:33:29 -0800554 super.dispatchDraw(canvas);
555 canvas.restore();
556 } else {
557 super.dispatchDraw(canvas);
558 }
Adam Cohen9b073942010-08-19 16:49:52 -0700559 }
Adam Cohen44729e32010-07-22 16:00:07 -0700560
Adam Cohen9b073942010-08-19 16:49:52 -0700561 private void onLayout() {
Adam Cohen44729e32010-07-22 16:00:07 -0700562 if (!mFirstLayoutHappened) {
Adam Cohen44729e32010-07-22 16:00:07 -0700563 mFirstLayoutHappened = true;
Adam Cohen53838d22011-01-26 21:32:33 -0800564 updateChildTransforms();
Adam Cohen44729e32010-07-22 16:00:07 -0700565 }
Adam Cohen36f43902010-12-15 21:17:33 -0800566
Adam Cohenc6a47162011-01-27 18:05:02 -0800567 final int newSlideAmount = Math.round(SLIDE_UP_RATIO * getMeasuredHeight());
568 if (mSlideAmount != newSlideAmount) {
569 mSlideAmount = newSlideAmount;
570 mSwipeThreshold = Math.round(SWIPE_THRESHOLD_RATIO * newSlideAmount);
571 }
572
Adam Cohen36f43902010-12-15 21:17:33 -0800573 if (Float.compare(mPerspectiveShiftY, mNewPerspectiveShiftY) != 0 ||
574 Float.compare(mPerspectiveShiftX, mNewPerspectiveShiftX) != 0) {
Adam Cohen78db1aa2011-01-25 12:24:23 -0800575
Adam Cohen36f43902010-12-15 21:17:33 -0800576 mPerspectiveShiftY = mNewPerspectiveShiftY;
577 mPerspectiveShiftX = mNewPerspectiveShiftX;
Adam Cohen53838d22011-01-26 21:32:33 -0800578 updateChildTransforms();
Adam Cohen36f43902010-12-15 21:17:33 -0800579 }
Adam Cohen44729e32010-07-22 16:00:07 -0700580 }
581
Adam Cohena8a7c922011-02-28 17:20:44 -0800582 @Override
583 public boolean onGenericMotionEvent(MotionEvent event) {
584 if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
585 switch (event.getAction()) {
586 case MotionEvent.ACTION_SCROLL: {
587 final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
588 if (vscroll < 0) {
589 pacedScroll(false);
590 return true;
591 } else if (vscroll > 0) {
592 pacedScroll(true);
593 return true;
594 }
595 }
596 }
597 }
598 return super.onGenericMotionEvent(event);
599 }
600
601 // This ensures that the frequency of stack flips caused by scrolls is capped
602 private void pacedScroll(boolean up) {
603 long timeSinceLastScroll = System.currentTimeMillis() - mLastScrollTime;
604 if (timeSinceLastScroll > MIN_TIME_BETWEEN_SCROLLS) {
605 if (up) {
606 showPrevious();
607 } else {
608 showNext();
609 }
610 mLastScrollTime = System.currentTimeMillis();
611 }
612 }
613
Adam Cohen0e2de6d2011-01-19 17:16:34 -0800614 /**
615 * {@inheritDoc}
616 */
Adam Cohen44729e32010-07-22 16:00:07 -0700617 @Override
618 public boolean onInterceptTouchEvent(MotionEvent ev) {
619 int action = ev.getAction();
620 switch(action & MotionEvent.ACTION_MASK) {
Adam Cohen44729e32010-07-22 16:00:07 -0700621 case MotionEvent.ACTION_DOWN: {
622 if (mActivePointerId == INVALID_POINTER) {
623 mInitialX = ev.getX();
624 mInitialY = ev.getY();
625 mActivePointerId = ev.getPointerId(0);
626 }
627 break;
628 }
629 case MotionEvent.ACTION_MOVE: {
630 int pointerIndex = ev.findPointerIndex(mActivePointerId);
631 if (pointerIndex == INVALID_POINTER) {
632 // no data for our primary pointer, this shouldn't happen, log it
633 Log.d(TAG, "Error: No data for our primary pointer.");
634 return false;
635 }
Adam Cohen44729e32010-07-22 16:00:07 -0700636 float newY = ev.getY(pointerIndex);
637 float deltaY = newY - mInitialY;
638
Adam Cohen32a42f12010-08-11 19:34:30 -0700639 beginGestureIfNeeded(deltaY);
Adam Cohen44729e32010-07-22 16:00:07 -0700640 break;
641 }
642 case MotionEvent.ACTION_POINTER_UP: {
643 onSecondaryPointerUp(ev);
644 break;
645 }
646 case MotionEvent.ACTION_UP:
647 case MotionEvent.ACTION_CANCEL: {
648 mActivePointerId = INVALID_POINTER;
649 mSwipeGestureType = GESTURE_NONE;
Adam Cohen44729e32010-07-22 16:00:07 -0700650 }
651 }
652
653 return mSwipeGestureType != GESTURE_NONE;
654 }
655
Adam Cohen32a42f12010-08-11 19:34:30 -0700656 private void beginGestureIfNeeded(float deltaY) {
657 if ((int) Math.abs(deltaY) > mTouchSlop && mSwipeGestureType == GESTURE_NONE) {
Adam Cohenc99ff732011-01-16 16:11:41 -0800658 final int swipeGestureType = deltaY < 0 ? GESTURE_SLIDE_UP : GESTURE_SLIDE_DOWN;
Adam Cohen32a42f12010-08-11 19:34:30 -0700659 cancelLongPress();
660 requestDisallowInterceptTouchEvent(true);
661
Adam Cohend51bbb52010-10-18 10:59:49 -0700662 if (mAdapter == null) return;
Adam Cohenef17dd42011-01-20 17:20:57 -0800663 final int adapterCount = getCount();
Adam Cohend51bbb52010-10-18 10:59:49 -0700664
Adam Cohen839f4a52010-08-26 17:36:48 -0700665 int activeIndex;
666 if (mStackMode == ITEMS_SLIDE_UP) {
Adam Cohen96d8d562010-10-24 11:12:18 -0700667 activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1;
Adam Cohen839f4a52010-08-26 17:36:48 -0700668 } else {
Adam Cohen96d8d562010-10-24 11:12:18 -0700669 activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 1 : 0;
Adam Cohen839f4a52010-08-26 17:36:48 -0700670 }
Adam Cohen32a42f12010-08-11 19:34:30 -0700671
Aurimas Liutikas99441c52016-10-11 16:48:32 -0700672 boolean endOfStack = mLoopViews && adapterCount == 1
673 && ((mStackMode == ITEMS_SLIDE_UP && swipeGestureType == GESTURE_SLIDE_UP)
674 || (mStackMode == ITEMS_SLIDE_DOWN && swipeGestureType == GESTURE_SLIDE_DOWN));
675 boolean beginningOfStack = mLoopViews && adapterCount == 1
676 && ((mStackMode == ITEMS_SLIDE_DOWN && swipeGestureType == GESTURE_SLIDE_UP)
677 || (mStackMode == ITEMS_SLIDE_UP && swipeGestureType == GESTURE_SLIDE_DOWN));
Adam Cohenc99ff732011-01-16 16:11:41 -0800678
Adam Cohen3352b682010-10-26 13:54:00 -0700679 int stackMode;
Adam Cohenc99ff732011-01-16 16:11:41 -0800680 if (mLoopViews && !beginningOfStack && !endOfStack) {
Adam Cohen3352b682010-10-26 13:54:00 -0700681 stackMode = StackSlider.NORMAL_MODE;
Adam Cohenc99ff732011-01-16 16:11:41 -0800682 } else if (mCurrentWindowStartUnbounded + activeIndex == -1 || beginningOfStack) {
Adam Cohen96d8d562010-10-24 11:12:18 -0700683 activeIndex++;
Adam Cohen3352b682010-10-26 13:54:00 -0700684 stackMode = StackSlider.BEGINNING_OF_STACK_MODE;
Adam Cohenc99ff732011-01-16 16:11:41 -0800685 } else if (mCurrentWindowStartUnbounded + activeIndex == adapterCount - 1 || endOfStack) {
Adam Cohen3352b682010-10-26 13:54:00 -0700686 stackMode = StackSlider.END_OF_STACK_MODE;
Adam Cohen3d07af02010-08-18 17:46:23 -0700687 } else {
Adam Cohen3352b682010-10-26 13:54:00 -0700688 stackMode = StackSlider.NORMAL_MODE;
Adam Cohen32a42f12010-08-11 19:34:30 -0700689 }
Adam Cohen3d07af02010-08-18 17:46:23 -0700690
Adam Cohen3352b682010-10-26 13:54:00 -0700691 mTransitionIsSetup = stackMode == StackSlider.NORMAL_MODE;
692
Adam Cohen3d07af02010-08-18 17:46:23 -0700693 View v = getViewAtRelativeIndex(activeIndex);
694 if (v == null) return;
695
Adam Cohen3352b682010-10-26 13:54:00 -0700696 setupStackSlider(v, stackMode);
Adam Cohen3d07af02010-08-18 17:46:23 -0700697
698 // We only register this gesture if we've made it this far without a problem
699 mSwipeGestureType = swipeGestureType;
Adam Cohena32edd42010-10-26 10:35:01 -0700700 cancelHandleClick();
Adam Cohen32a42f12010-08-11 19:34:30 -0700701 }
702 }
703
Adam Cohen0e2de6d2011-01-19 17:16:34 -0800704 /**
705 * {@inheritDoc}
706 */
Adam Cohen44729e32010-07-22 16:00:07 -0700707 @Override
708 public boolean onTouchEvent(MotionEvent ev) {
Adam Cohena32edd42010-10-26 10:35:01 -0700709 super.onTouchEvent(ev);
710
Adam Cohen44729e32010-07-22 16:00:07 -0700711 int action = ev.getAction();
712 int pointerIndex = ev.findPointerIndex(mActivePointerId);
713 if (pointerIndex == INVALID_POINTER) {
714 // no data for our primary pointer, this shouldn't happen, log it
715 Log.d(TAG, "Error: No data for our primary pointer.");
716 return false;
717 }
718
719 float newY = ev.getY(pointerIndex);
Adam Cohen32a42f12010-08-11 19:34:30 -0700720 float newX = ev.getX(pointerIndex);
Adam Cohen44729e32010-07-22 16:00:07 -0700721 float deltaY = newY - mInitialY;
Adam Cohen32a42f12010-08-11 19:34:30 -0700722 float deltaX = newX - mInitialX;
Adam Cohen44729e32010-07-22 16:00:07 -0700723 if (mVelocityTracker == null) {
724 mVelocityTracker = VelocityTracker.obtain();
725 }
726 mVelocityTracker.addMovement(ev);
727
728 switch (action & MotionEvent.ACTION_MASK) {
729 case MotionEvent.ACTION_MOVE: {
Adam Cohen32a42f12010-08-11 19:34:30 -0700730 beginGestureIfNeeded(deltaY);
731
Adam Cohendfcdddd2010-09-10 14:38:40 -0700732 float rx = deltaX / (mSlideAmount * 1.0f);
Adam Cohen32a42f12010-08-11 19:34:30 -0700733 if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
Adam Cohendfcdddd2010-09-10 14:38:40 -0700734 float r = (deltaY - mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
Adam Cohen839f4a52010-08-26 17:36:48 -0700735 if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
Adam Cohen32a42f12010-08-11 19:34:30 -0700736 mStackSlider.setYProgress(1 - r);
737 mStackSlider.setXProgress(rx);
738 return true;
739 } else if (mSwipeGestureType == GESTURE_SLIDE_UP) {
Adam Cohendfcdddd2010-09-10 14:38:40 -0700740 float r = -(deltaY + mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
Adam Cohen839f4a52010-08-26 17:36:48 -0700741 if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
Adam Cohen32a42f12010-08-11 19:34:30 -0700742 mStackSlider.setYProgress(r);
743 mStackSlider.setXProgress(rx);
744 return true;
Adam Cohen44729e32010-07-22 16:00:07 -0700745 }
Adam Cohen44729e32010-07-22 16:00:07 -0700746 break;
747 }
748 case MotionEvent.ACTION_UP: {
749 handlePointerUp(ev);
750 break;
751 }
752 case MotionEvent.ACTION_POINTER_UP: {
753 onSecondaryPointerUp(ev);
754 break;
755 }
756 case MotionEvent.ACTION_CANCEL: {
757 mActivePointerId = INVALID_POINTER;
Adam Cohen44729e32010-07-22 16:00:07 -0700758 mSwipeGestureType = GESTURE_NONE;
Adam Cohen44729e32010-07-22 16:00:07 -0700759 break;
760 }
761 }
762 return true;
763 }
764
Adam Cohen44729e32010-07-22 16:00:07 -0700765 private void onSecondaryPointerUp(MotionEvent ev) {
766 final int activePointerIndex = ev.getActionIndex();
767 final int pointerId = ev.getPointerId(activePointerIndex);
768 if (pointerId == mActivePointerId) {
769
Adam Cohen96d8d562010-10-24 11:12:18 -0700770 int activeViewIndex = (mSwipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1;
Adam Cohen44729e32010-07-22 16:00:07 -0700771
772 View v = getViewAtRelativeIndex(activeViewIndex);
773 if (v == null) return;
774
775 // Our primary pointer has gone up -- let's see if we can find
776 // another pointer on the view. If so, then we should replace
777 // our primary pointer with this new pointer and adjust things
778 // so that the view doesn't jump
779 for (int index = 0; index < ev.getPointerCount(); index++) {
780 if (index != activePointerIndex) {
781
782 float x = ev.getX(index);
783 float y = ev.getY(index);
784
Patrick Dubroye80202d2010-11-16 15:41:08 -0800785 mTouchRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
786 if (mTouchRect.contains(Math.round(x), Math.round(y))) {
Adam Cohen44729e32010-07-22 16:00:07 -0700787 float oldX = ev.getX(activePointerIndex);
788 float oldY = ev.getY(activePointerIndex);
789
790 // adjust our frame of reference to avoid a jump
791 mInitialY += (y - oldY);
792 mInitialX += (x - oldX);
793
794 mActivePointerId = ev.getPointerId(index);
795 if (mVelocityTracker != null) {
796 mVelocityTracker.clear();
797 }
798 // ok, we're good, we found a new pointer which is touching the active view
799 return;
800 }
801 }
802 }
803 // if we made it this far, it means we didn't find a satisfactory new pointer :(,
Adam Cohen3d07af02010-08-18 17:46:23 -0700804 // so end the gesture
Adam Cohen44729e32010-07-22 16:00:07 -0700805 handlePointerUp(ev);
806 }
807 }
808
809 private void handlePointerUp(MotionEvent ev) {
810 int pointerIndex = ev.findPointerIndex(mActivePointerId);
811 float newY = ev.getY(pointerIndex);
812 int deltaY = (int) (newY - mInitialY);
Adam Cohen26e30bb2010-12-03 18:16:12 -0800813 mLastInteractionTime = System.currentTimeMillis();
Adam Cohen44729e32010-07-22 16:00:07 -0700814
Adam Cohen3d07af02010-08-18 17:46:23 -0700815 if (mVelocityTracker != null) {
816 mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
817 mYVelocity = (int) mVelocityTracker.getYVelocity(mActivePointerId);
818 }
Adam Cohen44729e32010-07-22 16:00:07 -0700819
820 if (mVelocityTracker != null) {
821 mVelocityTracker.recycle();
822 mVelocityTracker = null;
823 }
824
Adam Cohen3d07af02010-08-18 17:46:23 -0700825 if (deltaY > mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_DOWN
826 && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800827 // We reset the gesture variable, because otherwise we will ignore showPrevious() /
828 // showNext();
829 mSwipeGestureType = GESTURE_NONE;
830
Adam Cohen44729e32010-07-22 16:00:07 -0700831 // Swipe threshold exceeded, swipe down
Adam Cohen839f4a52010-08-26 17:36:48 -0700832 if (mStackMode == ITEMS_SLIDE_UP) {
Adam Cohen839f4a52010-08-26 17:36:48 -0700833 showPrevious();
Adam Cohen96d8d562010-10-24 11:12:18 -0700834 } else {
835 showNext();
Adam Cohen839f4a52010-08-26 17:36:48 -0700836 }
Adam Cohen32a42f12010-08-11 19:34:30 -0700837 mHighlight.bringToFront();
Adam Cohen3d07af02010-08-18 17:46:23 -0700838 } else if (deltaY < -mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_UP
839 && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800840 // We reset the gesture variable, because otherwise we will ignore showPrevious() /
841 // showNext();
842 mSwipeGestureType = GESTURE_NONE;
843
Adam Cohen44729e32010-07-22 16:00:07 -0700844 // Swipe threshold exceeded, swipe up
Adam Cohen839f4a52010-08-26 17:36:48 -0700845 if (mStackMode == ITEMS_SLIDE_UP) {
Adam Cohen839f4a52010-08-26 17:36:48 -0700846 showNext();
Adam Cohen96d8d562010-10-24 11:12:18 -0700847 } else {
848 showPrevious();
Adam Cohen839f4a52010-08-26 17:36:48 -0700849 }
850
Adam Cohen32a42f12010-08-11 19:34:30 -0700851 mHighlight.bringToFront();
Adam Cohen839f4a52010-08-26 17:36:48 -0700852 } else if (mSwipeGestureType == GESTURE_SLIDE_UP ) {
Adam Cohen44729e32010-07-22 16:00:07 -0700853 // Didn't swipe up far enough, snap back down
Adam Cohen839f4a52010-08-26 17:36:48 -0700854 int duration;
855 float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 1 : 0;
856 if (mStackMode == ITEMS_SLIDE_UP || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
857 duration = Math.round(mStackSlider.getDurationForNeutralPosition());
858 } else {
859 duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
860 }
Adam Cohen44729e32010-07-22 16:00:07 -0700861
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700862 StackSlider animationSlider = new StackSlider(mStackSlider);
Chet Haase2794eb32010-10-12 16:29:28 -0700863 PropertyValuesHolder snapBackY = PropertyValuesHolder.ofFloat("YProgress", finalYProgress);
864 PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
865 ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
Adam Cohen839f4a52010-08-26 17:36:48 -0700866 snapBackX, snapBackY);
Chet Haase2794eb32010-10-12 16:29:28 -0700867 pa.setDuration(duration);
Adam Cohen839f4a52010-08-26 17:36:48 -0700868 pa.setInterpolator(new LinearInterpolator());
869 pa.start();
Adam Cohen32a42f12010-08-11 19:34:30 -0700870 } else if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
Adam Cohen44729e32010-07-22 16:00:07 -0700871 // Didn't swipe down far enough, snap back up
Adam Cohen839f4a52010-08-26 17:36:48 -0700872 float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 0 : 1;
873 int duration;
874 if (mStackMode == ITEMS_SLIDE_DOWN || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
875 duration = Math.round(mStackSlider.getDurationForNeutralPosition());
876 } else {
877 duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
878 }
Adam Cohen3d07af02010-08-18 17:46:23 -0700879
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700880 StackSlider animationSlider = new StackSlider(mStackSlider);
Chet Haase2794eb32010-10-12 16:29:28 -0700881 PropertyValuesHolder snapBackY =
882 PropertyValuesHolder.ofFloat("YProgress",finalYProgress);
883 PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
884 ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
Adam Cohen839f4a52010-08-26 17:36:48 -0700885 snapBackX, snapBackY);
Chet Haase2794eb32010-10-12 16:29:28 -0700886 pa.setDuration(duration);
Adam Cohen839f4a52010-08-26 17:36:48 -0700887 pa.start();
Adam Cohen44729e32010-07-22 16:00:07 -0700888 }
889
890 mActivePointerId = INVALID_POINTER;
Adam Cohen44729e32010-07-22 16:00:07 -0700891 mSwipeGestureType = GESTURE_NONE;
Adam Cohen32a42f12010-08-11 19:34:30 -0700892 }
893
894 private class StackSlider {
895 View mView;
896 float mYProgress;
897 float mXProgress;
898
Adam Cohen3d07af02010-08-18 17:46:23 -0700899 static final int NORMAL_MODE = 0;
900 static final int BEGINNING_OF_STACK_MODE = 1;
901 static final int END_OF_STACK_MODE = 2;
902
903 int mMode = NORMAL_MODE;
904
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700905 public StackSlider() {
906 }
907
908 public StackSlider(StackSlider copy) {
909 mView = copy.mView;
910 mYProgress = copy.mYProgress;
911 mXProgress = copy.mXProgress;
Adam Cohen3d07af02010-08-18 17:46:23 -0700912 mMode = copy.mMode;
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700913 }
914
Adam Cohen32a42f12010-08-11 19:34:30 -0700915 private float cubic(float r) {
Adam Cohen839f4a52010-08-26 17:36:48 -0700916 return (float) (Math.pow(2 * r - 1, 3) + 1) / 2.0f;
Adam Cohen32a42f12010-08-11 19:34:30 -0700917 }
918
919 private float highlightAlphaInterpolator(float r) {
920 float pivot = 0.4f;
921 if (r < pivot) {
Adam Cohen839f4a52010-08-26 17:36:48 -0700922 return 0.85f * cubic(r / pivot);
Adam Cohen32a42f12010-08-11 19:34:30 -0700923 } else {
Adam Cohen839f4a52010-08-26 17:36:48 -0700924 return 0.85f * cubic(1 - (r - pivot) / (1 - pivot));
Adam Cohen32a42f12010-08-11 19:34:30 -0700925 }
926 }
927
928 private float viewAlphaInterpolator(float r) {
929 float pivot = 0.3f;
930 if (r > pivot) {
Adam Cohen839f4a52010-08-26 17:36:48 -0700931 return (r - pivot) / (1 - pivot);
Adam Cohen32a42f12010-08-11 19:34:30 -0700932 } else {
933 return 0;
934 }
935 }
936
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700937 private float rotationInterpolator(float r) {
938 float pivot = 0.2f;
939 if (r < pivot) {
940 return 0;
941 } else {
Adam Cohen839f4a52010-08-26 17:36:48 -0700942 return (r - pivot) / (1 - pivot);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700943 }
944 }
945
Adam Cohen32a42f12010-08-11 19:34:30 -0700946 void setView(View v) {
947 mView = v;
948 }
949
950 public void setYProgress(float r) {
951 // enforce r between 0 and 1
952 r = Math.min(1.0f, r);
953 r = Math.max(0, r);
954
955 mYProgress = r;
Adam Cohena02fdf12010-11-03 13:27:40 -0700956 if (mView == null) return;
957
Adam Cohen32a42f12010-08-11 19:34:30 -0700958 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
959 final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
960
Adam Cohen839f4a52010-08-26 17:36:48 -0700961 int stackDirection = (mStackMode == ITEMS_SLIDE_UP) ? 1 : -1;
962
Adam Cohen48867212011-01-09 13:49:40 -0800963 // We need to prevent any clipping issues which may arise by setting a layer type.
964 // This doesn't come for free however, so we only want to enable it when required.
965 if (Float.compare(0f, mYProgress) != 0 && Float.compare(1.0f, mYProgress) != 0) {
966 if (mView.getLayerType() == LAYER_TYPE_NONE) {
967 mView.setLayerType(LAYER_TYPE_HARDWARE, null);
968 }
969 } else {
970 if (mView.getLayerType() != LAYER_TYPE_NONE) {
971 mView.setLayerType(LAYER_TYPE_NONE, null);
972 }
973 }
974
Adam Cohen3d07af02010-08-18 17:46:23 -0700975 switch (mMode) {
976 case NORMAL_MODE:
Adam Cohendfcdddd2010-09-10 14:38:40 -0700977 viewLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
978 highlightLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
Adam Cohen3d07af02010-08-18 17:46:23 -0700979 mHighlight.setAlpha(highlightAlphaInterpolator(r));
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700980
Adam Cohen839f4a52010-08-26 17:36:48 -0700981 float alpha = viewAlphaInterpolator(1 - r);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700982
Adam Cohen3d07af02010-08-18 17:46:23 -0700983 // We make sure that views which can't be seen (have 0 alpha) are also invisible
984 // so that they don't interfere with click events.
985 if (mView.getAlpha() == 0 && alpha != 0 && mView.getVisibility() != VISIBLE) {
986 mView.setVisibility(VISIBLE);
987 } else if (alpha == 0 && mView.getAlpha() != 0
988 && mView.getVisibility() == VISIBLE) {
989 mView.setVisibility(INVISIBLE);
990 }
991
992 mView.setAlpha(alpha);
Adam Cohen839f4a52010-08-26 17:36:48 -0700993 mView.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
994 mHighlight.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
Adam Cohen3d07af02010-08-18 17:46:23 -0700995 break;
Adam Cohen96d8d562010-10-24 11:12:18 -0700996 case END_OF_STACK_MODE:
Adam Cohen839f4a52010-08-26 17:36:48 -0700997 r = r * 0.2f;
Adam Cohendfcdddd2010-09-10 14:38:40 -0700998 viewLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount));
999 highlightLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount));
Adam Cohen3d07af02010-08-18 17:46:23 -07001000 mHighlight.setAlpha(highlightAlphaInterpolator(r));
1001 break;
Adam Cohen96d8d562010-10-24 11:12:18 -07001002 case BEGINNING_OF_STACK_MODE:
Adam Cohen839f4a52010-08-26 17:36:48 -07001003 r = (1-r) * 0.2f;
Adam Cohendfcdddd2010-09-10 14:38:40 -07001004 viewLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount));
1005 highlightLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount));
Adam Cohen3d07af02010-08-18 17:46:23 -07001006 mHighlight.setAlpha(highlightAlphaInterpolator(r));
1007 break;
Adam Cohenb04f7ad2010-08-15 13:22:42 -07001008 }
Adam Cohen32a42f12010-08-11 19:34:30 -07001009 }
1010
1011 public void setXProgress(float r) {
1012 // enforce r between 0 and 1
Adam Cohen3d07af02010-08-18 17:46:23 -07001013 r = Math.min(2.0f, r);
1014 r = Math.max(-2.0f, r);
Adam Cohen32a42f12010-08-11 19:34:30 -07001015
1016 mXProgress = r;
1017
Adam Cohena02fdf12010-11-03 13:27:40 -07001018 if (mView == null) return;
Adam Cohen32a42f12010-08-11 19:34:30 -07001019 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
1020 final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
1021
Adam Cohen3d07af02010-08-18 17:46:23 -07001022 r *= 0.2f;
Adam Cohendfcdddd2010-09-10 14:38:40 -07001023 viewLp.setHorizontalOffset(Math.round(r * mSlideAmount));
1024 highlightLp.setHorizontalOffset(Math.round(r * mSlideAmount));
Adam Cohen32a42f12010-08-11 19:34:30 -07001025 }
1026
Adam Cohen3d07af02010-08-18 17:46:23 -07001027 void setMode(int mode) {
1028 mMode = mode;
1029 }
1030
1031 float getDurationForNeutralPosition() {
Adam Cohen839f4a52010-08-26 17:36:48 -07001032 return getDuration(false, 0);
Adam Cohen3d07af02010-08-18 17:46:23 -07001033 }
1034
1035 float getDurationForOffscreenPosition() {
Adam Cohen839f4a52010-08-26 17:36:48 -07001036 return getDuration(true, 0);
Adam Cohen3d07af02010-08-18 17:46:23 -07001037 }
1038
Adam Cohen839f4a52010-08-26 17:36:48 -07001039 float getDurationForNeutralPosition(float velocity) {
1040 return getDuration(false, velocity);
1041 }
1042
1043 float getDurationForOffscreenPosition(float velocity) {
1044 return getDuration(true, velocity);
1045 }
1046
1047 private float getDuration(boolean invert, float velocity) {
Adam Cohen3d07af02010-08-18 17:46:23 -07001048 if (mView != null) {
1049 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
1050
Neil Fuller33253a42014-10-01 11:55:10 +01001051 float d = (float) Math.hypot(viewLp.horizontalOffset, viewLp.verticalOffset);
1052 float maxd = (float) Math.hypot(mSlideAmount, 0.4f * mSlideAmount);
Tony Wickham400ef792016-11-07 10:18:55 -08001053 if (d > maxd) {
1054 // Because mSlideAmount is updated in onLayout(), it is possible that d > maxd
1055 // if we get onLayout() right before this method is called.
1056 d = maxd;
1057 }
Adam Cohen839f4a52010-08-26 17:36:48 -07001058
1059 if (velocity == 0) {
1060 return (invert ? (1 - d / maxd) : d / maxd) * DEFAULT_ANIMATION_DURATION;
1061 } else {
1062 float duration = invert ? d / Math.abs(velocity) :
1063 (maxd - d) / Math.abs(velocity);
1064 if (duration < MINIMUM_ANIMATION_DURATION ||
1065 duration > DEFAULT_ANIMATION_DURATION) {
1066 return getDuration(invert, 0);
1067 } else {
1068 return duration;
1069 }
1070 }
Adam Cohen3d07af02010-08-18 17:46:23 -07001071 }
1072 return 0;
1073 }
1074
Romain Guye5ebcb02010-10-15 13:57:28 -07001075 // Used for animations
1076 @SuppressWarnings({"UnusedDeclaration"})
Adam Cohen839f4a52010-08-26 17:36:48 -07001077 public float getYProgress() {
Adam Cohen32a42f12010-08-11 19:34:30 -07001078 return mYProgress;
1079 }
1080
Romain Guye5ebcb02010-10-15 13:57:28 -07001081 // Used for animations
1082 @SuppressWarnings({"UnusedDeclaration"})
Adam Cohen839f4a52010-08-26 17:36:48 -07001083 public float getXProgress() {
Adam Cohen32a42f12010-08-11 19:34:30 -07001084 return mXProgress;
1085 }
Adam Cohen44729e32010-07-22 16:00:07 -07001086 }
1087
Adam Cohen9b073942010-08-19 16:49:52 -07001088 LayoutParams createOrReuseLayoutParams(View v) {
1089 final ViewGroup.LayoutParams currentLp = v.getLayoutParams();
1090 if (currentLp instanceof LayoutParams) {
1091 LayoutParams lp = (LayoutParams) currentLp;
1092 lp.setHorizontalOffset(0);
1093 lp.setVerticalOffset(0);
Adam Cohen839f4a52010-08-26 17:36:48 -07001094 lp.width = 0;
1095 lp.width = 0;
Adam Cohen9b073942010-08-19 16:49:52 -07001096 return lp;
1097 }
1098 return new LayoutParams(v);
Adam Cohen32a42f12010-08-11 19:34:30 -07001099 }
1100
Adam Cohen9b073942010-08-19 16:49:52 -07001101 @Override
1102 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Adam Cohenef17dd42011-01-20 17:20:57 -08001103 checkForAndHandleDataChanged();
Adam Cohenb04f7ad2010-08-15 13:22:42 -07001104
Adam Cohen9b073942010-08-19 16:49:52 -07001105 final int childCount = getChildCount();
1106 for (int i = 0; i < childCount; i++) {
1107 final View child = getChildAt(i);
Adam Cohen32a42f12010-08-11 19:34:30 -07001108
Adam Cohen9b073942010-08-19 16:49:52 -07001109 int childRight = mPaddingLeft + child.getMeasuredWidth();
1110 int childBottom = mPaddingTop + child.getMeasuredHeight();
1111 LayoutParams lp = (LayoutParams) child.getLayoutParams();
Adam Cohen32a42f12010-08-11 19:34:30 -07001112
Adam Cohen9b073942010-08-19 16:49:52 -07001113 child.layout(mPaddingLeft + lp.horizontalOffset, mPaddingTop + lp.verticalOffset,
1114 childRight + lp.horizontalOffset, childBottom + lp.verticalOffset);
1115
Adam Cohen9b073942010-08-19 16:49:52 -07001116 }
Adam Cohen9b073942010-08-19 16:49:52 -07001117 onLayout();
Adam Cohen32a42f12010-08-11 19:34:30 -07001118 }
1119
Adam Cohen26e30bb2010-12-03 18:16:12 -08001120 @Override
1121 public void advance() {
1122 long timeSinceLastInteraction = System.currentTimeMillis() - mLastInteractionTime;
Adam Cohenc99ff732011-01-16 16:11:41 -08001123
1124 if (mAdapter == null) return;
Adam Cohenef17dd42011-01-20 17:20:57 -08001125 final int adapterCount = getCount();
Adam Cohenc99ff732011-01-16 16:11:41 -08001126 if (adapterCount == 1 && mLoopViews) return;
1127
Adam Cohen26e30bb2010-12-03 18:16:12 -08001128 if (mSwipeGestureType == GESTURE_NONE &&
1129 timeSinceLastInteraction > MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE) {
1130 showNext();
1131 }
1132 }
1133
Adam Cohen839f4a52010-08-26 17:36:48 -07001134 private void measureChildren() {
1135 final int count = getChildCount();
Adam Cohen36f43902010-12-15 21:17:33 -08001136
1137 final int measuredWidth = getMeasuredWidth();
1138 final int measuredHeight = getMeasuredHeight();
1139
1140 final int childWidth = Math.round(measuredWidth*(1-PERSPECTIVE_SHIFT_FACTOR_X))
Adam Cohen026e1212010-12-13 12:29:17 -08001141 - mPaddingLeft - mPaddingRight;
Adam Cohen36f43902010-12-15 21:17:33 -08001142 final int childHeight = Math.round(measuredHeight*(1-PERSPECTIVE_SHIFT_FACTOR_Y))
Adam Cohen839f4a52010-08-26 17:36:48 -07001143 - mPaddingTop - mPaddingBottom;
1144
Adam Cohen36f43902010-12-15 21:17:33 -08001145 int maxWidth = 0;
1146 int maxHeight = 0;
1147
Adam Cohen839f4a52010-08-26 17:36:48 -07001148 for (int i = 0; i < count; i++) {
1149 final View child = getChildAt(i);
Adam Cohen36f43902010-12-15 21:17:33 -08001150 child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST),
1151 MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST));
1152
1153 if (child != mHighlight && child != mClickFeedback) {
1154 final int childMeasuredWidth = child.getMeasuredWidth();
1155 final int childMeasuredHeight = child.getMeasuredHeight();
1156 if (childMeasuredWidth > maxWidth) {
1157 maxWidth = childMeasuredWidth;
1158 }
1159 if (childMeasuredHeight > maxHeight) {
1160 maxHeight = childMeasuredHeight;
1161 }
1162 }
1163 }
1164
1165 mNewPerspectiveShiftX = PERSPECTIVE_SHIFT_FACTOR_X * measuredWidth;
1166 mNewPerspectiveShiftY = PERSPECTIVE_SHIFT_FACTOR_Y * measuredHeight;
Adam Cohen53838d22011-01-26 21:32:33 -08001167
1168 // If we have extra space, we try and spread the items out
Adam Cohen78db1aa2011-01-25 12:24:23 -08001169 if (maxWidth > 0 && count > 0 && maxWidth < childWidth) {
Adam Cohen36f43902010-12-15 21:17:33 -08001170 mNewPerspectiveShiftX = measuredWidth - maxWidth;
1171 }
1172
Adam Cohen78db1aa2011-01-25 12:24:23 -08001173 if (maxHeight > 0 && count > 0 && maxHeight < childHeight) {
Adam Cohen36f43902010-12-15 21:17:33 -08001174 mNewPerspectiveShiftY = measuredHeight - maxHeight;
Adam Cohen839f4a52010-08-26 17:36:48 -07001175 }
1176 }
1177
1178 @Override
1179 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1180 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
1181 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
1182 final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
1183 final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
1184
1185 boolean haveChildRefSize = (mReferenceChildWidth != -1 && mReferenceChildHeight != -1);
1186
1187 // We need to deal with the case where our parent hasn't told us how
1188 // big we should be. In this case we should
Adam Cohen026e1212010-12-13 12:29:17 -08001189 float factorY = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_Y);
Adam Cohen839f4a52010-08-26 17:36:48 -07001190 if (heightSpecMode == MeasureSpec.UNSPECIFIED) {
1191 heightSpecSize = haveChildRefSize ?
Adam Cohen026e1212010-12-13 12:29:17 -08001192 Math.round(mReferenceChildHeight * (1 + factorY)) +
Adam Cohen839f4a52010-08-26 17:36:48 -07001193 mPaddingTop + mPaddingBottom : 0;
1194 } else if (heightSpecMode == MeasureSpec.AT_MOST) {
Dianne Hackborn189ee182010-12-02 21:48:53 -08001195 if (haveChildRefSize) {
Adam Cohen026e1212010-12-13 12:29:17 -08001196 int height = Math.round(mReferenceChildHeight * (1 + factorY))
Dianne Hackborn189ee182010-12-02 21:48:53 -08001197 + mPaddingTop + mPaddingBottom;
1198 if (height <= heightSpecSize) {
1199 heightSpecSize = height;
1200 } else {
1201 heightSpecSize |= MEASURED_STATE_TOO_SMALL;
Adam Cohenef17dd42011-01-20 17:20:57 -08001202
Dianne Hackborn189ee182010-12-02 21:48:53 -08001203 }
1204 } else {
1205 heightSpecSize = 0;
1206 }
Adam Cohen839f4a52010-08-26 17:36:48 -07001207 }
1208
Adam Cohen026e1212010-12-13 12:29:17 -08001209 float factorX = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_X);
Adam Cohen839f4a52010-08-26 17:36:48 -07001210 if (widthSpecMode == MeasureSpec.UNSPECIFIED) {
Adam Cohen026e1212010-12-13 12:29:17 -08001211 widthSpecSize = haveChildRefSize ?
1212 Math.round(mReferenceChildWidth * (1 + factorX)) +
1213 mPaddingLeft + mPaddingRight : 0;
Adam Cohen839f4a52010-08-26 17:36:48 -07001214 } else if (heightSpecMode == MeasureSpec.AT_MOST) {
Dianne Hackborn189ee182010-12-02 21:48:53 -08001215 if (haveChildRefSize) {
1216 int width = mReferenceChildWidth + mPaddingLeft + mPaddingRight;
1217 if (width <= widthSpecSize) {
1218 widthSpecSize = width;
1219 } else {
1220 widthSpecSize |= MEASURED_STATE_TOO_SMALL;
1221 }
1222 } else {
1223 widthSpecSize = 0;
1224 }
Adam Cohen839f4a52010-08-26 17:36:48 -07001225 }
Adam Cohen839f4a52010-08-26 17:36:48 -07001226 setMeasuredDimension(widthSpecSize, heightSpecSize);
1227 measureChildren();
1228 }
1229
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001230 @Override
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001231 public CharSequence getAccessibilityClassName() {
1232 return StackView.class.getName();
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001233 }
1234
Alan Viverettea54956a2015-01-07 16:05:02 -08001235 /** @hide */
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001236 @Override
Alan Viverettea54956a2015-01-07 16:05:02 -08001237 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
1238 super.onInitializeAccessibilityNodeInfoInternal(info);
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
Alan Viverettea54956a2015-01-07 16:05:02 -08001250 /** @hide */
Svetoslav Ganov48d15862012-05-15 10:10:00 -07001251 @Override
Alan Viverettea54956a2015-01-07 16:05:02 -08001252 public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
1253 if (super.performAccessibilityActionInternal(action, arguments)) {
Svetoslav Ganov48d15862012-05-15 10:10:00 -07001254 return true;
1255 }
Svetoslav Ganovfb1e80a2012-05-16 17:33:19 -07001256 if (!isEnabled()) {
1257 return false;
1258 }
Svetoslav Ganov48d15862012-05-15 10:10:00 -07001259 switch (action) {
1260 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
1261 if (getDisplayedChild() < getChildCount() - 1) {
1262 showNext();
1263 return true;
1264 }
1265 } return false;
1266 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
1267 if (getDisplayedChild() > 0) {
1268 showPrevious();
1269 return true;
1270 }
1271 } return false;
1272 }
1273 return false;
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001274 }
1275
Adam Cohen9b073942010-08-19 16:49:52 -07001276 class LayoutParams extends ViewGroup.LayoutParams {
1277 int horizontalOffset;
1278 int verticalOffset;
1279 View mView;
Adam Cohend51bbb52010-10-18 10:59:49 -07001280 private final Rect parentRect = new Rect();
1281 private final Rect invalidateRect = new Rect();
1282 private final RectF invalidateRectf = new RectF();
1283 private final Rect globalInvalidateRect = new Rect();
Adam Cohen32a42f12010-08-11 19:34:30 -07001284
Adam Cohen9b073942010-08-19 16:49:52 -07001285 LayoutParams(View view) {
1286 super(0, 0);
Adam Cohen839f4a52010-08-26 17:36:48 -07001287 width = 0;
1288 height = 0;
Adam Cohen9b073942010-08-19 16:49:52 -07001289 horizontalOffset = 0;
1290 verticalOffset = 0;
1291 mView = view;
1292 }
Adam Cohen32a42f12010-08-11 19:34:30 -07001293
Adam Cohen9b073942010-08-19 16:49:52 -07001294 LayoutParams(Context c, AttributeSet attrs) {
1295 super(c, attrs);
1296 horizontalOffset = 0;
1297 verticalOffset = 0;
Adam Cohen839f4a52010-08-26 17:36:48 -07001298 width = 0;
1299 height = 0;
Adam Cohen9b073942010-08-19 16:49:52 -07001300 }
Adam Cohen32a42f12010-08-11 19:34:30 -07001301
Adam Cohen9b073942010-08-19 16:49:52 -07001302 void invalidateGlobalRegion(View v, Rect r) {
Adam Cohend51bbb52010-10-18 10:59:49 -07001303 // We need to make a new rect here, so as not to modify the one passed
1304 globalInvalidateRect.set(r);
Adam Cohen1ff65d12011-02-16 14:04:09 -08001305 globalInvalidateRect.union(0, 0, getWidth(), getHeight());
Adam Cohen9b073942010-08-19 16:49:52 -07001306 View p = v;
1307 if (!(v.getParent() != null && v.getParent() instanceof View)) return;
1308
Adam Cohen9b073942010-08-19 16:49:52 -07001309 boolean firstPass = true;
1310 parentRect.set(0, 0, 0, 0);
Adam Cohenb7f4d032010-09-16 15:25:54 -07001311 while (p.getParent() != null && p.getParent() instanceof View
Adam Cohend51bbb52010-10-18 10:59:49 -07001312 && !parentRect.contains(globalInvalidateRect)) {
Adam Cohen9b073942010-08-19 16:49:52 -07001313 if (!firstPass) {
Adam Cohend51bbb52010-10-18 10:59:49 -07001314 globalInvalidateRect.offset(p.getLeft() - p.getScrollX(), p.getTop()
1315 - p.getScrollY());
Adam Cohen9b073942010-08-19 16:49:52 -07001316 }
1317 firstPass = false;
1318 p = (View) p.getParent();
Adam Cohenb7f4d032010-09-16 15:25:54 -07001319 parentRect.set(p.getScrollX(), p.getScrollY(),
Adam Cohen1ff65d12011-02-16 14:04:09 -08001320 p.getWidth() + p.getScrollX(), p.getHeight() + p.getScrollY());
1321 p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top,
1322 globalInvalidateRect.right, globalInvalidateRect.bottom);
Adam Cohen9b073942010-08-19 16:49:52 -07001323 }
1324
Adam Cohend51bbb52010-10-18 10:59:49 -07001325 p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top,
1326 globalInvalidateRect.right, globalInvalidateRect.bottom);
Adam Cohen9b073942010-08-19 16:49:52 -07001327 }
1328
Adam Cohend51bbb52010-10-18 10:59:49 -07001329 Rect getInvalidateRect() {
1330 return invalidateRect;
1331 }
1332
1333 void resetInvalidateRect() {
Adam Cohen0ac116b2010-11-11 11:39:53 -08001334 invalidateRect.set(0, 0, 0, 0);
Adam Cohend51bbb52010-10-18 10:59:49 -07001335 }
1336
Chet Haasea18a86b2010-09-07 13:20:00 -07001337 // This is public so that ObjectAnimator can access it
Adam Cohen9b073942010-08-19 16:49:52 -07001338 public void setVerticalOffset(int newVerticalOffset) {
Adam Cohen0ac116b2010-11-11 11:39:53 -08001339 setOffsets(horizontalOffset, newVerticalOffset);
1340 }
1341
1342 public void setHorizontalOffset(int newHorizontalOffset) {
1343 setOffsets(newHorizontalOffset, verticalOffset);
1344 }
1345
1346 public void setOffsets(int newHorizontalOffset, int newVerticalOffset) {
1347 int horizontalOffsetDelta = newHorizontalOffset - horizontalOffset;
1348 horizontalOffset = newHorizontalOffset;
1349 int verticalOffsetDelta = newVerticalOffset - verticalOffset;
Adam Cohen9b073942010-08-19 16:49:52 -07001350 verticalOffset = newVerticalOffset;
1351
1352 if (mView != null) {
1353 mView.requestLayout();
Adam Cohen0ac116b2010-11-11 11:39:53 -08001354 int left = Math.min(mView.getLeft() + horizontalOffsetDelta, mView.getLeft());
1355 int right = Math.max(mView.getRight() + horizontalOffsetDelta, mView.getRight());
1356 int top = Math.min(mView.getTop() + verticalOffsetDelta, mView.getTop());
1357 int bottom = Math.max(mView.getBottom() + verticalOffsetDelta, mView.getBottom());
Adam Cohen9b073942010-08-19 16:49:52 -07001358
Adam Cohen0ac116b2010-11-11 11:39:53 -08001359 invalidateRectf.set(left, top, right, bottom);
Adam Cohen9b073942010-08-19 16:49:52 -07001360
1361 float xoffset = -invalidateRectf.left;
1362 float yoffset = -invalidateRectf.top;
1363 invalidateRectf.offset(xoffset, yoffset);
1364 mView.getMatrix().mapRect(invalidateRectf);
1365 invalidateRectf.offset(-xoffset, -yoffset);
1366
Adam Cohen0ac116b2010-11-11 11:39:53 -08001367 invalidateRect.set((int) Math.floor(invalidateRectf.left),
Adam Cohen9b073942010-08-19 16:49:52 -07001368 (int) Math.floor(invalidateRectf.top),
1369 (int) Math.ceil(invalidateRectf.right),
1370 (int) Math.ceil(invalidateRectf.bottom));
1371
1372 invalidateGlobalRegion(mView, invalidateRect);
1373 }
1374 }
1375 }
1376
1377 private static class HolographicHelper {
1378 private final Paint mHolographicPaint = new Paint();
1379 private final Paint mErasePaint = new Paint();
Adam Cohen839f4a52010-08-26 17:36:48 -07001380 private final Paint mBlurPaint = new Paint();
Adam Cohen8baf5df2010-11-11 15:23:41 -08001381 private static final int RES_OUT = 0;
1382 private static final int CLICK_FEEDBACK = 1;
1383 private float mDensity;
Patrick Dubroye80202d2010-11-16 15:41:08 -08001384 private BlurMaskFilter mSmallBlurMaskFilter;
1385 private BlurMaskFilter mLargeBlurMaskFilter;
1386 private final Canvas mCanvas = new Canvas();
1387 private final Canvas mMaskCanvas = new Canvas();
1388 private final int[] mTmpXY = new int[2];
1389 private final Matrix mIdentityMatrix = new Matrix();
Adam Cohen9b073942010-08-19 16:49:52 -07001390
Adam Cohendfcdddd2010-09-10 14:38:40 -07001391 HolographicHelper(Context context) {
Adam Cohen8baf5df2010-11-11 15:23:41 -08001392 mDensity = context.getResources().getDisplayMetrics().density;
Adam Cohendfcdddd2010-09-10 14:38:40 -07001393
Adam Cohen9b073942010-08-19 16:49:52 -07001394 mHolographicPaint.setFilterBitmap(true);
Adam Cohen839f4a52010-08-26 17:36:48 -07001395 mHolographicPaint.setMaskFilter(TableMaskFilter.CreateClipTable(0, 30));
Adam Cohen9b073942010-08-19 16:49:52 -07001396 mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
1397 mErasePaint.setFilterBitmap(true);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001398
1399 mSmallBlurMaskFilter = new BlurMaskFilter(2 * mDensity, BlurMaskFilter.Blur.NORMAL);
1400 mLargeBlurMaskFilter = new BlurMaskFilter(4 * mDensity, BlurMaskFilter.Blur.NORMAL);
Adam Cohen9b073942010-08-19 16:49:52 -07001401 }
1402
Adam Cohen26f072c2011-04-01 16:23:18 -07001403 Bitmap createClickOutline(View v, int color) {
1404 return createOutline(v, CLICK_FEEDBACK, color);
Adam Cohen8baf5df2010-11-11 15:23:41 -08001405 }
1406
Adam Cohen26f072c2011-04-01 16:23:18 -07001407 Bitmap createResOutline(View v, int color) {
1408 return createOutline(v, RES_OUT, color);
1409 }
1410
1411 Bitmap createOutline(View v, int type, int color) {
1412 mHolographicPaint.setColor(color);
Adam Cohen8baf5df2010-11-11 15:23:41 -08001413 if (type == RES_OUT) {
Patrick Dubroye80202d2010-11-16 15:41:08 -08001414 mBlurPaint.setMaskFilter(mSmallBlurMaskFilter);
Adam Cohen8baf5df2010-11-11 15:23:41 -08001415 } else if (type == CLICK_FEEDBACK) {
Patrick Dubroye80202d2010-11-16 15:41:08 -08001416 mBlurPaint.setMaskFilter(mLargeBlurMaskFilter);
Adam Cohen8baf5df2010-11-11 15:23:41 -08001417 }
1418
Adam Cohen9b073942010-08-19 16:49:52 -07001419 if (v.getMeasuredWidth() == 0 || v.getMeasuredHeight() == 0) {
1420 return null;
1421 }
1422
Dianne Hackborndde331c2012-08-03 14:01:57 -07001423 Bitmap bitmap = Bitmap.createBitmap(v.getResources().getDisplayMetrics(),
1424 v.getMeasuredWidth(), v.getMeasuredHeight(), Bitmap.Config.ARGB_8888);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001425 mCanvas.setBitmap(bitmap);
Adam Cohen9b073942010-08-19 16:49:52 -07001426
1427 float rotationX = v.getRotationX();
Adam Cohen839f4a52010-08-26 17:36:48 -07001428 float rotation = v.getRotation();
1429 float translationY = v.getTranslationY();
Adam Cohen026e1212010-12-13 12:29:17 -08001430 float translationX = v.getTranslationX();
Adam Cohen9b073942010-08-19 16:49:52 -07001431 v.setRotationX(0);
Adam Cohen839f4a52010-08-26 17:36:48 -07001432 v.setRotation(0);
1433 v.setTranslationY(0);
Adam Cohen026e1212010-12-13 12:29:17 -08001434 v.setTranslationX(0);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001435 v.draw(mCanvas);
Adam Cohen9b073942010-08-19 16:49:52 -07001436 v.setRotationX(rotationX);
Adam Cohen839f4a52010-08-26 17:36:48 -07001437 v.setRotation(rotation);
1438 v.setTranslationY(translationY);
Adam Cohen026e1212010-12-13 12:29:17 -08001439 v.setTranslationX(translationX);
Adam Cohen9b073942010-08-19 16:49:52 -07001440
Patrick Dubroye80202d2010-11-16 15:41:08 -08001441 drawOutline(mCanvas, bitmap);
Dianne Hackborn6311d0a2011-08-02 16:37:58 -07001442 mCanvas.setBitmap(null);
Adam Cohen9b073942010-08-19 16:49:52 -07001443 return bitmap;
1444 }
1445
Adam Cohen9b073942010-08-19 16:49:52 -07001446 void drawOutline(Canvas dest, Bitmap src) {
Patrick Dubroye80202d2010-11-16 15:41:08 -08001447 final int[] xy = mTmpXY;
Adam Cohen839f4a52010-08-26 17:36:48 -07001448 Bitmap mask = src.extractAlpha(mBlurPaint, xy);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001449 mMaskCanvas.setBitmap(mask);
1450 mMaskCanvas.drawBitmap(src, -xy[0], -xy[1], mErasePaint);
Adam Cohen9b073942010-08-19 16:49:52 -07001451 dest.drawColor(0, PorterDuff.Mode.CLEAR);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001452 dest.setMatrix(mIdentityMatrix);
Adam Cohen839f4a52010-08-26 17:36:48 -07001453 dest.drawBitmap(mask, xy[0], xy[1], mHolographicPaint);
Dianne Hackborn6311d0a2011-08-02 16:37:58 -07001454 mMaskCanvas.setBitmap(null);
Adam Cohen9b073942010-08-19 16:49:52 -07001455 mask.recycle();
1456 }
Adam Cohen32a42f12010-08-11 19:34:30 -07001457 }
Adam Cohenef17dd42011-01-20 17:20:57 -08001458}