diff options
-rw-r--r-- | api/current.xml | 83 | ||||
-rw-r--r-- | core/java/android/widget/AdapterViewAnimator.java | 419 | ||||
-rw-r--r-- | core/java/android/widget/RemoteViewsAdapter.java | 1 | ||||
-rw-r--r-- | core/java/android/widget/StackView.java | 487 |
4 files changed, 865 insertions, 125 deletions
diff --git a/api/current.xml b/api/current.xml index ba2de24..53d2fdd 100644 --- a/api/current.xml +++ b/api/current.xml @@ -213325,17 +213325,6 @@ visibility="public" > </method> -<method name="getVisibleTitleHeight" - return="int" - abstract="false" - native="false" - synchronized="false" - static="false" - final="false" - deprecated="not deprecated" - visibility="public" -> -</method> <method name="getZoomControls" return="android.view.View" abstract="false" @@ -216659,7 +216648,7 @@ </interface> <class name="AdapterViewAnimator" extends="android.widget.AdapterView" - abstract="false" + abstract="true" static="false" final="false" deprecated="not deprecated" @@ -216709,28 +216698,6 @@ visibility="public" > </method> -<method name="getDefaultInAnimation" - return="android.view.animation.Animation" - abstract="false" - native="false" - synchronized="false" - static="false" - final="false" - deprecated="not deprecated" - visibility="public" -> -</method> -<method name="getDefaultOutAnimation" - return="android.view.animation.Animation" - abstract="false" - native="false" - synchronized="false" - static="false" - final="false" - deprecated="not deprecated" - visibility="public" -> -</method> <method name="getDisplayedChild" return="int" abstract="false" @@ -216929,23 +216896,6 @@ visibility="public" > </method> -<method name="showOnly" - return="void" - abstract="false" - native="false" - synchronized="false" - static="false" - final="false" - deprecated="not deprecated" - visibility="protected" -> -<parameter name="childIndex" type="int"> -</parameter> -<parameter name="animate" type="boolean"> -</parameter> -<parameter name="onLayout" type="boolean"> -</parameter> -</method> <method name="showPrevious" return="void" abstract="false" @@ -230267,6 +230217,37 @@ </parameter> </method> </interface> +<class name="StackView" + extends="android.widget.AdapterViewAnimator" + abstract="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<constructor name="StackView" + type="android.widget.StackView" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="context" type="android.content.Context"> +</parameter> +</constructor> +<constructor name="StackView" + type="android.widget.StackView" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="context" type="android.content.Context"> +</parameter> +<parameter name="attrs" type="android.util.AttributeSet"> +</parameter> +</constructor> +</class> <class name="TabHost" extends="android.widget.FrameLayout" abstract="false" 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); + } +} |