Merge "Preliminary implementation of StackView, which extends AdapterViewAnimator."

This commit is contained in:
Adam Cohen
2010-08-10 15:10:59 -07:00
committed by Android (Google) Code Review
4 changed files with 866 additions and 126 deletions

View File

@@ -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"

View File

@@ -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,17 +250,27 @@ 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.
*/
@@ -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);
// Animate as necessary
if (mPreviousView != null && mPreviousView != mCurrentView) {
if (animate && mOutAnimation != null) {
mPreviousView.startAnimation(mOutAnimation);
// 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);
}
// 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);
}
}
if (mCurrentView != null && animate && mInAnimation != null) {
mCurrentView.startAnimation(mInAnimation);
}
// 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++) {
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();
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);
}
}
});
// 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);
}
}
mActiveViews[index].bringToFront();
}
for (int i = 0; i < mViewsToBringToFront.size(); i++) {
View v = mViewsToBringToFront.get(i);
v.bringToFront();
}
mViewsToBringToFront.clear();
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);
}
}
}
}

View File

@@ -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

View File

@@ -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);
}
}