summaryrefslogtreecommitdiffstats
path: root/core/java/android/widget
diff options
context:
space:
mode:
authorAdam Cohen <adamcohen@google.com>2010-07-22 16:00:07 -0700
committerAdam Cohen <adamcohen@google.com>2010-08-10 15:09:31 -0700
commit44729e3d1c01265858eec566c7b7c676c46a7916 (patch)
tree7c544b4386112fb6f857b6b5e752791373d53651 /core/java/android/widget
parenta774765686bd61b01b9b0386c35c338c61a46225 (diff)
downloadframeworks_base-44729e3d1c01265858eec566c7b7c676c46a7916.zip
frameworks_base-44729e3d1c01265858eec566c7b7c676c46a7916.tar.gz
frameworks_base-44729e3d1c01265858eec566c7b7c676c46a7916.tar.bz2
Preliminary implementation of StackView, which extends AdapterViewAnimator.
Change-Id: I3e9d1203fc8848835f28d6bc1c9dc0a3fcf7f242
Diffstat (limited to 'core/java/android/widget')
-rw-r--r--core/java/android/widget/AdapterViewAnimator.java419
-rw-r--r--core/java/android/widget/RemoteViewsAdapter.java1
-rw-r--r--core/java/android/widget/StackView.java487
3 files changed, 833 insertions, 74 deletions
diff --git a/core/java/android/widget/AdapterViewAnimator.java b/core/java/android/widget/AdapterViewAnimator.java
index a6d5170..c335a8b 100644
--- a/core/java/android/widget/AdapterViewAnimator.java
+++ b/core/java/android/widget/AdapterViewAnimator.java
@@ -16,14 +16,22 @@
package android.widget;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+
+import android.animation.PropertyAnimator;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
+import android.graphics.Rect;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
@@ -35,23 +43,100 @@ import android.view.animation.AnimationUtils;
* @attr ref android.R.styleable#AdapterViewAnimator_outAnimation
* @attr ref android.R.styleable#AdapterViewAnimator_animateFirstView
*/
-public class AdapterViewAnimator extends AdapterView<Adapter> implements RemoteViewsAdapter.RemoteAdapterConnectionCallback{
+public abstract class AdapterViewAnimator extends AdapterView<Adapter>
+ implements RemoteViewsAdapter.RemoteAdapterConnectionCallback{
private static final String TAG = "RemoteViewAnimator";
+ /**
+ * The index of the current child, which appears anywhere from the beginning
+ * to the end of the current set of children, as specified by {@link #mActiveOffset}
+ */
int mWhichChild = 0;
- boolean mFirstTime = true;
+
+ /**
+ * Whether or not the first view(s) should be animated in
+ */
boolean mAnimateFirstTime = true;
+ /**
+ * Represents where the in the current window of
+ * views the current <code>mDisplayedChild</code> sits
+ */
+ int mActiveOffset = 0;
+
+ /**
+ * The number of views that the {@link AdapterViewAnimator} keeps as children at any
+ * given time (not counting views that are pending removal, see {@link #mPreviousViews}).
+ */
+ int mNumActiveViews = 1;
+
+ /**
+ * Array of the children of the {@link AdapterViewAnimator}. This array
+ * is accessed in a circular fashion
+ */
+ View[] mActiveViews;
+
+ /**
+ * List of views pending removal from the {@link AdapterViewAnimator}
+ */
+ ArrayList<View> mPreviousViews;
+
+ /**
+ * The index, relative to the adapter, of the beginning of the window of views
+ */
+ int mCurrentWindowStart = 0;
+
+ /**
+ * The index, relative to the adapter, of the end of the window of views
+ */
+ int mCurrentWindowEnd = -1;
+
+ /**
+ * The same as {@link #mCurrentWindowStart}, except when the we have bounded
+ * {@link #mCurrentWindowStart} to be non-negative
+ */
+ int mCurrentWindowStartUnbounded = 0;
+
+ /**
+ * Indicates whether to treat the adapter to be a circular structure, ie.
+ * the view before 0 is considered to be <code>mAdapter.getCount() - 1</code>
+ *
+ * TODO: this doesn't do anything yet
+ *
+ */
+ boolean mCycleViews = false;
+
+ /**
+ * Handler to post events to the main thread
+ */
+ Handler mMainQueue;
+
+ /**
+ * Listens for data changes from the adapter
+ */
AdapterDataSetObserver mDataSetObserver;
- View mPreviousView;
- View mCurrentView;
+ /**
+ * The {@link Adapter} for this {@link AdapterViewAnimator}
+ */
+ Adapter mAdapter;
+
+ /**
+ * The {@link RemoteViewsAdapter} for this {@link AdapterViewAnimator}
+ */
+ RemoteViewsAdapter mRemoteViewsAdapter;
+
+ /**
+ * Specifies whether this is the first time the animator is showing views
+ */
+ boolean mFirstTime = true;
+ /**
+ * TODO: Animation stuff is still in flux, waiting on the new framework to settle a bit.
+ */
Animation mInAnimation;
Animation mOutAnimation;
- Adapter mAdapter;
- RemoteViewsAdapter mRemoteViewsAdapter;
- private Handler mMainQueue;
+ private ArrayList<View> mViewsToBringToFront;
public AdapterViewAnimator(Context context) {
super(context);
@@ -61,8 +146,10 @@ public class AdapterViewAnimator extends AdapterView<Adapter> implements RemoteV
public AdapterViewAnimator(Context context, AttributeSet attrs) {
super(context, attrs);
- TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ViewAnimator);
- int resource = a.getResourceId(com.android.internal.R.styleable.ViewAnimator_inAnimation, 0);
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.ViewAnimator);
+ int resource = a.getResourceId(
+ com.android.internal.R.styleable.ViewAnimator_inAnimation, 0);
if (resource > 0) {
setInAnimation(context, resource);
}
@@ -72,7 +159,8 @@ public class AdapterViewAnimator extends AdapterView<Adapter> implements RemoteV
setOutAnimation(context, resource);
}
- boolean flag = a.getBoolean(com.android.internal.R.styleable.ViewAnimator_animateFirstView, true);
+ boolean flag = a.getBoolean(
+ com.android.internal.R.styleable.ViewAnimator_animateFirstView, true);
setAnimateFirstView(flag);
a.recycle();
@@ -85,6 +173,54 @@ public class AdapterViewAnimator extends AdapterView<Adapter> implements RemoteV
*/
private void initViewAnimator(Context context, AttributeSet attrs) {
mMainQueue = new Handler(Looper.myLooper());
+ mActiveViews = new View[mNumActiveViews];
+ mPreviousViews = new ArrayList<View>();
+ mViewsToBringToFront = new ArrayList<View>();
+ }
+
+ /**
+ * This method is used by subclasses to configure the animator to display the
+ * desired number of views, and specify the offset
+ *
+ * @param numVisibleViews The number of views the animator keeps in the {@link ViewGroup}
+ * @param activeOffset This parameter specifies where the current index ({@link mWhichChild})
+ * sits within the window. For example if activeOffset is 1, and numVisibleViews is 3,
+ * and {@link setDisplayedChild} is called with 10, then the effective window will be
+ * the indexes 9, 10, and 11. In the same example, if activeOffset were 0, then the
+ * window would instead contain indexes 10, 11 and 12.
+ */
+ void configureViewAnimator(int numVisibleViews, int activeOffset) {
+ if (activeOffset > numVisibleViews - 1) {
+ // Throw an exception here.
+ }
+ mNumActiveViews = numVisibleViews;
+ mActiveOffset = activeOffset;
+ mActiveViews = new View[mNumActiveViews];
+ mPreviousViews.clear();
+ removeAllViewsInLayout();
+ mCurrentWindowStart = 0;
+ mCurrentWindowEnd = -1;
+ }
+
+ /**
+ * This class should be overridden by subclasses to customize view transitions within
+ * the set of visible views
+ *
+ * @param fromIndex The relative index within the window that the view was in, -1 if it wasn't
+ * in the window
+ * @param toIndex The relative index within the window that the view is going to, -1 if it is
+ * being removed
+ * @param view The view that is being animated
+ */
+ void animateViewForTransition(int fromIndex, int toIndex, View view) {
+ PropertyAnimator pa;
+ if (fromIndex == -1) {
+ pa = new PropertyAnimator(400, view, "alpha", 0.0f, 1.0f);
+ pa.start();
+ } else if (toIndex == -1) {
+ pa = new PropertyAnimator(400, view, "alpha", 1.0f, 0.0f);
+ pa.start();
+ }
}
/**
@@ -114,18 +250,28 @@ public class AdapterViewAnimator extends AdapterView<Adapter> implements RemoteV
/**
* Return default inAnimation. To be overriden by subclasses.
*/
- public Animation getDefaultInAnimation() {
+ Animation getDefaultInAnimation() {
return null;
}
/**
- * Return default outAnimation. To be overriden by subclasses.
+ * Return default outAnimation. To be overridden by subclasses.
*/
- public Animation getDefaultOutAnimation() {
+ Animation getDefaultOutAnimation() {
return null;
}
/**
+ * To be overridden by subclasses. This method applies a view / index specific
+ * transform to the child view.
+ *
+ * @param child
+ * @param relativeIndex
+ */
+ void applyTransformForChildAtIndex(View child, int relativeIndex) {
+ }
+
+ /**
* Returns the index of the currently displayed child view.
*/
public int getDisplayedChild() {
@@ -160,70 +306,137 @@ public class AdapterViewAnimator extends AdapterView<Adapter> implements RemoteV
showOnly(childIndex, animate, false);
}
- private LayoutParams makeLayoutParams() {
- int width = mMeasuredWidth - mPaddingLeft - mPaddingRight;
- int height = mMeasuredHeight - mPaddingTop - mPaddingBottom;
- return new LayoutParams(width, height);
+ private int modulo(int pos, int size) {
+ return (size + (pos % size)) % size;
}
- protected void showOnly(int childIndex, boolean animate, boolean onLayout) {
- if (mAdapter != null) {
- // The previous view should be removed from the ViewGroup
- if (mPreviousView != null) {
- mPreviousView.clearAnimation();
+ /**
+ * Get the view at this index relative to the current window's start
+ *
+ * @param relativeIndex Position relative to the current window's start
+ * @return View at this index, null if the index is outside the bounds
+ */
+ View getViewAtRelativeIndex(int relativeIndex) {
+ if (relativeIndex >= 0 && relativeIndex <= mNumActiveViews - 1) {
+ int index = mCurrentWindowStartUnbounded + relativeIndex;
+ return mActiveViews[modulo(index, mNumActiveViews)];
+ }
+ return null;
+ }
- // TODO: this is where we would store the the view for
- // recycling
- removeViewInLayout(mPreviousView);
- }
+ private LayoutParams createOrReuseLayoutParams(View v) {
+ final LayoutParams currentLp = (LayoutParams) v.getLayoutParams();
+ if (currentLp instanceof LayoutParams) {
+ return currentLp;
+ }
+ return new LayoutParams(v);
+ }
- // If the current view is still being animated, we should
- // force the animation to end
- if (mCurrentView != null) {
- mCurrentView.clearAnimation();
- }
+ void showOnly(int childIndex, boolean animate, boolean onLayout) {
+ if (mAdapter == null) return;
- // load the new mCurrentView from our adapter
- mPreviousView = mCurrentView;
- mCurrentView = mAdapter.getView(childIndex, null, this);
- if (mPreviousView != mCurrentView) {
- addViewInLayout(mCurrentView, 0, makeLayoutParams(), true);
- mCurrentView.bringToFront();
+ for (int i = 0; i < mPreviousViews.size(); i++) {
+ View viewToRemove = mPreviousViews.get(i);
+ viewToRemove.clearAnimation();
+ // applyTransformForChildAtIndex here just allows for any cleanup
+ // associated with this view that may need to be done by a subclass
+ applyTransformForChildAtIndex(viewToRemove, -1);
+ removeViewInLayout(viewToRemove);
+ }
+ mPreviousViews.clear();
+ int newWindowStartUnbounded = childIndex - mActiveOffset;
+ int newWindowEndUnbounded = newWindowStartUnbounded + mNumActiveViews - 1;
+ int newWindowStart = Math.max(0, newWindowStartUnbounded);
+ int newWindowEnd = Math.min(mAdapter.getCount(), newWindowEndUnbounded);
+
+ // This section clears out any items that are in our mActiveViews list
+ // but are outside the effective bounds of our window (this is becomes an issue
+ // at the extremities of the list, eg. where newWindowStartUnbounded < 0 or
+ // newWindowEndUnbounded > mAdapter.getCount() - 1
+ for (int i = newWindowStartUnbounded; i < newWindowEndUnbounded; i++) {
+ if (i < newWindowStart || i > newWindowEnd) {
+ int index = modulo(i, mNumActiveViews);
+ if (mActiveViews[index] != null) {
+ View previousView = mActiveViews[index];
+ mPreviousViews.add(previousView);
+ int previousViewRelativeIndex = modulo(index - mCurrentWindowStart,
+ mNumActiveViews);
+ animateViewForTransition(previousViewRelativeIndex, -1, previousView);
+ }
}
+ }
+ // If the window has changed
+ if (! (newWindowStart == mCurrentWindowStart && newWindowEnd == mCurrentWindowEnd)) {
+ // Run through the indices in the new range
+ for (int i = newWindowStart; i <= newWindowEnd; i++) {
+
+ int oldRelativeIndex = i - mCurrentWindowStartUnbounded;
+ int newRelativeIndex = i - newWindowStartUnbounded;
+ int index = modulo(i, mNumActiveViews);
+
+ // If this item is in the current window, great, we just need to apply
+ // the transform for it's new relative position in the window, and animate
+ // between it's current and new relative positions
+ if (i >= mCurrentWindowStart && i <= mCurrentWindowEnd) {
+ View view = mActiveViews[index];
+ applyTransformForChildAtIndex(view, newRelativeIndex);
+ animateViewForTransition(oldRelativeIndex, newRelativeIndex, view);
+
+ // Otherwise this view is new, so first we have to displace the view that's
+ // taking the new view's place within our cache (a circular array)
+ } else {
+ if (mActiveViews[index] != null) {
+ View previousView = mActiveViews[index];
+ mPreviousViews.add(previousView);
+ int previousViewRelativeIndex = modulo(index - mCurrentWindowStart,
+ mNumActiveViews);
+ animateViewForTransition(previousViewRelativeIndex, -1, previousView);
+
+ if (mCurrentWindowStart > newWindowStart) {
+ mViewsToBringToFront.add(previousView);
+ }
+ }
-
- // Animate as necessary
- if (mPreviousView != null && mPreviousView != mCurrentView) {
- if (animate && mOutAnimation != null) {
- mPreviousView.startAnimation(mOutAnimation);
+ // We've cleared a spot for the new view. Get it from the adapter, add it
+ // and apply any transform / animation
+ View newView = mAdapter.getView(i, null, this);
+ if (newView != null) {
+ mActiveViews[index] = newView;
+ addViewInLayout(newView, -1, createOrReuseLayoutParams(newView));
+ applyTransformForChildAtIndex(newView, newRelativeIndex);
+ animateViewForTransition(-1, newRelativeIndex, newView);
+ }
}
- // This line results in the view becoming invisible *after*
- // the above animation is complete, or, if there is no animation
- // then it becomes invisble immediately
- mPreviousView.setVisibility(View.GONE);
+ mActiveViews[index].bringToFront();
}
- if (mCurrentView != null && animate && mInAnimation != null) {
- mCurrentView.startAnimation(mInAnimation);
+ for (int i = 0; i < mViewsToBringToFront.size(); i++) {
+ View v = mViewsToBringToFront.get(i);
+ v.bringToFront();
}
+ mViewsToBringToFront.clear();
- mFirstTime = false;
- if (!onLayout) {
- requestLayout();
- invalidate();
- } else {
- // If the Adapter tries to layout the current view when we get it using getView above
- // the layout will end up being ignored since we are currently laying out, so
- // we post a delayed requestLayout and invalidate
- mMainQueue.post(new Runnable() {
- @Override
- public void run() {
- mCurrentView.requestLayout();
- mCurrentView.invalidate();
- }
- });
- }
+ mCurrentWindowStart = newWindowStart;
+ mCurrentWindowEnd = newWindowEnd;
+ mCurrentWindowStartUnbounded = newWindowStartUnbounded;
+ }
+
+ mFirstTime = false;
+ if (!onLayout) {
+ requestLayout();
+ invalidate();
+ } else {
+ // If the Adapter tries to layout the current view when we get it using getView
+ // above the layout will end up being ignored since we are currently laying out, so
+ // we post a delayed requestLayout and invalidate
+ mMainQueue.post(new Runnable() {
+ @Override
+ public void run() {
+ requestLayout();
+ invalidate();
+ }
+ });
}
}
@@ -247,10 +460,11 @@ public class AdapterViewAnimator extends AdapterView<Adapter> implements RemoteV
int childRight = mPaddingLeft + child.getMeasuredWidth();
int childBottom = mPaddingTop + child.getMeasuredHeight();
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
- child.layout(mPaddingLeft, mPaddingTop, childRight, childBottom);
+ child.layout(mPaddingLeft + lp.horizontalOffset, mPaddingTop + lp.verticalOffset,
+ childRight + lp.horizontalOffset, childBottom + lp.verticalOffset);
}
-
mDataChanged = false;
}
@@ -261,8 +475,6 @@ public class AdapterViewAnimator extends AdapterView<Adapter> implements RemoteV
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
- Log.v(TAG, "onMeasure");
-
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
@@ -278,7 +490,6 @@ public class AdapterViewAnimator extends AdapterView<Adapter> implements RemoteV
child.measure(childWidthMeasureSpec, childheightMeasureSpec);
}
-
setMeasuredDimension(widthSpecSize, heightSpecSize);
}
@@ -302,7 +513,7 @@ public class AdapterViewAnimator extends AdapterView<Adapter> implements RemoteV
* @see #getDisplayedChild()
*/
public View getCurrentView() {
- return mCurrentView;
+ return getViewAtRelativeIndex(mActiveOffset);
}
/**
@@ -412,12 +623,15 @@ public class AdapterViewAnimator extends AdapterView<Adapter> implements RemoteV
mDataSetObserver = new AdapterDataSetObserver();
mAdapter.registerDataSetObserver(mDataSetObserver);
}
+ setFocusable(true);
}
/**
- * Sets up this AdapterViewAnimator to use a remote views adapter which connects to a RemoteViewsService
- * through the specified intent.
- * @param intent the intent used to identify the RemoteViewsService for the adapter to connect to.
+ * Sets up this AdapterViewAnimator to use a remote views adapter which connects to a
+ * RemoteViewsService through the specified intent.
+ *
+ * @param intent the intent used to identify the RemoteViewsService for the adapter to
+ * connect to.
*/
@android.view.RemotableViewMethod
public void setRemoteViewsAdapter(Intent intent) {
@@ -431,7 +645,7 @@ public class AdapterViewAnimator extends AdapterView<Adapter> implements RemoteV
@Override
public View getSelectedView() {
- return mCurrentView;
+ return getViewAtRelativeIndex(mActiveOffset);
}
/**
@@ -452,4 +666,61 @@ public class AdapterViewAnimator extends AdapterView<Adapter> implements RemoteV
setAdapter(mRemoteViewsAdapter);
}
}
+
+ static class LayoutParams extends ViewGroup.LayoutParams {
+ int horizontalOffset;
+ int verticalOffset;
+ View mView;
+
+ LayoutParams(View view) {
+ super(0, 0);
+ horizontalOffset = 0;
+ verticalOffset = 0;
+ mView = view;
+ }
+
+ LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+ horizontalOffset = 0;
+ verticalOffset = 0;
+ }
+
+ void setHorizontalOffset(int newHorizontalOffset) {
+ horizontalOffset = newHorizontalOffset;
+ if (mView != null) {
+ mView.requestLayout();
+ mView.invalidate();
+ }
+ }
+
+ private Rect parentRect = new Rect();
+ void invalidateGlobalRegion(View v, Rect r) {
+ View p = v;
+ boolean firstPass = true;
+ parentRect.set(0, 0, 0, 0);
+ while (p.getParent() != null && p.getParent() instanceof View
+ && !parentRect.contains(r)) {
+ if (!firstPass) r.offset(p.getLeft() - p.getScrollX(), p.getTop() - p.getScrollY());
+ firstPass = false;
+ p = (View) p.getParent();
+ parentRect.set(p.getLeft() - p.getScrollX(), p.getTop() - p.getScrollY(),
+ p.getRight() - p.getScrollX(), p.getBottom() - p.getScrollY());
+ }
+ p.invalidate(r.left, r.top, r.right, r.bottom);
+ }
+
+ private Rect invalidateRect = new Rect();
+ // This is public so that PropertyAnimator can access it
+ public void setVerticalOffset(int newVerticalOffset) {
+ int offsetDelta = newVerticalOffset - verticalOffset;
+ verticalOffset = newVerticalOffset;
+ if (mView != null) {
+ mView.requestLayout();
+ int top = Math.min(mView.getTop() + offsetDelta, mView.getTop());
+ int bottom = Math.max(mView.getBottom() + offsetDelta, mView.getBottom());
+ invalidateRect.set(mView.getLeft(), top, mView.getRight(), bottom);
+ invalidateGlobalRegion(mView, invalidateRect);
+ }
+ }
+ }
}
diff --git a/core/java/android/widget/RemoteViewsAdapter.java b/core/java/android/widget/RemoteViewsAdapter.java
index 52635e8..ebf5d6e 100644
--- a/core/java/android/widget/RemoteViewsAdapter.java
+++ b/core/java/android/widget/RemoteViewsAdapter.java
@@ -433,6 +433,7 @@ public class RemoteViewsAdapter extends BaseAdapter {
int cacheIndex = getCacheIndex(position);
FrameLayout flipper = mViewCache[cacheIndex].flipper;
flipper.setVisibility(View.VISIBLE);
+ flipper.setAlpha(1.0f);
if (indexInfo == null) {
// hide the item view and show the loading view
diff --git a/core/java/android/widget/StackView.java b/core/java/android/widget/StackView.java
new file mode 100644
index 0000000..4cd44d9
--- /dev/null
+++ b/core/java/android/widget/StackView.java
@@ -0,0 +1,487 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import java.util.WeakHashMap;
+
+import android.animation.PropertyAnimator;
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.widget.RemoteViews.RemoteView;
+
+@RemoteView
+/**
+ * A view that displays its children in a stack and allows users to discretely swipe
+ * through the children.
+ */
+public class StackView extends AdapterViewAnimator {
+ private final String TAG = "StackView";
+
+ /**
+ * Default animation parameters
+ */
+ private final int DEFAULT_ANIMATION_DURATION = 400;
+ private final int MINIMUM_ANIMATION_DURATION = 50;
+
+ /**
+ * These specify the different gesture states
+ */
+ private final int GESTURE_NONE = 0;
+ private final int GESTURE_SLIDE_UP = 1;
+ private final int GESTURE_SLIDE_DOWN = 2;
+
+ /**
+ * Specifies how far you need to swipe (up or down) before it
+ * will be consider a completed gesture when you lift your finger
+ */
+ private final float SWIPE_THRESHOLD_RATIO = 0.35f;
+ private final float SLIDE_UP_RATIO = 0.7f;
+
+ private final WeakHashMap<View, Float> mRotations = new WeakHashMap<View, Float>();
+ private final WeakHashMap<View, Integer>
+ mChildrenToApplyTransformsTo = new WeakHashMap<View, Integer>();
+
+ /**
+ * Sentinel value for no current active pointer.
+ * Used by {@link #mActivePointerId}.
+ */
+ private static final int INVALID_POINTER = -1;
+
+ /**
+ * These variables are all related to the current state of touch interaction
+ * with the stack
+ */
+ private boolean mGestureComplete = false;
+ private float mInitialY;
+ private float mInitialX;
+ private int mActivePointerId;
+ private int mYOffset = 0;
+ private int mYVelocity = 0;
+ private int mSwipeGestureType = GESTURE_NONE;
+ private int mViewHeight;
+ private int mSwipeThreshold;
+ private int mTouchSlop;
+ private int mMaximumVelocity;
+ private VelocityTracker mVelocityTracker;
+
+ private boolean mFirstLayoutHappened = false;
+
+ // TODO: temp hack to get this thing started
+ int mIndex = 5;
+
+ public StackView(Context context) {
+ super(context);
+ initStackView();
+ }
+
+ public StackView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initStackView();
+ }
+
+ private void initStackView() {
+ configureViewAnimator(4, 2);
+ setStaticTransformationsEnabled(true);
+ final ViewConfiguration configuration = ViewConfiguration.get(getContext());
+ mTouchSlop = configuration.getScaledTouchSlop();// + 5;
+ mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+ mActivePointerId = INVALID_POINTER;
+ }
+
+ /**
+ * Animate the views between different relative indexes within the {@link AdapterViewAnimator}
+ */
+ void animateViewForTransition(int fromIndex, int toIndex, View view) {
+ if (fromIndex == -1 && toIndex == 0) {
+ // Fade item in
+ if (view.getAlpha() == 1) {
+ view.setAlpha(0);
+ }
+ PropertyAnimator fadeIn = new PropertyAnimator(DEFAULT_ANIMATION_DURATION,
+ view, "alpha", view.getAlpha(), 1.0f);
+ fadeIn.start();
+ } else if (fromIndex == mNumActiveViews - 1 && toIndex == mNumActiveViews - 2) {
+ // Slide item in
+ view.setVisibility(VISIBLE);
+ LayoutParams lp = (LayoutParams) view.getLayoutParams();
+
+ int largestDuration = (int) Math.round(
+ (lp.verticalOffset*1.0f/-mViewHeight)*DEFAULT_ANIMATION_DURATION);
+ int duration = largestDuration;
+ if (mYVelocity != 0) {
+ duration = 1000*(0 - lp.verticalOffset)/Math.abs(mYVelocity);
+ }
+
+ duration = Math.min(duration, largestDuration);
+ duration = Math.max(duration, MINIMUM_ANIMATION_DURATION);
+
+ PropertyAnimator slideDown = new PropertyAnimator(duration, lp,
+ "verticalOffset", lp.verticalOffset, 0);
+ slideDown.start();
+
+ PropertyAnimator fadeIn = new PropertyAnimator(duration, view,
+ "alpha", view.getAlpha(), 1.0f);
+ fadeIn.start();
+ } else if (fromIndex == mNumActiveViews - 2 && toIndex == mNumActiveViews - 1) {
+ // Slide item out
+ LayoutParams lp = (LayoutParams) view.getLayoutParams();
+
+ int largestDuration = (int) Math.round(
+ (1 - (lp.verticalOffset*1.0f/-mViewHeight))*DEFAULT_ANIMATION_DURATION);
+ int duration = largestDuration;
+ if (mYVelocity != 0) {
+ duration = 1000*(lp.verticalOffset + mViewHeight)/Math.abs(mYVelocity);
+ }
+
+ duration = Math.min(duration, largestDuration);
+ duration = Math.max(duration, MINIMUM_ANIMATION_DURATION);
+
+ PropertyAnimator slideUp = new PropertyAnimator(duration, lp,
+ "verticalOffset", lp.verticalOffset, -mViewHeight);
+ slideUp.start();
+
+ PropertyAnimator fadeOut = new PropertyAnimator(duration, view,
+ "alpha", view.getAlpha(), 0.0f);
+ fadeOut.start();
+ } else if (fromIndex == -1 && toIndex == mNumActiveViews - 1) {
+ // Make sure this view that is "waiting in the wings" is invisible
+ view.setAlpha(0.0f);
+ } else if (toIndex == -1) {
+ // Fade item out
+ PropertyAnimator fadeOut = new PropertyAnimator(DEFAULT_ANIMATION_DURATION,
+ view, "alpha", view.getAlpha(), 0);
+ fadeOut.start();
+ }
+ }
+
+ /**
+ * Apply any necessary tranforms for the child that is being added.
+ */
+ void applyTransformForChildAtIndex(View child, int relativeIndex) {
+ float rotation;
+
+ if (!mRotations.containsKey(child)) {
+ rotation = (float) (Math.random()*26 - 13);
+ mRotations.put(child, rotation);
+ } else {
+ rotation = mRotations.get(child);
+ }
+
+ // Child has been removed
+ if (relativeIndex == -1) {
+ if (mRotations.containsKey(child)) {
+ mRotations.remove(child);
+ }
+ if (mChildrenToApplyTransformsTo.containsKey(child)) {
+ mChildrenToApplyTransformsTo.remove(child);
+ }
+ }
+
+ // if this view is already in the layout, we need to
+ // wait until layout has finished in order to set the
+ // pivot point of the rotation (requiring getMeasuredWidth/Height())
+ mChildrenToApplyTransformsTo.put(child, relativeIndex);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ if (!mChildrenToApplyTransformsTo.isEmpty()) {
+ for (View child: mChildrenToApplyTransformsTo.keySet()) {
+ if (mRotations.containsKey(child)) {
+ child.setPivotX(child.getMeasuredWidth()/2);
+ child.setPivotY(child.getMeasuredHeight()/2);
+ child.setRotation(mRotations.get(child));
+ }
+ }
+ mChildrenToApplyTransformsTo.clear();
+ }
+
+ if (!mFirstLayoutHappened) {
+ mViewHeight = (int) Math.round(SLIDE_UP_RATIO*getMeasuredHeight());
+ mSwipeThreshold = (int) Math.round(SWIPE_THRESHOLD_RATIO*mViewHeight);
+
+ // TODO: Right now this walks all the way up the view hierarchy and disables
+ // ClipChildren and ClipToPadding. We're probably going to want to reset
+ // these flags as well.
+ setClipChildren(false);
+ ViewGroup view = this;
+ while (view.getParent() != null && view.getParent() instanceof ViewGroup) {
+ view = (ViewGroup) view.getParent();
+ view.setClipChildren(false);
+ view.setClipToPadding(false);
+ }
+
+ mFirstLayoutHappened = true;
+ }
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ int action = ev.getAction();
+ switch(action & MotionEvent.ACTION_MASK) {
+
+ case MotionEvent.ACTION_DOWN: {
+ if (mActivePointerId == INVALID_POINTER) {
+ mInitialX = ev.getX();
+ mInitialY = ev.getY();
+ mActivePointerId = ev.getPointerId(0);
+ }
+ break;
+ }
+ case MotionEvent.ACTION_MOVE: {
+ int pointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (pointerIndex == INVALID_POINTER) {
+ // no data for our primary pointer, this shouldn't happen, log it
+ Log.d(TAG, "Error: No data for our primary pointer.");
+ return false;
+ }
+
+ float newY = ev.getY(pointerIndex);
+ float deltaY = newY - mInitialY;
+
+ if ((int) Math.abs(deltaY) > mTouchSlop && mSwipeGestureType == GESTURE_NONE) {
+ mSwipeGestureType = deltaY < 0 ? GESTURE_SLIDE_UP : GESTURE_SLIDE_DOWN;
+ mGestureComplete = false;
+ cancelLongPress();
+ requestDisallowInterceptTouchEvent(true);
+ }
+ break;
+ }
+ case MotionEvent.ACTION_POINTER_UP: {
+ onSecondaryPointerUp(ev);
+ break;
+ }
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL: {
+ mActivePointerId = INVALID_POINTER;
+ mSwipeGestureType = GESTURE_NONE;
+ mGestureComplete = true;
+ }
+ }
+
+ return mSwipeGestureType != GESTURE_NONE;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ int action = ev.getAction();
+ int pointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (pointerIndex == INVALID_POINTER) {
+ // no data for our primary pointer, this shouldn't happen, log it
+ Log.d(TAG, "Error: No data for our primary pointer.");
+ return false;
+ }
+
+ float newY = ev.getY(pointerIndex);
+ float deltaY = newY - mInitialY;
+
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ mVelocityTracker.addMovement(ev);
+
+ switch (action & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_MOVE: {
+ if ((int) Math.abs(deltaY) > mTouchSlop && mSwipeGestureType == GESTURE_NONE) {
+ mSwipeGestureType = deltaY < 0 ? GESTURE_SLIDE_UP : GESTURE_SLIDE_DOWN;
+ mGestureComplete = false;
+ cancelLongPress();
+ requestDisallowInterceptTouchEvent(true);
+ }
+
+ if (!mGestureComplete) {
+ if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
+ View v = getViewAtRelativeIndex(mNumActiveViews - 1);
+ if (v != null) {
+ // This view is present but hidden, make sure it's visible
+ // if they pull down
+ v.setVisibility(VISIBLE);
+
+ float r = (deltaY-mTouchSlop)*1.0f / (mSwipeThreshold);
+ mYOffset = Math.min(-mViewHeight + (int) Math.round(
+ r*mSwipeThreshold) - mTouchSlop, 0);
+ LayoutParams lp = (LayoutParams) v.getLayoutParams();
+ lp.setVerticalOffset(mYOffset);
+
+ float alpha = Math.max(0.0f, 1.0f - (1.0f*mYOffset/-mViewHeight));
+ alpha = Math.min(1.0f, alpha);
+ v.setAlpha(alpha);
+ }
+ return true;
+ } else if (mSwipeGestureType == GESTURE_SLIDE_UP) {
+ View v = getViewAtRelativeIndex(mNumActiveViews - 2);
+
+ if (v != null) {
+ float r = -(deltaY*1.0f + mTouchSlop) / (mSwipeThreshold);
+ mYOffset = Math.min((int) Math.round(r*-mSwipeThreshold), 0);
+ LayoutParams lp = (LayoutParams) v.getLayoutParams();
+ lp.setVerticalOffset(mYOffset);
+
+ float alpha = Math.max(0.0f, 1.0f - (1.0f*mYOffset/-mViewHeight));
+ alpha = Math.min(1.0f, alpha);
+ v.setAlpha(alpha);
+ }
+ return true;
+ }
+ }
+ break;
+ }
+ case MotionEvent.ACTION_UP: {
+ handlePointerUp(ev);
+ break;
+ }
+ case MotionEvent.ACTION_POINTER_UP: {
+ onSecondaryPointerUp(ev);
+ break;
+ }
+ case MotionEvent.ACTION_CANCEL: {
+ mActivePointerId = INVALID_POINTER;
+ mGestureComplete = true;
+ mSwipeGestureType = GESTURE_NONE;
+ mYOffset = 0;
+ break;
+ }
+ }
+ return true;
+ }
+
+ private final Rect touchRect = new Rect();
+ private void onSecondaryPointerUp(MotionEvent ev) {
+ final int activePointerIndex = ev.getActionIndex();
+ final int pointerId = ev.getPointerId(activePointerIndex);
+ if (pointerId == mActivePointerId) {
+
+ int activeViewIndex = (mSwipeGestureType == GESTURE_SLIDE_DOWN) ? mNumActiveViews - 1
+ : mNumActiveViews - 2;
+
+ View v = getViewAtRelativeIndex(activeViewIndex);
+ if (v == null) return;
+
+ // Our primary pointer has gone up -- let's see if we can find
+ // another pointer on the view. If so, then we should replace
+ // our primary pointer with this new pointer and adjust things
+ // so that the view doesn't jump
+ for (int index = 0; index < ev.getPointerCount(); index++) {
+ if (index != activePointerIndex) {
+
+ float x = ev.getX(index);
+ float y = ev.getY(index);
+
+ touchRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
+ if (touchRect.contains((int) Math.round(x), (int) Math.round(y))) {
+ float oldX = ev.getX(activePointerIndex);
+ float oldY = ev.getY(activePointerIndex);
+
+ // adjust our frame of reference to avoid a jump
+ mInitialY += (y - oldY);
+ mInitialX += (x - oldX);
+
+ mActivePointerId = ev.getPointerId(index);
+ if (mVelocityTracker != null) {
+ mVelocityTracker.clear();
+ }
+ // ok, we're good, we found a new pointer which is touching the active view
+ return;
+ }
+ }
+ }
+ // if we made it this far, it means we didn't find a satisfactory new pointer :(,
+ // so end the
+ handlePointerUp(ev);
+ }
+ }
+
+ private void handlePointerUp(MotionEvent ev) {
+ int pointerIndex = ev.findPointerIndex(mActivePointerId);
+ float newY = ev.getY(pointerIndex);
+ int deltaY = (int) (newY - mInitialY);
+
+ mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+ mYVelocity = (int) mVelocityTracker.getYVelocity(mActivePointerId);
+
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+
+ if (deltaY > mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_DOWN &&
+ !mGestureComplete) {
+ // Swipe threshold exceeded, swipe down
+ showNext();
+ } else if (deltaY < -mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_UP &&
+ !mGestureComplete) {
+ // Swipe threshold exceeded, swipe up
+ showPrevious();
+ } else if (mSwipeGestureType == GESTURE_SLIDE_UP && !mGestureComplete) {
+ // Didn't swipe up far enough, snap back down
+ View v = getViewAtRelativeIndex(mNumActiveViews - 2);
+ if (v != null) {
+ // Compute the animation duration based on how far they pulled it up
+ LayoutParams lp = (LayoutParams) v.getLayoutParams();
+ int duration = (int) Math.round(
+ lp.verticalOffset*1.0f/-mViewHeight*DEFAULT_ANIMATION_DURATION);
+ duration = Math.max(MINIMUM_ANIMATION_DURATION, duration);
+
+ // Animate back down
+ PropertyAnimator slideDown = new PropertyAnimator(duration, lp,
+ "verticalOffset", lp.verticalOffset, 0);
+ slideDown.start();
+ PropertyAnimator fadeIn = new PropertyAnimator(duration, v,
+ "alpha",v.getAlpha(), 1.0f);
+ fadeIn.start();
+ }
+ } else if (mSwipeGestureType == GESTURE_SLIDE_DOWN && !mGestureComplete) {
+ // Didn't swipe down far enough, snap back up
+ View v = getViewAtRelativeIndex(mNumActiveViews - 1);
+ if (v != null) {
+ // Compute the animation duration based on how far they pulled it down
+ LayoutParams lp = (LayoutParams) v.getLayoutParams();
+ int duration = (int) Math.round(
+ (1 - lp.verticalOffset*1.0f/-mViewHeight)*DEFAULT_ANIMATION_DURATION);
+ duration = Math.max(MINIMUM_ANIMATION_DURATION, duration);
+
+ // Animate back up
+ PropertyAnimator slideUp = new PropertyAnimator(duration, lp,
+ "verticalOffset", lp.verticalOffset, -mViewHeight);
+ slideUp.start();
+ PropertyAnimator fadeOut = new PropertyAnimator(duration, v,
+ "alpha",v.getAlpha(), 0.0f);
+ fadeOut.start();
+ }
+ }
+
+ mActivePointerId = INVALID_POINTER;
+ mGestureComplete = true;
+ mSwipeGestureType = GESTURE_NONE;
+ mYOffset = 0;
+ }
+
+ @Override
+ public void onRemoteAdapterConnected() {
+ super.onRemoteAdapterConnected();
+ setDisplayedChild(mIndex);
+ }
+}