blob: ad35633168544b9100d43efea33a0472529e684d [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
yingleiw7b2e8882019-07-25 18:27:56 -070045import com.android.internal.R;
46
Aurimas Liutikas99441c52016-10-11 16:48:32 -070047import java.lang.ref.WeakReference;
48
Adam Cohen44729e32010-07-22 16:00:07 -070049@RemoteView
50/**
51 * A view that displays its children in a stack and allows users to discretely swipe
52 * through the children.
53 */
54public class StackView extends AdapterViewAnimator {
55 private final String TAG = "StackView";
56
57 /**
58 * Default animation parameters
59 */
Romain Guye5ebcb02010-10-15 13:57:28 -070060 private static final int DEFAULT_ANIMATION_DURATION = 400;
61 private static final int MINIMUM_ANIMATION_DURATION = 50;
Adam Cohenc0b53be2010-12-17 19:23:41 -080062 private static final int STACK_RELAYOUT_DURATION = 100;
Adam Cohen44729e32010-07-22 16:00:07 -070063
64 /**
Adam Cohen839f4a52010-08-26 17:36:48 -070065 * Parameters effecting the perspective visuals
66 */
Adam Cohen026e1212010-12-13 12:29:17 -080067 private static final float PERSPECTIVE_SHIFT_FACTOR_Y = 0.1f;
68 private static final float PERSPECTIVE_SHIFT_FACTOR_X = 0.1f;
69
Adam Cohen36f43902010-12-15 21:17:33 -080070 private float mPerspectiveShiftX;
71 private float mPerspectiveShiftY;
72 private float mNewPerspectiveShiftX;
73 private float mNewPerspectiveShiftY;
74
Romain Guye5ebcb02010-10-15 13:57:28 -070075 @SuppressWarnings({"FieldCanBeLocal"})
Adam Cohen78db1aa2011-01-25 12:24:23 -080076 private static final float PERSPECTIVE_SCALE_FACTOR = 0f;
Adam Cohen839f4a52010-08-26 17:36:48 -070077
78 /**
79 * Represent the two possible stack modes, one where items slide up, and the other
80 * where items slide down. The perspective is also inverted between these two modes.
81 */
82 private static final int ITEMS_SLIDE_UP = 0;
83 private static final int ITEMS_SLIDE_DOWN = 1;
84
85 /**
Adam Cohen44729e32010-07-22 16:00:07 -070086 * These specify the different gesture states
87 */
Romain Guy5b53f912010-08-16 18:24:33 -070088 private static final int GESTURE_NONE = 0;
89 private static final int GESTURE_SLIDE_UP = 1;
90 private static final int GESTURE_SLIDE_DOWN = 2;
Adam Cohen44729e32010-07-22 16:00:07 -070091
92 /**
93 * Specifies how far you need to swipe (up or down) before it
94 * will be consider a completed gesture when you lift your finger
95 */
Adam Cohena9238c82010-10-25 14:01:29 -070096 private static final float SWIPE_THRESHOLD_RATIO = 0.2f;
97
98 /**
99 * Specifies the total distance, relative to the size of the stack,
100 * that views will be slid, either up or down
101 */
Romain Guy5b53f912010-08-16 18:24:33 -0700102 private static final float SLIDE_UP_RATIO = 0.7f;
Adam Cohen44729e32010-07-22 16:00:07 -0700103
Adam Cohen44729e32010-07-22 16:00:07 -0700104 /**
105 * Sentinel value for no current active pointer.
106 * Used by {@link #mActivePointerId}.
107 */
108 private static final int INVALID_POINTER = -1;
109
110 /**
Adam Cohen839f4a52010-08-26 17:36:48 -0700111 * Number of active views in the stack. One fewer view is actually visible, as one is hidden.
112 */
113 private static final int NUM_ACTIVE_VIEWS = 5;
114
Adam Cohendfcdddd2010-09-10 14:38:40 -0700115 private static final int FRAME_PADDING = 4;
Adam Cohen839f4a52010-08-26 17:36:48 -0700116
Patrick Dubroye80202d2010-11-16 15:41:08 -0800117 private final Rect mTouchRect = new Rect();
118
Adam Cohen26e30bb2010-12-03 18:16:12 -0800119 private static final int MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE = 5000;
120
Romain Guy4725a902011-05-25 10:54:29 -0700121 private static final long MIN_TIME_BETWEEN_SCROLLS = 100;
Adam Cohena8a7c922011-02-28 17:20:44 -0800122
Adam Cohen839f4a52010-08-26 17:36:48 -0700123 /**
Adam Cohen44729e32010-07-22 16:00:07 -0700124 * These variables are all related to the current state of touch interaction
125 * with the stack
126 */
Adam Cohen44729e32010-07-22 16:00:07 -0700127 private float mInitialY;
128 private float mInitialX;
129 private int mActivePointerId;
Adam Cohen44729e32010-07-22 16:00:07 -0700130 private int mYVelocity = 0;
131 private int mSwipeGestureType = GESTURE_NONE;
Adam Cohendfcdddd2010-09-10 14:38:40 -0700132 private int mSlideAmount;
Adam Cohen44729e32010-07-22 16:00:07 -0700133 private int mSwipeThreshold;
134 private int mTouchSlop;
135 private int mMaximumVelocity;
136 private VelocityTracker mVelocityTracker;
Adam Cohen3352b682010-10-26 13:54:00 -0700137 private boolean mTransitionIsSetup = false;
Adam Cohen26f072c2011-04-01 16:23:18 -0700138 private int mResOutColor;
139 private int mClickColor;
Adam Cohen44729e32010-07-22 16:00:07 -0700140
Adam Cohen9b073942010-08-19 16:49:52 -0700141 private static HolographicHelper sHolographicHelper;
Adam Cohen32a42f12010-08-11 19:34:30 -0700142 private ImageView mHighlight;
Adam Cohen8baf5df2010-11-11 15:23:41 -0800143 private ImageView mClickFeedback;
144 private boolean mClickFeedbackIsValid = false;
Adam Cohen32a42f12010-08-11 19:34:30 -0700145 private StackSlider mStackSlider;
Adam Cohen44729e32010-07-22 16:00:07 -0700146 private boolean mFirstLayoutHappened = false;
Adam Cohen26e30bb2010-12-03 18:16:12 -0800147 private long mLastInteractionTime = 0;
Adam Cohena8a7c922011-02-28 17:20:44 -0800148 private long mLastScrollTime;
Adam Cohen839f4a52010-08-26 17:36:48 -0700149 private int mStackMode;
Adam Cohendfcdddd2010-09-10 14:38:40 -0700150 private int mFramePadding;
Adam Cohen0ac116b2010-11-11 11:39:53 -0800151 private final Rect stackInvalidateRect = new Rect();
Adam Cohen44729e32010-07-22 16:00:07 -0700152
Winson Chungabc8b50a2011-09-06 18:38:50 -0700153 /**
154 * {@inheritDoc}
155 */
Adam Cohen44729e32010-07-22 16:00:07 -0700156 public StackView(Context context) {
Adam Cohen26f072c2011-04-01 16:23:18 -0700157 this(context, null);
Adam Cohen44729e32010-07-22 16:00:07 -0700158 }
159
Winson Chungabc8b50a2011-09-06 18:38:50 -0700160 /**
161 * {@inheritDoc}
162 */
Adam Cohen44729e32010-07-22 16:00:07 -0700163 public StackView(Context context, AttributeSet attrs) {
Adam Cohen26f072c2011-04-01 16:23:18 -0700164 this(context, attrs, com.android.internal.R.attr.stackViewStyle);
165 }
166
Winson Chungabc8b50a2011-09-06 18:38:50 -0700167 /**
168 * {@inheritDoc}
169 */
Adam Cohen26f072c2011-04-01 16:23:18 -0700170 public StackView(Context context, AttributeSet attrs, int defStyleAttr) {
Alan Viverette617feb92013-09-09 18:09:13 -0700171 this(context, attrs, defStyleAttr, 0);
172 }
173
174 /**
175 * {@inheritDoc}
176 */
177 public StackView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
178 super(context, attrs, defStyleAttr, defStyleRes);
179 final TypedArray a = context.obtainStyledAttributes(
180 attrs, com.android.internal.R.styleable.StackView, defStyleAttr, defStyleRes);
Aurimas Liutikasab324cf2019-02-07 16:46:38 -0800181 saveAttributeDataForStyleable(context, com.android.internal.R.styleable.StackView,
182 attrs, a, defStyleAttr, defStyleRes);
Adam Cohen26f072c2011-04-01 16:23:18 -0700183
184 mResOutColor = a.getColor(
185 com.android.internal.R.styleable.StackView_resOutColor, 0);
186 mClickColor = a.getColor(
187 com.android.internal.R.styleable.StackView_clickColor, 0);
188
189 a.recycle();
Adam Cohen44729e32010-07-22 16:00:07 -0700190 initStackView();
191 }
192
193 private void initStackView() {
Adam Cohen96d8d562010-10-24 11:12:18 -0700194 configureViewAnimator(NUM_ACTIVE_VIEWS, 1);
Adam Cohen44729e32010-07-22 16:00:07 -0700195 setStaticTransformationsEnabled(true);
196 final ViewConfiguration configuration = ViewConfiguration.get(getContext());
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700197 mTouchSlop = configuration.getScaledTouchSlop();
Adam Cohen44729e32010-07-22 16:00:07 -0700198 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
199 mActivePointerId = INVALID_POINTER;
Adam Cohen32a42f12010-08-11 19:34:30 -0700200
201 mHighlight = new ImageView(getContext());
202 mHighlight.setLayoutParams(new LayoutParams(mHighlight));
203 addViewInLayout(mHighlight, -1, new LayoutParams(mHighlight));
Adam Cohen8baf5df2010-11-11 15:23:41 -0800204
205 mClickFeedback = new ImageView(getContext());
206 mClickFeedback.setLayoutParams(new LayoutParams(mClickFeedback));
207 addViewInLayout(mClickFeedback, -1, new LayoutParams(mClickFeedback));
208 mClickFeedback.setVisibility(INVISIBLE);
209
Adam Cohen32a42f12010-08-11 19:34:30 -0700210 mStackSlider = new StackSlider();
211
Adam Cohen9b073942010-08-19 16:49:52 -0700212 if (sHolographicHelper == null) {
Adam Cohendfcdddd2010-09-10 14:38:40 -0700213 sHolographicHelper = new HolographicHelper(mContext);
Adam Cohen32a42f12010-08-11 19:34:30 -0700214 }
Adam Cohen9b073942010-08-19 16:49:52 -0700215 setClipChildren(false);
216 setClipToPadding(false);
Adam Cohen1480fdd2010-08-25 17:24:53 -0700217
Adam Cohen839f4a52010-08-26 17:36:48 -0700218 // This sets the form of the StackView, which is currently to have the perspective-shifted
219 // views above the active view, and have items slide down when sliding out. The opposite is
220 // available by using ITEMS_SLIDE_UP.
221 mStackMode = ITEMS_SLIDE_DOWN;
222
Adam Cohen1480fdd2010-08-25 17:24:53 -0700223 // This is a flag to indicate the the stack is loading for the first time
224 mWhichChild = -1;
Adam Cohendfcdddd2010-09-10 14:38:40 -0700225
226 // Adjust the frame padding based on the density, since the highlight changes based
227 // on the density
228 final float density = mContext.getResources().getDisplayMetrics().density;
229 mFramePadding = (int) Math.ceil(density * FRAME_PADDING);
Adam Cohen44729e32010-07-22 16:00:07 -0700230 }
231
232 /**
233 * Animate the views between different relative indexes within the {@link AdapterViewAnimator}
234 */
Adam Cohen78db1aa2011-01-25 12:24:23 -0800235 void transformViewForTransition(int fromIndex, int toIndex, final View view, boolean animate) {
Adam Cohen78db1aa2011-01-25 12:24:23 -0800236 if (!animate) {
237 ((StackFrame) view).cancelSliderAnimator();
238 view.setRotationX(0f);
239 LayoutParams lp = (LayoutParams) view.getLayoutParams();
240 lp.setVerticalOffset(0);
241 lp.setHorizontalOffset(0);
Adam Cohen69d66e02011-01-12 14:39:13 -0800242 }
243
Adam Cohen50204582011-01-16 17:28:25 -0800244 if (fromIndex == -1 && toIndex == getNumActiveViews() -1) {
Adam Cohen50204582011-01-16 17:28:25 -0800245 transformViewAtIndex(toIndex, view, false);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700246 view.setVisibility(VISIBLE);
Adam Cohenc798b662011-10-21 12:07:38 -0700247 view.setAlpha(1.0f);
Adam Cohen96d8d562010-10-24 11:12:18 -0700248 } else if (fromIndex == 0 && toIndex == 1) {
Adam Cohen44729e32010-07-22 16:00:07 -0700249 // Slide item in
Adam Cohen78db1aa2011-01-25 12:24:23 -0800250 ((StackFrame) view).cancelSliderAnimator();
Adam Cohen44729e32010-07-22 16:00:07 -0700251 view.setVisibility(VISIBLE);
Adam Cohen32a42f12010-08-11 19:34:30 -0700252
Adam Cohen839f4a52010-08-26 17:36:48 -0700253 int duration = Math.round(mStackSlider.getDurationForNeutralPosition(mYVelocity));
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700254 StackSlider animationSlider = new StackSlider(mStackSlider);
Adam Cohen69d66e02011-01-12 14:39:13 -0800255 animationSlider.setView(view);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800256
257 if (animate) {
258 PropertyValuesHolder slideInY = PropertyValuesHolder.ofFloat("YProgress", 0.0f);
259 PropertyValuesHolder slideInX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
260 ObjectAnimator slideIn = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
261 slideInX, slideInY);
262 slideIn.setDuration(duration);
263 slideIn.setInterpolator(new LinearInterpolator());
264 ((StackFrame) view).setSliderAnimator(slideIn);
265 slideIn.start();
266 } else {
267 animationSlider.setYProgress(0f);
268 animationSlider.setXProgress(0f);
269 }
Adam Cohen96d8d562010-10-24 11:12:18 -0700270 } else if (fromIndex == 1 && toIndex == 0) {
Adam Cohen44729e32010-07-22 16:00:07 -0700271 // Slide item out
Adam Cohen78db1aa2011-01-25 12:24:23 -0800272 ((StackFrame) view).cancelSliderAnimator();
Adam Cohen839f4a52010-08-26 17:36:48 -0700273 int duration = Math.round(mStackSlider.getDurationForOffscreenPosition(mYVelocity));
Adam Cohen44729e32010-07-22 16:00:07 -0700274
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700275 StackSlider animationSlider = new StackSlider(mStackSlider);
Adam Cohen69d66e02011-01-12 14:39:13 -0800276 animationSlider.setView(view);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800277 if (animate) {
278 PropertyValuesHolder slideOutY = PropertyValuesHolder.ofFloat("YProgress", 1.0f);
279 PropertyValuesHolder slideOutX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
280 ObjectAnimator slideOut = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
281 slideOutX, slideOutY);
282 slideOut.setDuration(duration);
283 slideOut.setInterpolator(new LinearInterpolator());
284 ((StackFrame) view).setSliderAnimator(slideOut);
285 slideOut.start();
286 } else {
287 animationSlider.setYProgress(1.0f);
288 animationSlider.setXProgress(0f);
289 }
Adam Cohen69d66e02011-01-12 14:39:13 -0800290 } else if (toIndex == 0) {
Adam Cohen44729e32010-07-22 16:00:07 -0700291 // Make sure this view that is "waiting in the wings" is invisible
292 view.setAlpha(0.0f);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700293 view.setVisibility(INVISIBLE);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800294 } else if ((fromIndex == 0 || fromIndex == 1) && toIndex > 1) {
Adam Cohen69d66e02011-01-12 14:39:13 -0800295 view.setVisibility(VISIBLE);
296 view.setAlpha(1.0f);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800297 view.setRotationX(0f);
298 LayoutParams lp = (LayoutParams) view.getLayoutParams();
299 lp.setVerticalOffset(0);
300 lp.setHorizontalOffset(0);
Adam Cohenc0b53be2010-12-17 19:23:41 -0800301 } else if (fromIndex == -1) {
302 view.setAlpha(1.0f);
303 view.setVisibility(VISIBLE);
Adam Cohen44729e32010-07-22 16:00:07 -0700304 } else if (toIndex == -1) {
Adam Cohen78db1aa2011-01-25 12:24:23 -0800305 if (animate) {
Adam Cohenc798b662011-10-21 12:07:38 -0700306 postDelayed(new Runnable() {
307 public void run() {
308 view.setAlpha(0);
309 }
310 }, STACK_RELAYOUT_DURATION);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800311 } else {
312 view.setAlpha(0f);
313 }
Adam Cohen44729e32010-07-22 16:00:07 -0700314 }
Adam Cohen839f4a52010-08-26 17:36:48 -0700315
316 // Implement the faked perspective
317 if (toIndex != -1) {
Adam Cohen78db1aa2011-01-25 12:24:23 -0800318 transformViewAtIndex(toIndex, view, animate);
Adam Cohenf04e2252010-09-09 18:41:20 -0700319 }
320 }
Adam Cohen839f4a52010-08-26 17:36:48 -0700321
Adam Cohen36f43902010-12-15 21:17:33 -0800322 private void transformViewAtIndex(int index, final View view, boolean animate) {
323 final float maxPerspectiveShiftY = mPerspectiveShiftY;
324 final float maxPerspectiveShiftX = mPerspectiveShiftX;
Adam Cohen026e1212010-12-13 12:29:17 -0800325
Adam Cohenc99ff732011-01-16 16:11:41 -0800326 if (mStackMode == ITEMS_SLIDE_DOWN) {
327 index = mMaxNumActiveViews - index - 1;
328 if (index == mMaxNumActiveViews - 1) index--;
329 } else {
330 index--;
331 if (index < 0) index++;
332 }
Adam Cohen026e1212010-12-13 12:29:17 -0800333
334 float r = (index * 1.0f) / (mMaxNumActiveViews - 2);
335
Adam Cohen36f43902010-12-15 21:17:33 -0800336 final float scale = 1 - PERSPECTIVE_SCALE_FACTOR * (1 - r);
Adam Cohen026e1212010-12-13 12:29:17 -0800337
Adam Cohenc99ff732011-01-16 16:11:41 -0800338 float perspectiveTranslationY = r * maxPerspectiveShiftY;
339 float scaleShiftCorrectionY = (scale - 1) *
Adam Cohen026e1212010-12-13 12:29:17 -0800340 (getMeasuredHeight() * (1 - PERSPECTIVE_SHIFT_FACTOR_Y) / 2.0f);
Adam Cohen36f43902010-12-15 21:17:33 -0800341 final float transY = perspectiveTranslationY + scaleShiftCorrectionY;
Adam Cohen026e1212010-12-13 12:29:17 -0800342
343 float perspectiveTranslationX = (1 - r) * maxPerspectiveShiftX;
344 float scaleShiftCorrectionX = (1 - scale) *
345 (getMeasuredWidth() * (1 - PERSPECTIVE_SHIFT_FACTOR_X) / 2.0f);
Adam Cohen36f43902010-12-15 21:17:33 -0800346 final float transX = perspectiveTranslationX + scaleShiftCorrectionX;
Adam Cohen026e1212010-12-13 12:29:17 -0800347
Adam Cohenc99ff732011-01-16 16:11:41 -0800348 // If this view is currently being animated for a certain position, we need to cancel
Adam Cohen69d66e02011-01-12 14:39:13 -0800349 // this animation so as not to interfere with the new transformation.
Adam Cohen78db1aa2011-01-25 12:24:23 -0800350 if (view instanceof StackFrame) {
351 ((StackFrame) view).cancelTransformAnimator();
Adam Cohen69d66e02011-01-12 14:39:13 -0800352 }
353
Adam Cohen36f43902010-12-15 21:17:33 -0800354 if (animate) {
355 PropertyValuesHolder translationX = PropertyValuesHolder.ofFloat("translationX", transX);
356 PropertyValuesHolder translationY = PropertyValuesHolder.ofFloat("translationY", transY);
357 PropertyValuesHolder scalePropX = PropertyValuesHolder.ofFloat("scaleX", scale);
358 PropertyValuesHolder scalePropY = PropertyValuesHolder.ofFloat("scaleY", scale);
Adam Cohen026e1212010-12-13 12:29:17 -0800359
Adam Cohen36f43902010-12-15 21:17:33 -0800360 ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(view, scalePropX, scalePropY,
361 translationY, translationX);
Adam Cohenc0b53be2010-12-17 19:23:41 -0800362 oa.setDuration(STACK_RELAYOUT_DURATION);
Adam Cohen78db1aa2011-01-25 12:24:23 -0800363 if (view instanceof StackFrame) {
364 ((StackFrame) view).setTransformAnimator(oa);
365 }
Adam Cohen36f43902010-12-15 21:17:33 -0800366 oa.start();
367 } else {
Adam Cohen36f43902010-12-15 21:17:33 -0800368 view.setTranslationX(transX);
369 view.setTranslationY(transY);
370 view.setScaleX(scale);
371 view.setScaleY(scale);
372 }
Adam Cohen026e1212010-12-13 12:29:17 -0800373 }
374
Adam Cohen3352b682010-10-26 13:54:00 -0700375 private void setupStackSlider(View v, int mode) {
376 mStackSlider.setMode(mode);
377 if (v != null) {
Adam Cohen26f072c2011-04-01 16:23:18 -0700378 mHighlight.setImageBitmap(sHolographicHelper.createResOutline(v, mResOutColor));
Adam Cohen3352b682010-10-26 13:54:00 -0700379 mHighlight.setRotation(v.getRotation());
380 mHighlight.setTranslationY(v.getTranslationY());
Adam Cohen026e1212010-12-13 12:29:17 -0800381 mHighlight.setTranslationX(v.getTranslationX());
Adam Cohen3352b682010-10-26 13:54:00 -0700382 mHighlight.bringToFront();
383 v.bringToFront();
384 mStackSlider.setView(v);
385
386 v.setVisibility(VISIBLE);
387 }
388 }
389
Adam Cohen0e2de6d2011-01-19 17:16:34 -0800390 /**
391 * {@inheritDoc}
392 */
Adam Cohen3352b682010-10-26 13:54:00 -0700393 @Override
394 @android.view.RemotableViewMethod
395 public void showNext() {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800396 if (mSwipeGestureType != GESTURE_NONE) return;
Adam Cohen3352b682010-10-26 13:54:00 -0700397 if (!mTransitionIsSetup) {
398 View v = getViewAtRelativeIndex(1);
399 if (v != null) {
400 setupStackSlider(v, StackSlider.NORMAL_MODE);
401 mStackSlider.setYProgress(0);
402 mStackSlider.setXProgress(0);
403 }
404 }
405 super.showNext();
406 }
407
Adam Cohen0e2de6d2011-01-19 17:16:34 -0800408 /**
409 * {@inheritDoc}
410 */
Adam Cohen3352b682010-10-26 13:54:00 -0700411 @Override
412 @android.view.RemotableViewMethod
413 public void showPrevious() {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800414 if (mSwipeGestureType != GESTURE_NONE) return;
Adam Cohen3352b682010-10-26 13:54:00 -0700415 if (!mTransitionIsSetup) {
416 View v = getViewAtRelativeIndex(0);
417 if (v != null) {
418 setupStackSlider(v, StackSlider.NORMAL_MODE);
419 mStackSlider.setYProgress(1);
420 mStackSlider.setXProgress(0);
421 }
422 }
423 super.showPrevious();
424 }
425
Adam Cohen96d8d562010-10-24 11:12:18 -0700426 @Override
Adam Cohenef17dd42011-01-20 17:20:57 -0800427 void showOnly(int childIndex, boolean animate) {
428 super.showOnly(childIndex, animate);
Adam Cohen96d8d562010-10-24 11:12:18 -0700429
430 // Here we need to make sure that the z-order of the children is correct
Adam Cohen3352b682010-10-26 13:54:00 -0700431 for (int i = mCurrentWindowEnd; i >= mCurrentWindowStart; i--) {
Adam Cohen96d8d562010-10-24 11:12:18 -0700432 int index = modulo(i, getWindowSize());
Adam Cohend38a0ce2011-04-06 13:20:42 -0700433 ViewAndMetaData vm = mViewsMap.get(index);
434 if (vm != null) {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800435 View v = mViewsMap.get(index).view;
436 if (v != null) v.bringToFront();
437 }
Adam Cohen96d8d562010-10-24 11:12:18 -0700438 }
Adam Cohen78db1aa2011-01-25 12:24:23 -0800439 if (mHighlight != null) {
440 mHighlight.bringToFront();
441 }
Adam Cohen3352b682010-10-26 13:54:00 -0700442 mTransitionIsSetup = false;
Adam Cohen8baf5df2010-11-11 15:23:41 -0800443 mClickFeedbackIsValid = false;
444 }
445
446 void updateClickFeedback() {
447 if (!mClickFeedbackIsValid) {
Adam Cohen9c295482010-11-18 15:19:48 -0800448 View v = getViewAtRelativeIndex(1);
Adam Cohen8baf5df2010-11-11 15:23:41 -0800449 if (v != null) {
Adam Cohen26f072c2011-04-01 16:23:18 -0700450 mClickFeedback.setImageBitmap(
451 sHolographicHelper.createClickOutline(v, mClickColor));
Adam Cohen8baf5df2010-11-11 15:23:41 -0800452 mClickFeedback.setTranslationX(v.getTranslationX());
453 mClickFeedback.setTranslationY(v.getTranslationY());
454 }
455 mClickFeedbackIsValid = true;
456 }
457 }
458
459 @Override
460 void showTapFeedback(View v) {
461 updateClickFeedback();
462 mClickFeedback.setVisibility(VISIBLE);
463 mClickFeedback.bringToFront();
464 invalidate();
465 }
466
467 @Override
468 void hideTapFeedback(View v) {
469 mClickFeedback.setVisibility(INVISIBLE);
470 invalidate();
Adam Cohen96d8d562010-10-24 11:12:18 -0700471 }
472
Adam Cohenf04e2252010-09-09 18:41:20 -0700473 private void updateChildTransforms() {
Adam Cohen96d8d562010-10-24 11:12:18 -0700474 for (int i = 0; i < getNumActiveViews(); i++) {
Adam Cohenf04e2252010-09-09 18:41:20 -0700475 View v = getViewAtRelativeIndex(i);
476 if (v != null) {
Adam Cohen36f43902010-12-15 21:17:33 -0800477 transformViewAtIndex(i, v, false);
Adam Cohenf04e2252010-09-09 18:41:20 -0700478 }
Adam Cohen839f4a52010-08-26 17:36:48 -0700479 }
Adam Cohen44729e32010-07-22 16:00:07 -0700480 }
481
Adam Cohen78db1aa2011-01-25 12:24:23 -0800482 private static class StackFrame extends FrameLayout {
Adam Cohen78db1aa2011-01-25 12:24:23 -0800483 WeakReference<ObjectAnimator> transformAnimator;
484 WeakReference<ObjectAnimator> sliderAnimator;
485
486 public StackFrame(Context context) {
487 super(context);
488 }
489
Adam Cohen78db1aa2011-01-25 12:24:23 -0800490 void setTransformAnimator(ObjectAnimator oa) {
491 transformAnimator = new WeakReference<ObjectAnimator>(oa);
492 }
493
494 void setSliderAnimator(ObjectAnimator oa) {
495 sliderAnimator = new WeakReference<ObjectAnimator>(oa);
496 }
497
Adam Cohen78db1aa2011-01-25 12:24:23 -0800498 boolean cancelTransformAnimator() {
499 if (transformAnimator != null) {
500 ObjectAnimator oa = transformAnimator.get();
501 if (oa != null) {
502 oa.cancel();
503 return true;
504 }
505 }
506 return false;
507 }
508
509 boolean cancelSliderAnimator() {
510 if (sliderAnimator != null) {
511 ObjectAnimator oa = sliderAnimator.get();
512 if (oa != null) {
513 oa.cancel();
514 return true;
515 }
516 }
517 return false;
518 }
519 }
520
Adam Cohendfcdddd2010-09-10 14:38:40 -0700521 @Override
522 FrameLayout getFrameForChild() {
Adam Cohen78db1aa2011-01-25 12:24:23 -0800523 StackFrame fl = new StackFrame(mContext);
Adam Cohendfcdddd2010-09-10 14:38:40 -0700524 fl.setPadding(mFramePadding, mFramePadding, mFramePadding, mFramePadding);
525 return fl;
526 }
527
Adam Cohen44729e32010-07-22 16:00:07 -0700528 /**
529 * Apply any necessary tranforms for the child that is being added.
530 */
531 void applyTransformForChildAtIndex(View child, int relativeIndex) {
Adam Cohen44729e32010-07-22 16:00:07 -0700532 }
533
534 @Override
Adam Cohen9b073942010-08-19 16:49:52 -0700535 protected void dispatchDraw(Canvas canvas) {
Adam Cohen321aa2b2011-03-08 13:33:29 -0800536 boolean expandClipRegion = false;
537
Adam Cohen0ac116b2010-11-11 11:39:53 -0800538 canvas.getClipBounds(stackInvalidateRect);
Adam Cohend51bbb52010-10-18 10:59:49 -0700539 final int childCount = getChildCount();
540 for (int i = 0; i < childCount; i++) {
Adam Cohene86ff4d2011-01-21 17:46:11 -0800541 final View child = getChildAt(i);
542 LayoutParams lp = (LayoutParams) child.getLayoutParams();
543 if ((lp.horizontalOffset == 0 && lp.verticalOffset == 0) ||
544 child.getAlpha() == 0f || child.getVisibility() != VISIBLE) {
545 lp.resetInvalidateRect();
546 }
Adam Cohen321aa2b2011-03-08 13:33:29 -0800547 Rect childInvalidateRect = lp.getInvalidateRect();
548 if (!childInvalidateRect.isEmpty()) {
549 expandClipRegion = true;
550 stackInvalidateRect.union(childInvalidateRect);
551 }
Adam Cohen44729e32010-07-22 16:00:07 -0700552 }
Adam Cohen321aa2b2011-03-08 13:33:29 -0800553
554 // We only expand the clip bounds if necessary.
555 if (expandClipRegion) {
Derek Sollenberger2ad19e52018-04-18 15:03:09 -0400556 canvas.save();
557 canvas.clipRectUnion(stackInvalidateRect);
Adam Cohen321aa2b2011-03-08 13:33:29 -0800558 super.dispatchDraw(canvas);
559 canvas.restore();
560 } else {
561 super.dispatchDraw(canvas);
562 }
Adam Cohen9b073942010-08-19 16:49:52 -0700563 }
Adam Cohen44729e32010-07-22 16:00:07 -0700564
Adam Cohen9b073942010-08-19 16:49:52 -0700565 private void onLayout() {
Adam Cohen44729e32010-07-22 16:00:07 -0700566 if (!mFirstLayoutHappened) {
Adam Cohen44729e32010-07-22 16:00:07 -0700567 mFirstLayoutHappened = true;
Adam Cohen53838d22011-01-26 21:32:33 -0800568 updateChildTransforms();
Adam Cohen44729e32010-07-22 16:00:07 -0700569 }
Adam Cohen36f43902010-12-15 21:17:33 -0800570
Adam Cohenc6a47162011-01-27 18:05:02 -0800571 final int newSlideAmount = Math.round(SLIDE_UP_RATIO * getMeasuredHeight());
572 if (mSlideAmount != newSlideAmount) {
573 mSlideAmount = newSlideAmount;
574 mSwipeThreshold = Math.round(SWIPE_THRESHOLD_RATIO * newSlideAmount);
575 }
576
Adam Cohen36f43902010-12-15 21:17:33 -0800577 if (Float.compare(mPerspectiveShiftY, mNewPerspectiveShiftY) != 0 ||
578 Float.compare(mPerspectiveShiftX, mNewPerspectiveShiftX) != 0) {
Adam Cohen78db1aa2011-01-25 12:24:23 -0800579
Adam Cohen36f43902010-12-15 21:17:33 -0800580 mPerspectiveShiftY = mNewPerspectiveShiftY;
581 mPerspectiveShiftX = mNewPerspectiveShiftX;
Adam Cohen53838d22011-01-26 21:32:33 -0800582 updateChildTransforms();
Adam Cohen36f43902010-12-15 21:17:33 -0800583 }
Adam Cohen44729e32010-07-22 16:00:07 -0700584 }
585
Adam Cohena8a7c922011-02-28 17:20:44 -0800586 @Override
587 public boolean onGenericMotionEvent(MotionEvent event) {
588 if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
589 switch (event.getAction()) {
590 case MotionEvent.ACTION_SCROLL: {
591 final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
592 if (vscroll < 0) {
593 pacedScroll(false);
594 return true;
595 } else if (vscroll > 0) {
596 pacedScroll(true);
597 return true;
598 }
599 }
600 }
601 }
602 return super.onGenericMotionEvent(event);
603 }
604
605 // This ensures that the frequency of stack flips caused by scrolls is capped
606 private void pacedScroll(boolean up) {
607 long timeSinceLastScroll = System.currentTimeMillis() - mLastScrollTime;
608 if (timeSinceLastScroll > MIN_TIME_BETWEEN_SCROLLS) {
609 if (up) {
610 showPrevious();
611 } else {
612 showNext();
613 }
614 mLastScrollTime = System.currentTimeMillis();
615 }
616 }
617
Adam Cohen0e2de6d2011-01-19 17:16:34 -0800618 /**
619 * {@inheritDoc}
620 */
Adam Cohen44729e32010-07-22 16:00:07 -0700621 @Override
622 public boolean onInterceptTouchEvent(MotionEvent ev) {
623 int action = ev.getAction();
624 switch(action & MotionEvent.ACTION_MASK) {
Adam Cohen44729e32010-07-22 16:00:07 -0700625 case MotionEvent.ACTION_DOWN: {
626 if (mActivePointerId == INVALID_POINTER) {
627 mInitialX = ev.getX();
628 mInitialY = ev.getY();
629 mActivePointerId = ev.getPointerId(0);
630 }
631 break;
632 }
633 case MotionEvent.ACTION_MOVE: {
634 int pointerIndex = ev.findPointerIndex(mActivePointerId);
635 if (pointerIndex == INVALID_POINTER) {
636 // no data for our primary pointer, this shouldn't happen, log it
637 Log.d(TAG, "Error: No data for our primary pointer.");
638 return false;
639 }
Adam Cohen44729e32010-07-22 16:00:07 -0700640 float newY = ev.getY(pointerIndex);
641 float deltaY = newY - mInitialY;
642
Adam Cohen32a42f12010-08-11 19:34:30 -0700643 beginGestureIfNeeded(deltaY);
Adam Cohen44729e32010-07-22 16:00:07 -0700644 break;
645 }
646 case MotionEvent.ACTION_POINTER_UP: {
647 onSecondaryPointerUp(ev);
648 break;
649 }
650 case MotionEvent.ACTION_UP:
651 case MotionEvent.ACTION_CANCEL: {
652 mActivePointerId = INVALID_POINTER;
653 mSwipeGestureType = GESTURE_NONE;
Adam Cohen44729e32010-07-22 16:00:07 -0700654 }
655 }
656
657 return mSwipeGestureType != GESTURE_NONE;
658 }
659
Adam Cohen32a42f12010-08-11 19:34:30 -0700660 private void beginGestureIfNeeded(float deltaY) {
661 if ((int) Math.abs(deltaY) > mTouchSlop && mSwipeGestureType == GESTURE_NONE) {
Adam Cohenc99ff732011-01-16 16:11:41 -0800662 final int swipeGestureType = deltaY < 0 ? GESTURE_SLIDE_UP : GESTURE_SLIDE_DOWN;
Adam Cohen32a42f12010-08-11 19:34:30 -0700663 cancelLongPress();
664 requestDisallowInterceptTouchEvent(true);
665
Adam Cohend51bbb52010-10-18 10:59:49 -0700666 if (mAdapter == null) return;
Adam Cohenef17dd42011-01-20 17:20:57 -0800667 final int adapterCount = getCount();
Adam Cohend51bbb52010-10-18 10:59:49 -0700668
Adam Cohen839f4a52010-08-26 17:36:48 -0700669 int activeIndex;
670 if (mStackMode == ITEMS_SLIDE_UP) {
Adam Cohen96d8d562010-10-24 11:12:18 -0700671 activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1;
Adam Cohen839f4a52010-08-26 17:36:48 -0700672 } else {
Adam Cohen96d8d562010-10-24 11:12:18 -0700673 activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 1 : 0;
Adam Cohen839f4a52010-08-26 17:36:48 -0700674 }
Adam Cohen32a42f12010-08-11 19:34:30 -0700675
Aurimas Liutikas99441c52016-10-11 16:48:32 -0700676 boolean endOfStack = mLoopViews && adapterCount == 1
677 && ((mStackMode == ITEMS_SLIDE_UP && swipeGestureType == GESTURE_SLIDE_UP)
678 || (mStackMode == ITEMS_SLIDE_DOWN && swipeGestureType == GESTURE_SLIDE_DOWN));
679 boolean beginningOfStack = mLoopViews && adapterCount == 1
680 && ((mStackMode == ITEMS_SLIDE_DOWN && swipeGestureType == GESTURE_SLIDE_UP)
681 || (mStackMode == ITEMS_SLIDE_UP && swipeGestureType == GESTURE_SLIDE_DOWN));
Adam Cohenc99ff732011-01-16 16:11:41 -0800682
Adam Cohen3352b682010-10-26 13:54:00 -0700683 int stackMode;
Adam Cohenc99ff732011-01-16 16:11:41 -0800684 if (mLoopViews && !beginningOfStack && !endOfStack) {
Adam Cohen3352b682010-10-26 13:54:00 -0700685 stackMode = StackSlider.NORMAL_MODE;
Adam Cohenc99ff732011-01-16 16:11:41 -0800686 } else if (mCurrentWindowStartUnbounded + activeIndex == -1 || beginningOfStack) {
Adam Cohen96d8d562010-10-24 11:12:18 -0700687 activeIndex++;
Adam Cohen3352b682010-10-26 13:54:00 -0700688 stackMode = StackSlider.BEGINNING_OF_STACK_MODE;
Adam Cohenc99ff732011-01-16 16:11:41 -0800689 } else if (mCurrentWindowStartUnbounded + activeIndex == adapterCount - 1 || endOfStack) {
Adam Cohen3352b682010-10-26 13:54:00 -0700690 stackMode = StackSlider.END_OF_STACK_MODE;
Adam Cohen3d07af02010-08-18 17:46:23 -0700691 } else {
Adam Cohen3352b682010-10-26 13:54:00 -0700692 stackMode = StackSlider.NORMAL_MODE;
Adam Cohen32a42f12010-08-11 19:34:30 -0700693 }
Adam Cohen3d07af02010-08-18 17:46:23 -0700694
Adam Cohen3352b682010-10-26 13:54:00 -0700695 mTransitionIsSetup = stackMode == StackSlider.NORMAL_MODE;
696
Adam Cohen3d07af02010-08-18 17:46:23 -0700697 View v = getViewAtRelativeIndex(activeIndex);
698 if (v == null) return;
699
Adam Cohen3352b682010-10-26 13:54:00 -0700700 setupStackSlider(v, stackMode);
Adam Cohen3d07af02010-08-18 17:46:23 -0700701
702 // We only register this gesture if we've made it this far without a problem
703 mSwipeGestureType = swipeGestureType;
Adam Cohena32edd42010-10-26 10:35:01 -0700704 cancelHandleClick();
Adam Cohen32a42f12010-08-11 19:34:30 -0700705 }
706 }
707
Adam Cohen0e2de6d2011-01-19 17:16:34 -0800708 /**
709 * {@inheritDoc}
710 */
Adam Cohen44729e32010-07-22 16:00:07 -0700711 @Override
712 public boolean onTouchEvent(MotionEvent ev) {
Adam Cohena32edd42010-10-26 10:35:01 -0700713 super.onTouchEvent(ev);
714
Adam Cohen44729e32010-07-22 16:00:07 -0700715 int action = ev.getAction();
716 int pointerIndex = ev.findPointerIndex(mActivePointerId);
717 if (pointerIndex == INVALID_POINTER) {
718 // no data for our primary pointer, this shouldn't happen, log it
719 Log.d(TAG, "Error: No data for our primary pointer.");
720 return false;
721 }
722
723 float newY = ev.getY(pointerIndex);
Adam Cohen32a42f12010-08-11 19:34:30 -0700724 float newX = ev.getX(pointerIndex);
Adam Cohen44729e32010-07-22 16:00:07 -0700725 float deltaY = newY - mInitialY;
Adam Cohen32a42f12010-08-11 19:34:30 -0700726 float deltaX = newX - mInitialX;
Adam Cohen44729e32010-07-22 16:00:07 -0700727 if (mVelocityTracker == null) {
728 mVelocityTracker = VelocityTracker.obtain();
729 }
730 mVelocityTracker.addMovement(ev);
731
732 switch (action & MotionEvent.ACTION_MASK) {
733 case MotionEvent.ACTION_MOVE: {
Adam Cohen32a42f12010-08-11 19:34:30 -0700734 beginGestureIfNeeded(deltaY);
735
Adam Cohendfcdddd2010-09-10 14:38:40 -0700736 float rx = deltaX / (mSlideAmount * 1.0f);
Adam Cohen32a42f12010-08-11 19:34:30 -0700737 if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
Adam Cohendfcdddd2010-09-10 14:38:40 -0700738 float r = (deltaY - mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
Adam Cohen839f4a52010-08-26 17:36:48 -0700739 if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
Adam Cohen32a42f12010-08-11 19:34:30 -0700740 mStackSlider.setYProgress(1 - r);
741 mStackSlider.setXProgress(rx);
742 return true;
743 } else if (mSwipeGestureType == GESTURE_SLIDE_UP) {
Adam Cohendfcdddd2010-09-10 14:38:40 -0700744 float r = -(deltaY + mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
Adam Cohen839f4a52010-08-26 17:36:48 -0700745 if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
Adam Cohen32a42f12010-08-11 19:34:30 -0700746 mStackSlider.setYProgress(r);
747 mStackSlider.setXProgress(rx);
748 return true;
Adam Cohen44729e32010-07-22 16:00:07 -0700749 }
Adam Cohen44729e32010-07-22 16:00:07 -0700750 break;
751 }
752 case MotionEvent.ACTION_UP: {
753 handlePointerUp(ev);
754 break;
755 }
756 case MotionEvent.ACTION_POINTER_UP: {
757 onSecondaryPointerUp(ev);
758 break;
759 }
760 case MotionEvent.ACTION_CANCEL: {
761 mActivePointerId = INVALID_POINTER;
Adam Cohen44729e32010-07-22 16:00:07 -0700762 mSwipeGestureType = GESTURE_NONE;
Adam Cohen44729e32010-07-22 16:00:07 -0700763 break;
764 }
765 }
766 return true;
767 }
768
Adam Cohen44729e32010-07-22 16:00:07 -0700769 private void onSecondaryPointerUp(MotionEvent ev) {
770 final int activePointerIndex = ev.getActionIndex();
771 final int pointerId = ev.getPointerId(activePointerIndex);
772 if (pointerId == mActivePointerId) {
773
Adam Cohen96d8d562010-10-24 11:12:18 -0700774 int activeViewIndex = (mSwipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1;
Adam Cohen44729e32010-07-22 16:00:07 -0700775
776 View v = getViewAtRelativeIndex(activeViewIndex);
777 if (v == null) return;
778
779 // Our primary pointer has gone up -- let's see if we can find
780 // another pointer on the view. If so, then we should replace
781 // our primary pointer with this new pointer and adjust things
782 // so that the view doesn't jump
783 for (int index = 0; index < ev.getPointerCount(); index++) {
784 if (index != activePointerIndex) {
785
786 float x = ev.getX(index);
787 float y = ev.getY(index);
788
Patrick Dubroye80202d2010-11-16 15:41:08 -0800789 mTouchRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
790 if (mTouchRect.contains(Math.round(x), Math.round(y))) {
Adam Cohen44729e32010-07-22 16:00:07 -0700791 float oldX = ev.getX(activePointerIndex);
792 float oldY = ev.getY(activePointerIndex);
793
794 // adjust our frame of reference to avoid a jump
795 mInitialY += (y - oldY);
796 mInitialX += (x - oldX);
797
798 mActivePointerId = ev.getPointerId(index);
799 if (mVelocityTracker != null) {
800 mVelocityTracker.clear();
801 }
802 // ok, we're good, we found a new pointer which is touching the active view
803 return;
804 }
805 }
806 }
807 // if we made it this far, it means we didn't find a satisfactory new pointer :(,
Adam Cohen3d07af02010-08-18 17:46:23 -0700808 // so end the gesture
Adam Cohen44729e32010-07-22 16:00:07 -0700809 handlePointerUp(ev);
810 }
811 }
812
813 private void handlePointerUp(MotionEvent ev) {
814 int pointerIndex = ev.findPointerIndex(mActivePointerId);
815 float newY = ev.getY(pointerIndex);
816 int deltaY = (int) (newY - mInitialY);
Adam Cohen26e30bb2010-12-03 18:16:12 -0800817 mLastInteractionTime = System.currentTimeMillis();
Adam Cohen44729e32010-07-22 16:00:07 -0700818
Adam Cohen3d07af02010-08-18 17:46:23 -0700819 if (mVelocityTracker != null) {
820 mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
821 mYVelocity = (int) mVelocityTracker.getYVelocity(mActivePointerId);
822 }
Adam Cohen44729e32010-07-22 16:00:07 -0700823
824 if (mVelocityTracker != null) {
825 mVelocityTracker.recycle();
826 mVelocityTracker = null;
827 }
828
Adam Cohen3d07af02010-08-18 17:46:23 -0700829 if (deltaY > mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_DOWN
830 && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800831 // We reset the gesture variable, because otherwise we will ignore showPrevious() /
832 // showNext();
833 mSwipeGestureType = GESTURE_NONE;
834
Adam Cohen44729e32010-07-22 16:00:07 -0700835 // Swipe threshold exceeded, swipe down
Adam Cohen839f4a52010-08-26 17:36:48 -0700836 if (mStackMode == ITEMS_SLIDE_UP) {
Adam Cohen839f4a52010-08-26 17:36:48 -0700837 showPrevious();
Adam Cohen96d8d562010-10-24 11:12:18 -0700838 } else {
839 showNext();
Adam Cohen839f4a52010-08-26 17:36:48 -0700840 }
Adam Cohen32a42f12010-08-11 19:34:30 -0700841 mHighlight.bringToFront();
Adam Cohen3d07af02010-08-18 17:46:23 -0700842 } else if (deltaY < -mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_UP
843 && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
Adam Cohen0ac116b2010-11-11 11:39:53 -0800844 // We reset the gesture variable, because otherwise we will ignore showPrevious() /
845 // showNext();
846 mSwipeGestureType = GESTURE_NONE;
847
Adam Cohen44729e32010-07-22 16:00:07 -0700848 // Swipe threshold exceeded, swipe up
Adam Cohen839f4a52010-08-26 17:36:48 -0700849 if (mStackMode == ITEMS_SLIDE_UP) {
Adam Cohen839f4a52010-08-26 17:36:48 -0700850 showNext();
Adam Cohen96d8d562010-10-24 11:12:18 -0700851 } else {
852 showPrevious();
Adam Cohen839f4a52010-08-26 17:36:48 -0700853 }
854
Adam Cohen32a42f12010-08-11 19:34:30 -0700855 mHighlight.bringToFront();
Adam Cohen839f4a52010-08-26 17:36:48 -0700856 } else if (mSwipeGestureType == GESTURE_SLIDE_UP ) {
Adam Cohen44729e32010-07-22 16:00:07 -0700857 // Didn't swipe up far enough, snap back down
Adam Cohen839f4a52010-08-26 17:36:48 -0700858 int duration;
859 float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 1 : 0;
860 if (mStackMode == ITEMS_SLIDE_UP || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
861 duration = Math.round(mStackSlider.getDurationForNeutralPosition());
862 } else {
863 duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
864 }
Adam Cohen44729e32010-07-22 16:00:07 -0700865
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700866 StackSlider animationSlider = new StackSlider(mStackSlider);
Chet Haase2794eb32010-10-12 16:29:28 -0700867 PropertyValuesHolder snapBackY = PropertyValuesHolder.ofFloat("YProgress", finalYProgress);
868 PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
869 ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
Adam Cohen839f4a52010-08-26 17:36:48 -0700870 snapBackX, snapBackY);
Chet Haase2794eb32010-10-12 16:29:28 -0700871 pa.setDuration(duration);
Adam Cohen839f4a52010-08-26 17:36:48 -0700872 pa.setInterpolator(new LinearInterpolator());
873 pa.start();
Adam Cohen32a42f12010-08-11 19:34:30 -0700874 } else if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
Adam Cohen44729e32010-07-22 16:00:07 -0700875 // Didn't swipe down far enough, snap back up
Adam Cohen839f4a52010-08-26 17:36:48 -0700876 float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 0 : 1;
877 int duration;
878 if (mStackMode == ITEMS_SLIDE_DOWN || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
879 duration = Math.round(mStackSlider.getDurationForNeutralPosition());
880 } else {
881 duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
882 }
Adam Cohen3d07af02010-08-18 17:46:23 -0700883
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700884 StackSlider animationSlider = new StackSlider(mStackSlider);
Chet Haase2794eb32010-10-12 16:29:28 -0700885 PropertyValuesHolder snapBackY =
886 PropertyValuesHolder.ofFloat("YProgress",finalYProgress);
887 PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
888 ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
Adam Cohen839f4a52010-08-26 17:36:48 -0700889 snapBackX, snapBackY);
Chet Haase2794eb32010-10-12 16:29:28 -0700890 pa.setDuration(duration);
Adam Cohen839f4a52010-08-26 17:36:48 -0700891 pa.start();
Adam Cohen44729e32010-07-22 16:00:07 -0700892 }
893
894 mActivePointerId = INVALID_POINTER;
Adam Cohen44729e32010-07-22 16:00:07 -0700895 mSwipeGestureType = GESTURE_NONE;
Adam Cohen32a42f12010-08-11 19:34:30 -0700896 }
897
898 private class StackSlider {
899 View mView;
900 float mYProgress;
901 float mXProgress;
902
Adam Cohen3d07af02010-08-18 17:46:23 -0700903 static final int NORMAL_MODE = 0;
904 static final int BEGINNING_OF_STACK_MODE = 1;
905 static final int END_OF_STACK_MODE = 2;
906
907 int mMode = NORMAL_MODE;
908
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700909 public StackSlider() {
910 }
911
912 public StackSlider(StackSlider copy) {
913 mView = copy.mView;
914 mYProgress = copy.mYProgress;
915 mXProgress = copy.mXProgress;
Adam Cohen3d07af02010-08-18 17:46:23 -0700916 mMode = copy.mMode;
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700917 }
918
Adam Cohen32a42f12010-08-11 19:34:30 -0700919 private float cubic(float r) {
Adam Cohen839f4a52010-08-26 17:36:48 -0700920 return (float) (Math.pow(2 * r - 1, 3) + 1) / 2.0f;
Adam Cohen32a42f12010-08-11 19:34:30 -0700921 }
922
923 private float highlightAlphaInterpolator(float r) {
924 float pivot = 0.4f;
925 if (r < pivot) {
Adam Cohen839f4a52010-08-26 17:36:48 -0700926 return 0.85f * cubic(r / pivot);
Adam Cohen32a42f12010-08-11 19:34:30 -0700927 } else {
Adam Cohen839f4a52010-08-26 17:36:48 -0700928 return 0.85f * cubic(1 - (r - pivot) / (1 - pivot));
Adam Cohen32a42f12010-08-11 19:34:30 -0700929 }
930 }
931
932 private float viewAlphaInterpolator(float r) {
933 float pivot = 0.3f;
934 if (r > pivot) {
Adam Cohen839f4a52010-08-26 17:36:48 -0700935 return (r - pivot) / (1 - pivot);
Adam Cohen32a42f12010-08-11 19:34:30 -0700936 } else {
937 return 0;
938 }
939 }
940
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700941 private float rotationInterpolator(float r) {
942 float pivot = 0.2f;
943 if (r < pivot) {
944 return 0;
945 } else {
Adam Cohen839f4a52010-08-26 17:36:48 -0700946 return (r - pivot) / (1 - pivot);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700947 }
948 }
949
Adam Cohen32a42f12010-08-11 19:34:30 -0700950 void setView(View v) {
951 mView = v;
952 }
953
954 public void setYProgress(float r) {
955 // enforce r between 0 and 1
956 r = Math.min(1.0f, r);
957 r = Math.max(0, r);
958
959 mYProgress = r;
Adam Cohena02fdf12010-11-03 13:27:40 -0700960 if (mView == null) return;
961
Adam Cohen32a42f12010-08-11 19:34:30 -0700962 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
963 final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
964
Adam Cohen839f4a52010-08-26 17:36:48 -0700965 int stackDirection = (mStackMode == ITEMS_SLIDE_UP) ? 1 : -1;
966
Adam Cohen48867212011-01-09 13:49:40 -0800967 // We need to prevent any clipping issues which may arise by setting a layer type.
968 // This doesn't come for free however, so we only want to enable it when required.
969 if (Float.compare(0f, mYProgress) != 0 && Float.compare(1.0f, mYProgress) != 0) {
970 if (mView.getLayerType() == LAYER_TYPE_NONE) {
971 mView.setLayerType(LAYER_TYPE_HARDWARE, null);
972 }
973 } else {
974 if (mView.getLayerType() != LAYER_TYPE_NONE) {
975 mView.setLayerType(LAYER_TYPE_NONE, null);
976 }
977 }
978
Adam Cohen3d07af02010-08-18 17:46:23 -0700979 switch (mMode) {
980 case NORMAL_MODE:
Adam Cohendfcdddd2010-09-10 14:38:40 -0700981 viewLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
982 highlightLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
Adam Cohen3d07af02010-08-18 17:46:23 -0700983 mHighlight.setAlpha(highlightAlphaInterpolator(r));
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700984
Adam Cohen839f4a52010-08-26 17:36:48 -0700985 float alpha = viewAlphaInterpolator(1 - r);
Adam Cohenb04f7ad2010-08-15 13:22:42 -0700986
Adam Cohen3d07af02010-08-18 17:46:23 -0700987 // We make sure that views which can't be seen (have 0 alpha) are also invisible
988 // so that they don't interfere with click events.
989 if (mView.getAlpha() == 0 && alpha != 0 && mView.getVisibility() != VISIBLE) {
990 mView.setVisibility(VISIBLE);
991 } else if (alpha == 0 && mView.getAlpha() != 0
992 && mView.getVisibility() == VISIBLE) {
993 mView.setVisibility(INVISIBLE);
994 }
995
996 mView.setAlpha(alpha);
Adam Cohen839f4a52010-08-26 17:36:48 -0700997 mView.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
998 mHighlight.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
Adam Cohen3d07af02010-08-18 17:46:23 -0700999 break;
Adam Cohen96d8d562010-10-24 11:12:18 -07001000 case END_OF_STACK_MODE:
Adam Cohen839f4a52010-08-26 17:36:48 -07001001 r = r * 0.2f;
Adam Cohendfcdddd2010-09-10 14:38:40 -07001002 viewLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount));
1003 highlightLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount));
Adam Cohen3d07af02010-08-18 17:46:23 -07001004 mHighlight.setAlpha(highlightAlphaInterpolator(r));
1005 break;
Adam Cohen96d8d562010-10-24 11:12:18 -07001006 case BEGINNING_OF_STACK_MODE:
Adam Cohen839f4a52010-08-26 17:36:48 -07001007 r = (1-r) * 0.2f;
Adam Cohendfcdddd2010-09-10 14:38:40 -07001008 viewLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount));
1009 highlightLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount));
Adam Cohen3d07af02010-08-18 17:46:23 -07001010 mHighlight.setAlpha(highlightAlphaInterpolator(r));
1011 break;
Adam Cohenb04f7ad2010-08-15 13:22:42 -07001012 }
Adam Cohen32a42f12010-08-11 19:34:30 -07001013 }
1014
1015 public void setXProgress(float r) {
1016 // enforce r between 0 and 1
Adam Cohen3d07af02010-08-18 17:46:23 -07001017 r = Math.min(2.0f, r);
1018 r = Math.max(-2.0f, r);
Adam Cohen32a42f12010-08-11 19:34:30 -07001019
1020 mXProgress = r;
1021
Adam Cohena02fdf12010-11-03 13:27:40 -07001022 if (mView == null) return;
Adam Cohen32a42f12010-08-11 19:34:30 -07001023 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
1024 final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
1025
Adam Cohen3d07af02010-08-18 17:46:23 -07001026 r *= 0.2f;
Adam Cohendfcdddd2010-09-10 14:38:40 -07001027 viewLp.setHorizontalOffset(Math.round(r * mSlideAmount));
1028 highlightLp.setHorizontalOffset(Math.round(r * mSlideAmount));
Adam Cohen32a42f12010-08-11 19:34:30 -07001029 }
1030
Adam Cohen3d07af02010-08-18 17:46:23 -07001031 void setMode(int mode) {
1032 mMode = mode;
1033 }
1034
1035 float getDurationForNeutralPosition() {
Adam Cohen839f4a52010-08-26 17:36:48 -07001036 return getDuration(false, 0);
Adam Cohen3d07af02010-08-18 17:46:23 -07001037 }
1038
1039 float getDurationForOffscreenPosition() {
Adam Cohen839f4a52010-08-26 17:36:48 -07001040 return getDuration(true, 0);
Adam Cohen3d07af02010-08-18 17:46:23 -07001041 }
1042
Adam Cohen839f4a52010-08-26 17:36:48 -07001043 float getDurationForNeutralPosition(float velocity) {
1044 return getDuration(false, velocity);
1045 }
1046
1047 float getDurationForOffscreenPosition(float velocity) {
1048 return getDuration(true, velocity);
1049 }
1050
1051 private float getDuration(boolean invert, float velocity) {
Adam Cohen3d07af02010-08-18 17:46:23 -07001052 if (mView != null) {
1053 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
1054
Neil Fuller33253a42014-10-01 11:55:10 +01001055 float d = (float) Math.hypot(viewLp.horizontalOffset, viewLp.verticalOffset);
1056 float maxd = (float) Math.hypot(mSlideAmount, 0.4f * mSlideAmount);
Tony Wickham400ef792016-11-07 10:18:55 -08001057 if (d > maxd) {
1058 // Because mSlideAmount is updated in onLayout(), it is possible that d > maxd
1059 // if we get onLayout() right before this method is called.
1060 d = maxd;
1061 }
Adam Cohen839f4a52010-08-26 17:36:48 -07001062
1063 if (velocity == 0) {
1064 return (invert ? (1 - d / maxd) : d / maxd) * DEFAULT_ANIMATION_DURATION;
1065 } else {
1066 float duration = invert ? d / Math.abs(velocity) :
1067 (maxd - d) / Math.abs(velocity);
1068 if (duration < MINIMUM_ANIMATION_DURATION ||
1069 duration > DEFAULT_ANIMATION_DURATION) {
1070 return getDuration(invert, 0);
1071 } else {
1072 return duration;
1073 }
1074 }
Adam Cohen3d07af02010-08-18 17:46:23 -07001075 }
1076 return 0;
1077 }
1078
Romain Guye5ebcb02010-10-15 13:57:28 -07001079 // Used for animations
1080 @SuppressWarnings({"UnusedDeclaration"})
Adam Cohen839f4a52010-08-26 17:36:48 -07001081 public float getYProgress() {
Adam Cohen32a42f12010-08-11 19:34:30 -07001082 return mYProgress;
1083 }
1084
Romain Guye5ebcb02010-10-15 13:57:28 -07001085 // Used for animations
1086 @SuppressWarnings({"UnusedDeclaration"})
Adam Cohen839f4a52010-08-26 17:36:48 -07001087 public float getXProgress() {
Adam Cohen32a42f12010-08-11 19:34:30 -07001088 return mXProgress;
1089 }
Adam Cohen44729e32010-07-22 16:00:07 -07001090 }
1091
Adam Cohen9b073942010-08-19 16:49:52 -07001092 LayoutParams createOrReuseLayoutParams(View v) {
1093 final ViewGroup.LayoutParams currentLp = v.getLayoutParams();
1094 if (currentLp instanceof LayoutParams) {
1095 LayoutParams lp = (LayoutParams) currentLp;
1096 lp.setHorizontalOffset(0);
1097 lp.setVerticalOffset(0);
Adam Cohen839f4a52010-08-26 17:36:48 -07001098 lp.width = 0;
1099 lp.width = 0;
Adam Cohen9b073942010-08-19 16:49:52 -07001100 return lp;
1101 }
1102 return new LayoutParams(v);
Adam Cohen32a42f12010-08-11 19:34:30 -07001103 }
1104
Adam Cohen9b073942010-08-19 16:49:52 -07001105 @Override
1106 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Adam Cohenef17dd42011-01-20 17:20:57 -08001107 checkForAndHandleDataChanged();
Adam Cohenb04f7ad2010-08-15 13:22:42 -07001108
Adam Cohen9b073942010-08-19 16:49:52 -07001109 final int childCount = getChildCount();
1110 for (int i = 0; i < childCount; i++) {
1111 final View child = getChildAt(i);
Adam Cohen32a42f12010-08-11 19:34:30 -07001112
Adam Cohen9b073942010-08-19 16:49:52 -07001113 int childRight = mPaddingLeft + child.getMeasuredWidth();
1114 int childBottom = mPaddingTop + child.getMeasuredHeight();
1115 LayoutParams lp = (LayoutParams) child.getLayoutParams();
Adam Cohen32a42f12010-08-11 19:34:30 -07001116
Adam Cohen9b073942010-08-19 16:49:52 -07001117 child.layout(mPaddingLeft + lp.horizontalOffset, mPaddingTop + lp.verticalOffset,
1118 childRight + lp.horizontalOffset, childBottom + lp.verticalOffset);
1119
Adam Cohen9b073942010-08-19 16:49:52 -07001120 }
Adam Cohen9b073942010-08-19 16:49:52 -07001121 onLayout();
Adam Cohen32a42f12010-08-11 19:34:30 -07001122 }
1123
Adam Cohen26e30bb2010-12-03 18:16:12 -08001124 @Override
1125 public void advance() {
1126 long timeSinceLastInteraction = System.currentTimeMillis() - mLastInteractionTime;
Adam Cohenc99ff732011-01-16 16:11:41 -08001127
1128 if (mAdapter == null) return;
Adam Cohenef17dd42011-01-20 17:20:57 -08001129 final int adapterCount = getCount();
Adam Cohenc99ff732011-01-16 16:11:41 -08001130 if (adapterCount == 1 && mLoopViews) return;
1131
Adam Cohen26e30bb2010-12-03 18:16:12 -08001132 if (mSwipeGestureType == GESTURE_NONE &&
1133 timeSinceLastInteraction > MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE) {
1134 showNext();
1135 }
1136 }
1137
Adam Cohen839f4a52010-08-26 17:36:48 -07001138 private void measureChildren() {
1139 final int count = getChildCount();
Adam Cohen36f43902010-12-15 21:17:33 -08001140
1141 final int measuredWidth = getMeasuredWidth();
1142 final int measuredHeight = getMeasuredHeight();
1143
1144 final int childWidth = Math.round(measuredWidth*(1-PERSPECTIVE_SHIFT_FACTOR_X))
Adam Cohen026e1212010-12-13 12:29:17 -08001145 - mPaddingLeft - mPaddingRight;
Adam Cohen36f43902010-12-15 21:17:33 -08001146 final int childHeight = Math.round(measuredHeight*(1-PERSPECTIVE_SHIFT_FACTOR_Y))
Adam Cohen839f4a52010-08-26 17:36:48 -07001147 - mPaddingTop - mPaddingBottom;
1148
Adam Cohen36f43902010-12-15 21:17:33 -08001149 int maxWidth = 0;
1150 int maxHeight = 0;
1151
Adam Cohen839f4a52010-08-26 17:36:48 -07001152 for (int i = 0; i < count; i++) {
1153 final View child = getChildAt(i);
Adam Cohen36f43902010-12-15 21:17:33 -08001154 child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST),
1155 MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST));
1156
1157 if (child != mHighlight && child != mClickFeedback) {
1158 final int childMeasuredWidth = child.getMeasuredWidth();
1159 final int childMeasuredHeight = child.getMeasuredHeight();
1160 if (childMeasuredWidth > maxWidth) {
1161 maxWidth = childMeasuredWidth;
1162 }
1163 if (childMeasuredHeight > maxHeight) {
1164 maxHeight = childMeasuredHeight;
1165 }
1166 }
1167 }
1168
1169 mNewPerspectiveShiftX = PERSPECTIVE_SHIFT_FACTOR_X * measuredWidth;
1170 mNewPerspectiveShiftY = PERSPECTIVE_SHIFT_FACTOR_Y * measuredHeight;
Adam Cohen53838d22011-01-26 21:32:33 -08001171
1172 // If we have extra space, we try and spread the items out
Adam Cohen78db1aa2011-01-25 12:24:23 -08001173 if (maxWidth > 0 && count > 0 && maxWidth < childWidth) {
Adam Cohen36f43902010-12-15 21:17:33 -08001174 mNewPerspectiveShiftX = measuredWidth - maxWidth;
1175 }
1176
Adam Cohen78db1aa2011-01-25 12:24:23 -08001177 if (maxHeight > 0 && count > 0 && maxHeight < childHeight) {
Adam Cohen36f43902010-12-15 21:17:33 -08001178 mNewPerspectiveShiftY = measuredHeight - maxHeight;
Adam Cohen839f4a52010-08-26 17:36:48 -07001179 }
1180 }
1181
1182 @Override
1183 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1184 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
1185 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
1186 final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
1187 final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
1188
1189 boolean haveChildRefSize = (mReferenceChildWidth != -1 && mReferenceChildHeight != -1);
1190
1191 // We need to deal with the case where our parent hasn't told us how
1192 // big we should be. In this case we should
Adam Cohen026e1212010-12-13 12:29:17 -08001193 float factorY = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_Y);
Adam Cohen839f4a52010-08-26 17:36:48 -07001194 if (heightSpecMode == MeasureSpec.UNSPECIFIED) {
1195 heightSpecSize = haveChildRefSize ?
Adam Cohen026e1212010-12-13 12:29:17 -08001196 Math.round(mReferenceChildHeight * (1 + factorY)) +
Adam Cohen839f4a52010-08-26 17:36:48 -07001197 mPaddingTop + mPaddingBottom : 0;
1198 } else if (heightSpecMode == MeasureSpec.AT_MOST) {
Dianne Hackborn189ee182010-12-02 21:48:53 -08001199 if (haveChildRefSize) {
Adam Cohen026e1212010-12-13 12:29:17 -08001200 int height = Math.round(mReferenceChildHeight * (1 + factorY))
Dianne Hackborn189ee182010-12-02 21:48:53 -08001201 + mPaddingTop + mPaddingBottom;
1202 if (height <= heightSpecSize) {
1203 heightSpecSize = height;
1204 } else {
1205 heightSpecSize |= MEASURED_STATE_TOO_SMALL;
Adam Cohenef17dd42011-01-20 17:20:57 -08001206
Dianne Hackborn189ee182010-12-02 21:48:53 -08001207 }
1208 } else {
1209 heightSpecSize = 0;
1210 }
Adam Cohen839f4a52010-08-26 17:36:48 -07001211 }
1212
Adam Cohen026e1212010-12-13 12:29:17 -08001213 float factorX = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_X);
Adam Cohen839f4a52010-08-26 17:36:48 -07001214 if (widthSpecMode == MeasureSpec.UNSPECIFIED) {
Adam Cohen026e1212010-12-13 12:29:17 -08001215 widthSpecSize = haveChildRefSize ?
1216 Math.round(mReferenceChildWidth * (1 + factorX)) +
1217 mPaddingLeft + mPaddingRight : 0;
Adam Cohen839f4a52010-08-26 17:36:48 -07001218 } else if (heightSpecMode == MeasureSpec.AT_MOST) {
Dianne Hackborn189ee182010-12-02 21:48:53 -08001219 if (haveChildRefSize) {
1220 int width = mReferenceChildWidth + mPaddingLeft + mPaddingRight;
1221 if (width <= widthSpecSize) {
1222 widthSpecSize = width;
1223 } else {
1224 widthSpecSize |= MEASURED_STATE_TOO_SMALL;
1225 }
1226 } else {
1227 widthSpecSize = 0;
1228 }
Adam Cohen839f4a52010-08-26 17:36:48 -07001229 }
Adam Cohen839f4a52010-08-26 17:36:48 -07001230 setMeasuredDimension(widthSpecSize, heightSpecSize);
1231 measureChildren();
1232 }
1233
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001234 @Override
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001235 public CharSequence getAccessibilityClassName() {
1236 return StackView.class.getName();
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001237 }
1238
Alan Viverettea54956a2015-01-07 16:05:02 -08001239 /** @hide */
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001240 @Override
Alan Viverettea54956a2015-01-07 16:05:02 -08001241 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
1242 super.onInitializeAccessibilityNodeInfoInternal(info);
Svetoslav Ganov48d15862012-05-15 10:10:00 -07001243 info.setScrollable(getChildCount() > 1);
Svetoslav Ganovfb1e80a2012-05-16 17:33:19 -07001244 if (isEnabled()) {
1245 if (getDisplayedChild() < getChildCount() - 1) {
yingleiw7b2e8882019-07-25 18:27:56 -07001246 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
1247 if (mStackMode == ITEMS_SLIDE_UP) {
1248 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_DOWN);
1249 } else {
1250 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_UP);
1251 }
Svetoslav Ganovfb1e80a2012-05-16 17:33:19 -07001252 }
1253 if (getDisplayedChild() > 0) {
yingleiw7b2e8882019-07-25 18:27:56 -07001254 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
1255 if (mStackMode == ITEMS_SLIDE_UP) {
1256 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_UP);
1257 } else {
1258 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_DOWN);
1259 }
Svetoslav Ganovfb1e80a2012-05-16 17:33:19 -07001260 }
Svetoslav Ganov48d15862012-05-15 10:10:00 -07001261 }
1262 }
1263
yingleiw7b2e8882019-07-25 18:27:56 -07001264 private boolean goForward() {
1265 if (getDisplayedChild() < getChildCount() - 1) {
1266 showNext();
1267 return true;
1268 }
1269 return false;
1270 }
1271
1272 private boolean goBackward() {
1273 if (getDisplayedChild() > 0) {
1274 showPrevious();
1275 return true;
1276 }
1277 return false;
1278 }
1279
Alan Viverettea54956a2015-01-07 16:05:02 -08001280 /** @hide */
Svetoslav Ganov48d15862012-05-15 10:10:00 -07001281 @Override
Alan Viverettea54956a2015-01-07 16:05:02 -08001282 public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
1283 if (super.performAccessibilityActionInternal(action, arguments)) {
Svetoslav Ganov48d15862012-05-15 10:10:00 -07001284 return true;
1285 }
Svetoslav Ganovfb1e80a2012-05-16 17:33:19 -07001286 if (!isEnabled()) {
1287 return false;
1288 }
Svetoslav Ganov48d15862012-05-15 10:10:00 -07001289 switch (action) {
1290 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
yingleiw7b2e8882019-07-25 18:27:56 -07001291 return goForward();
1292 }
Svetoslav Ganov48d15862012-05-15 10:10:00 -07001293 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
yingleiw7b2e8882019-07-25 18:27:56 -07001294 return goBackward();
1295 }
1296 case R.id.accessibilityActionPageUp: {
1297 if (mStackMode == ITEMS_SLIDE_UP) {
1298 return goBackward();
1299 } else {
1300 return goForward();
Svetoslav Ganov48d15862012-05-15 10:10:00 -07001301 }
yingleiw7b2e8882019-07-25 18:27:56 -07001302 }
1303 case R.id.accessibilityActionPageDown: {
1304 if (mStackMode == ITEMS_SLIDE_UP) {
1305 return goForward();
1306 } else {
1307 return goBackward();
1308 }
1309 }
Svetoslav Ganov48d15862012-05-15 10:10:00 -07001310 }
1311 return false;
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001312 }
1313
Adam Cohen9b073942010-08-19 16:49:52 -07001314 class LayoutParams extends ViewGroup.LayoutParams {
1315 int horizontalOffset;
1316 int verticalOffset;
1317 View mView;
Adam Cohend51bbb52010-10-18 10:59:49 -07001318 private final Rect parentRect = new Rect();
1319 private final Rect invalidateRect = new Rect();
1320 private final RectF invalidateRectf = new RectF();
1321 private final Rect globalInvalidateRect = new Rect();
Adam Cohen32a42f12010-08-11 19:34:30 -07001322
Adam Cohen9b073942010-08-19 16:49:52 -07001323 LayoutParams(View view) {
1324 super(0, 0);
Adam Cohen839f4a52010-08-26 17:36:48 -07001325 width = 0;
1326 height = 0;
Adam Cohen9b073942010-08-19 16:49:52 -07001327 horizontalOffset = 0;
1328 verticalOffset = 0;
1329 mView = view;
1330 }
Adam Cohen32a42f12010-08-11 19:34:30 -07001331
Adam Cohen9b073942010-08-19 16:49:52 -07001332 LayoutParams(Context c, AttributeSet attrs) {
1333 super(c, attrs);
1334 horizontalOffset = 0;
1335 verticalOffset = 0;
Adam Cohen839f4a52010-08-26 17:36:48 -07001336 width = 0;
1337 height = 0;
Adam Cohen9b073942010-08-19 16:49:52 -07001338 }
Adam Cohen32a42f12010-08-11 19:34:30 -07001339
Adam Cohen9b073942010-08-19 16:49:52 -07001340 void invalidateGlobalRegion(View v, Rect r) {
Adam Cohend51bbb52010-10-18 10:59:49 -07001341 // We need to make a new rect here, so as not to modify the one passed
1342 globalInvalidateRect.set(r);
Adam Cohen1ff65d12011-02-16 14:04:09 -08001343 globalInvalidateRect.union(0, 0, getWidth(), getHeight());
Adam Cohen9b073942010-08-19 16:49:52 -07001344 View p = v;
1345 if (!(v.getParent() != null && v.getParent() instanceof View)) return;
1346
Adam Cohen9b073942010-08-19 16:49:52 -07001347 boolean firstPass = true;
1348 parentRect.set(0, 0, 0, 0);
Adam Cohenb7f4d032010-09-16 15:25:54 -07001349 while (p.getParent() != null && p.getParent() instanceof View
Adam Cohend51bbb52010-10-18 10:59:49 -07001350 && !parentRect.contains(globalInvalidateRect)) {
Adam Cohen9b073942010-08-19 16:49:52 -07001351 if (!firstPass) {
Adam Cohend51bbb52010-10-18 10:59:49 -07001352 globalInvalidateRect.offset(p.getLeft() - p.getScrollX(), p.getTop()
1353 - p.getScrollY());
Adam Cohen9b073942010-08-19 16:49:52 -07001354 }
1355 firstPass = false;
1356 p = (View) p.getParent();
Adam Cohenb7f4d032010-09-16 15:25:54 -07001357 parentRect.set(p.getScrollX(), p.getScrollY(),
Adam Cohen1ff65d12011-02-16 14:04:09 -08001358 p.getWidth() + p.getScrollX(), p.getHeight() + p.getScrollY());
1359 p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top,
1360 globalInvalidateRect.right, globalInvalidateRect.bottom);
Adam Cohen9b073942010-08-19 16:49:52 -07001361 }
1362
Adam Cohend51bbb52010-10-18 10:59:49 -07001363 p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top,
1364 globalInvalidateRect.right, globalInvalidateRect.bottom);
Adam Cohen9b073942010-08-19 16:49:52 -07001365 }
1366
Adam Cohend51bbb52010-10-18 10:59:49 -07001367 Rect getInvalidateRect() {
1368 return invalidateRect;
1369 }
1370
1371 void resetInvalidateRect() {
Adam Cohen0ac116b2010-11-11 11:39:53 -08001372 invalidateRect.set(0, 0, 0, 0);
Adam Cohend51bbb52010-10-18 10:59:49 -07001373 }
1374
Chet Haasea18a86b2010-09-07 13:20:00 -07001375 // This is public so that ObjectAnimator can access it
Adam Cohen9b073942010-08-19 16:49:52 -07001376 public void setVerticalOffset(int newVerticalOffset) {
Adam Cohen0ac116b2010-11-11 11:39:53 -08001377 setOffsets(horizontalOffset, newVerticalOffset);
1378 }
1379
1380 public void setHorizontalOffset(int newHorizontalOffset) {
1381 setOffsets(newHorizontalOffset, verticalOffset);
1382 }
1383
1384 public void setOffsets(int newHorizontalOffset, int newVerticalOffset) {
1385 int horizontalOffsetDelta = newHorizontalOffset - horizontalOffset;
1386 horizontalOffset = newHorizontalOffset;
1387 int verticalOffsetDelta = newVerticalOffset - verticalOffset;
Adam Cohen9b073942010-08-19 16:49:52 -07001388 verticalOffset = newVerticalOffset;
1389
1390 if (mView != null) {
1391 mView.requestLayout();
Adam Cohen0ac116b2010-11-11 11:39:53 -08001392 int left = Math.min(mView.getLeft() + horizontalOffsetDelta, mView.getLeft());
1393 int right = Math.max(mView.getRight() + horizontalOffsetDelta, mView.getRight());
1394 int top = Math.min(mView.getTop() + verticalOffsetDelta, mView.getTop());
1395 int bottom = Math.max(mView.getBottom() + verticalOffsetDelta, mView.getBottom());
Adam Cohen9b073942010-08-19 16:49:52 -07001396
Adam Cohen0ac116b2010-11-11 11:39:53 -08001397 invalidateRectf.set(left, top, right, bottom);
Adam Cohen9b073942010-08-19 16:49:52 -07001398
1399 float xoffset = -invalidateRectf.left;
1400 float yoffset = -invalidateRectf.top;
1401 invalidateRectf.offset(xoffset, yoffset);
1402 mView.getMatrix().mapRect(invalidateRectf);
1403 invalidateRectf.offset(-xoffset, -yoffset);
1404
Adam Cohen0ac116b2010-11-11 11:39:53 -08001405 invalidateRect.set((int) Math.floor(invalidateRectf.left),
Adam Cohen9b073942010-08-19 16:49:52 -07001406 (int) Math.floor(invalidateRectf.top),
1407 (int) Math.ceil(invalidateRectf.right),
1408 (int) Math.ceil(invalidateRectf.bottom));
1409
1410 invalidateGlobalRegion(mView, invalidateRect);
1411 }
1412 }
1413 }
1414
1415 private static class HolographicHelper {
1416 private final Paint mHolographicPaint = new Paint();
1417 private final Paint mErasePaint = new Paint();
Adam Cohen839f4a52010-08-26 17:36:48 -07001418 private final Paint mBlurPaint = new Paint();
Adam Cohen8baf5df2010-11-11 15:23:41 -08001419 private static final int RES_OUT = 0;
1420 private static final int CLICK_FEEDBACK = 1;
1421 private float mDensity;
Patrick Dubroye80202d2010-11-16 15:41:08 -08001422 private BlurMaskFilter mSmallBlurMaskFilter;
1423 private BlurMaskFilter mLargeBlurMaskFilter;
1424 private final Canvas mCanvas = new Canvas();
1425 private final Canvas mMaskCanvas = new Canvas();
1426 private final int[] mTmpXY = new int[2];
1427 private final Matrix mIdentityMatrix = new Matrix();
Adam Cohen9b073942010-08-19 16:49:52 -07001428
Adam Cohendfcdddd2010-09-10 14:38:40 -07001429 HolographicHelper(Context context) {
Adam Cohen8baf5df2010-11-11 15:23:41 -08001430 mDensity = context.getResources().getDisplayMetrics().density;
Adam Cohendfcdddd2010-09-10 14:38:40 -07001431
Adam Cohen9b073942010-08-19 16:49:52 -07001432 mHolographicPaint.setFilterBitmap(true);
Adam Cohen839f4a52010-08-26 17:36:48 -07001433 mHolographicPaint.setMaskFilter(TableMaskFilter.CreateClipTable(0, 30));
Adam Cohen9b073942010-08-19 16:49:52 -07001434 mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
1435 mErasePaint.setFilterBitmap(true);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001436
1437 mSmallBlurMaskFilter = new BlurMaskFilter(2 * mDensity, BlurMaskFilter.Blur.NORMAL);
1438 mLargeBlurMaskFilter = new BlurMaskFilter(4 * mDensity, BlurMaskFilter.Blur.NORMAL);
Adam Cohen9b073942010-08-19 16:49:52 -07001439 }
1440
Adam Cohen26f072c2011-04-01 16:23:18 -07001441 Bitmap createClickOutline(View v, int color) {
1442 return createOutline(v, CLICK_FEEDBACK, color);
Adam Cohen8baf5df2010-11-11 15:23:41 -08001443 }
1444
Adam Cohen26f072c2011-04-01 16:23:18 -07001445 Bitmap createResOutline(View v, int color) {
1446 return createOutline(v, RES_OUT, color);
1447 }
1448
1449 Bitmap createOutline(View v, int type, int color) {
1450 mHolographicPaint.setColor(color);
Adam Cohen8baf5df2010-11-11 15:23:41 -08001451 if (type == RES_OUT) {
Patrick Dubroye80202d2010-11-16 15:41:08 -08001452 mBlurPaint.setMaskFilter(mSmallBlurMaskFilter);
Adam Cohen8baf5df2010-11-11 15:23:41 -08001453 } else if (type == CLICK_FEEDBACK) {
Patrick Dubroye80202d2010-11-16 15:41:08 -08001454 mBlurPaint.setMaskFilter(mLargeBlurMaskFilter);
Adam Cohen8baf5df2010-11-11 15:23:41 -08001455 }
1456
Adam Cohen9b073942010-08-19 16:49:52 -07001457 if (v.getMeasuredWidth() == 0 || v.getMeasuredHeight() == 0) {
1458 return null;
1459 }
1460
Dianne Hackborndde331c2012-08-03 14:01:57 -07001461 Bitmap bitmap = Bitmap.createBitmap(v.getResources().getDisplayMetrics(),
1462 v.getMeasuredWidth(), v.getMeasuredHeight(), Bitmap.Config.ARGB_8888);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001463 mCanvas.setBitmap(bitmap);
Adam Cohen9b073942010-08-19 16:49:52 -07001464
1465 float rotationX = v.getRotationX();
Adam Cohen839f4a52010-08-26 17:36:48 -07001466 float rotation = v.getRotation();
1467 float translationY = v.getTranslationY();
Adam Cohen026e1212010-12-13 12:29:17 -08001468 float translationX = v.getTranslationX();
Adam Cohen9b073942010-08-19 16:49:52 -07001469 v.setRotationX(0);
Adam Cohen839f4a52010-08-26 17:36:48 -07001470 v.setRotation(0);
1471 v.setTranslationY(0);
Adam Cohen026e1212010-12-13 12:29:17 -08001472 v.setTranslationX(0);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001473 v.draw(mCanvas);
Adam Cohen9b073942010-08-19 16:49:52 -07001474 v.setRotationX(rotationX);
Adam Cohen839f4a52010-08-26 17:36:48 -07001475 v.setRotation(rotation);
1476 v.setTranslationY(translationY);
Adam Cohen026e1212010-12-13 12:29:17 -08001477 v.setTranslationX(translationX);
Adam Cohen9b073942010-08-19 16:49:52 -07001478
Patrick Dubroye80202d2010-11-16 15:41:08 -08001479 drawOutline(mCanvas, bitmap);
Dianne Hackborn6311d0a2011-08-02 16:37:58 -07001480 mCanvas.setBitmap(null);
Adam Cohen9b073942010-08-19 16:49:52 -07001481 return bitmap;
1482 }
1483
Adam Cohen9b073942010-08-19 16:49:52 -07001484 void drawOutline(Canvas dest, Bitmap src) {
Patrick Dubroye80202d2010-11-16 15:41:08 -08001485 final int[] xy = mTmpXY;
Adam Cohen839f4a52010-08-26 17:36:48 -07001486 Bitmap mask = src.extractAlpha(mBlurPaint, xy);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001487 mMaskCanvas.setBitmap(mask);
1488 mMaskCanvas.drawBitmap(src, -xy[0], -xy[1], mErasePaint);
Adam Cohen9b073942010-08-19 16:49:52 -07001489 dest.drawColor(0, PorterDuff.Mode.CLEAR);
Patrick Dubroye80202d2010-11-16 15:41:08 -08001490 dest.setMatrix(mIdentityMatrix);
Adam Cohen839f4a52010-08-26 17:36:48 -07001491 dest.drawBitmap(mask, xy[0], xy[1], mHolographicPaint);
Dianne Hackborn6311d0a2011-08-02 16:37:58 -07001492 mMaskCanvas.setBitmap(null);
Adam Cohen9b073942010-08-19 16:49:52 -07001493 mask.recycle();
1494 }
Adam Cohen32a42f12010-08-11 19:34:30 -07001495 }
Adam Cohenef17dd42011-01-20 17:20:57 -08001496}