diff options
author | Yigit Boyar <yboyar@google.com> | 2015-09-01 14:38:34 -0700 |
---|---|---|
committer | Yigit Boyar <yboyar@google.com> | 2015-09-23 13:12:34 -0700 |
commit | 3d1224e65280d8a933d9085757a8a0ea853a2025 (patch) | |
tree | 77ed2f3d312447f5548c016aebc8c6c6c44717cd | |
parent | 6b73c45db3eed935144e69ca7a603dac128da0e9 (diff) | |
download | android_development-3d1224e65280d8a933d9085757a8a0ea853a2025.tar.gz android_development-3d1224e65280d8a933d9085757a8a0ea853a2025.tar.bz2 android_development-3d1224e65280d8a933d9085757a8a0ea853a2025.zip |
RecyclerView in place animations
Bug: 22507896
Change-Id: I0344348011c412ea00b97eec97fa8d566bb09c7e
5 files changed, 267 insertions, 48 deletions
diff --git a/samples/Support7Demos/res/layout/animated_recycler_view.xml b/samples/Support7Demos/res/layout/animated_recycler_view.xml index 29a23e26a..e5ff03760 100644 --- a/samples/Support7Demos/res/layout/animated_recycler_view.xml +++ b/samples/Support7Demos/res/layout/animated_recycler_view.xml @@ -26,9 +26,9 @@ android:layout_height="wrap_content"/> <CheckBox - android:id="@+id/enableChangeAnimations" + android:id="@+id/enableInPlaceChange" android:checked="false" - android:text="@string/enableChangeAnimations" + android:text="@string/enableInPlaceChange" android:layout_width="wrap_content" android:layout_height="wrap_content"/> diff --git a/samples/Support7Demos/res/layout/selectable_item.xml b/samples/Support7Demos/res/layout/selectable_item.xml index 3cab6fb0c..90aa080e0 100644 --- a/samples/Support7Demos/res/layout/selectable_item.xml +++ b/samples/Support7Demos/res/layout/selectable_item.xml @@ -5,27 +5,13 @@ android:onClick="itemClicked" android:layout_width="match_parent" android:layout_height="match_parent"> - <CheckBox android:id="@+id/selected" android:onClick="checkboxClicked" android:layout_width="wrap_content" android:layout_height="wrap_content"/> - - <LinearLayout - android:orientation="vertical" + <TextView + android:id="@+id/text" android:layout_width="match_parent" - android:layout_height="wrap_content"> - <TextView - android:id="@+id/text" - android:layout_width="match_parent" - android:layout_height="wrap_content"/> - - <TextView - android:id="@+id/expandedText" - android:visibility="gone" - android:layout_width="match_parent" - android:layout_height="wrap_content"/> - </LinearLayout> - + android:layout_height="wrap_content"/> </LinearLayout>
\ No newline at end of file diff --git a/samples/Support7Demos/res/values/strings.xml b/samples/Support7Demos/res/values/strings.xml index accf739d1..85c1303c2 100644 --- a/samples/Support7Demos/res/values/strings.xml +++ b/samples/Support7Demos/res/values/strings.xml @@ -155,7 +155,7 @@ <string name="checkbox_stack_from_end">Stack From End</string> <string name="enableAnimations">Animate</string> <string name="enablePredictiveAnimations">Predictive</string> - <string name="enableChangeAnimations">Change Anims</string> + <string name="enableInPlaceChange">In Place Change</string> <string name="add_item">Add</string> <string name="delete_item">Del</string> <string name="add_delete_item">A+D</string> diff --git a/samples/Support7Demos/src/com/example/android/supportv7/widget/AnimatedRecyclerView.java b/samples/Support7Demos/src/com/example/android/supportv7/widget/AnimatedRecyclerView.java index af5c6532d..6714f6d02 100644 --- a/samples/Support7Demos/src/com/example/android/supportv7/widget/AnimatedRecyclerView.java +++ b/samples/Support7Demos/src/com/example/android/supportv7/widget/AnimatedRecyclerView.java @@ -15,13 +15,20 @@ */ package com.example.android.supportv7.widget; -import android.support.v4.util.ArrayMap; -import android.widget.CompoundButton; import com.example.android.supportv7.R; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; +import android.os.Build; import android.os.Bundle; +import android.support.v4.util.ArrayMap; import android.support.v4.view.MenuItemCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.ViewPropertyAnimatorListener; +import android.support.v7.widget.DefaultItemAnimator; import android.support.v7.widget.RecyclerView; import android.util.DisplayMetrics; import android.util.TypedValue; @@ -30,10 +37,10 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; +import android.widget.CompoundButton; import android.widget.TextView; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; public class AnimatedRecyclerView extends Activity { @@ -49,6 +56,7 @@ public class AnimatedRecyclerView extends Activity { boolean mAnimationsEnabled = true; boolean mPredictiveAnimationsEnabled = true; RecyclerView.ItemAnimator mCachedAnimator = null; + boolean mEnableInPlaceChange = true; @Override protected void onCreate(Bundle savedInstanceState) { @@ -57,7 +65,9 @@ public class AnimatedRecyclerView extends Activity { ViewGroup container = (ViewGroup) findViewById(R.id.container); mRecyclerView = new RecyclerView(this); - mCachedAnimator = mRecyclerView.getItemAnimator(); + mCachedAnimator = createAnimator(); + mCachedAnimator.setChangeDuration(2000); + mRecyclerView.setItemAnimator(mCachedAnimator); mRecyclerView.setLayoutManager(new MyLayoutManager(this)); mRecyclerView.setHasFixedSize(true); mRecyclerView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, @@ -91,14 +101,241 @@ public class AnimatedRecyclerView extends Activity { } }); - CheckBox enableChangeAnimations = - (CheckBox) findViewById(R.id.enableChangeAnimations); - enableChangeAnimations.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + CheckBox enableInPlaceChange = (CheckBox) findViewById(R.id.enableInPlaceChange); + enableInPlaceChange.setChecked(mEnableInPlaceChange); + enableInPlaceChange.setOnCheckedChangeListener( + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + mEnableInPlaceChange = isChecked; + } + }); + } + + private RecyclerView.ItemAnimator createAnimator() { + return new DefaultItemAnimator() { + List<ItemChangeAnimator> mPendingChangeAnimations = new ArrayList<>(); + ArrayMap<RecyclerView.ViewHolder, ItemChangeAnimator> mRunningAnimations + = new ArrayMap<>(); + ArrayMap<MyViewHolder, Long> mPendingSettleList = new ArrayMap<>(); + @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - mCachedAnimator.setSupportsChangeAnimations(isChecked); + public void runPendingAnimations() { + super.runPendingAnimations(); + for (ItemChangeAnimator anim : mPendingChangeAnimations) { + anim.start(); + mRunningAnimations.put(anim.mViewHolder, anim); + } + mPendingChangeAnimations.clear(); + for (int i = mPendingSettleList.size() - 1; i >=0; i--) { + final MyViewHolder vh = mPendingSettleList.keyAt(i); + final long duration = mPendingSettleList.valueAt(i); + ViewCompat.animate(vh.textView).translationX(0f).alpha(1f) + .setDuration(duration).setListener( + new ViewPropertyAnimatorListener() { + @Override + public void onAnimationStart(View view) { + dispatchAnimationStarted(vh); + } + + @Override + public void onAnimationEnd(View view) { + ViewCompat.setTranslationX(vh.textView, 0f); + ViewCompat.setAlpha(vh.textView, 1f); + dispatchAnimationFinished(vh); + } + + @Override + public void onAnimationCancel(View view) { + + } + }).start(); + } + mPendingSettleList.clear(); } - }); + + @Override + public ItemHolderInfo recordPreLayoutInformation(RecyclerView.State state, + RecyclerView.ViewHolder viewHolder, + @AdapterChanges int changeFlags, List<Object> payloads) { + MyItemInfo info = (MyItemInfo) super + .recordPreLayoutInformation(state, viewHolder, changeFlags, payloads); + info.text = ((MyViewHolder) viewHolder).textView.getText(); + return info; + } + + @Override + public ItemHolderInfo recordPostLayoutInformation(RecyclerView.State state, + RecyclerView.ViewHolder viewHolder) { + MyItemInfo info = (MyItemInfo) super.recordPostLayoutInformation(state, viewHolder); + info.text = ((MyViewHolder) viewHolder).textView.getText(); + return info; + } + + + @Override + public boolean canReuseUpdatedViewHolder(RecyclerView.ViewHolder viewHolder) { + return mEnableInPlaceChange; + } + + @Override + public void endAnimation(RecyclerView.ViewHolder item) { + super.endAnimation(item); + for (int i = mPendingChangeAnimations.size() - 1; i >= 0; i--) { + ItemChangeAnimator anim = mPendingChangeAnimations.get(i); + if (anim.mViewHolder == item) { + mPendingChangeAnimations.remove(i); + anim.setFraction(1f); + dispatchChangeFinished(item, true); + } + } + for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { + ItemChangeAnimator animator = mRunningAnimations.get(item); + if (animator != null) { + animator.end(); + mRunningAnimations.removeAt(i); + } + } + for (int i = mPendingSettleList.size() - 1; i >= 0; i--) { + final MyViewHolder vh = mPendingSettleList.keyAt(i); + if (vh == item) { + mPendingSettleList.removeAt(i); + dispatchChangeFinished(item, true); + } + } + } + + @Override + public boolean animateChange(RecyclerView.ViewHolder oldHolder, + RecyclerView.ViewHolder newHolder, ItemHolderInfo preInfo, + ItemHolderInfo postInfo) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR1 + || oldHolder != newHolder) { + return super.animateChange(oldHolder, newHolder, preInfo, postInfo); + } + return animateChangeApiHoneycombMr1(oldHolder, newHolder, preInfo, postInfo); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) + private boolean animateChangeApiHoneycombMr1(RecyclerView.ViewHolder oldHolder, + RecyclerView.ViewHolder newHolder, + ItemHolderInfo preInfo, ItemHolderInfo postInfo) { + endAnimation(oldHolder); + MyItemInfo pre = (MyItemInfo) preInfo; + MyItemInfo post = (MyItemInfo) postInfo; + MyViewHolder vh = (MyViewHolder) oldHolder; + + CharSequence finalText = post.text; + + if (pre.text.equals(post.text)) { + // same content. Just translate back to 0 + final long duration = (long) (getChangeDuration() + * (ViewCompat.getTranslationX(vh.textView) / vh.textView.getWidth())); + mPendingSettleList.put(vh, duration); + // we set it here because previous endAnimation would set it to other value. + vh.textView.setText(finalText); + } else { + // different content, get out and come back. + vh.textView.setText(pre.text); + final ItemChangeAnimator anim = new ItemChangeAnimator(vh, finalText, + getChangeDuration()) { + @Override + public void onAnimationEnd(Animator animation) { + setFraction(1f); + dispatchChangeFinished(mViewHolder, true); + } + + @Override + public void onAnimationStart(Animator animation) { + dispatchChangeStarting(mViewHolder, true); + } + }; + mPendingChangeAnimations.add(anim); + } + return true; + } + + @Override + public ItemHolderInfo obtainHolderInfo() { + return new MyItemInfo(); + } + }; + } + + + @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) + abstract private static class ItemChangeAnimator implements + ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener { + CharSequence mFinalText; + ValueAnimator mValueAnimator; + MyViewHolder mViewHolder; + final float mMaxX; + final float mStartRatio; + public ItemChangeAnimator(MyViewHolder viewHolder, CharSequence finalText, long duration) { + mViewHolder = viewHolder; + mMaxX = mViewHolder.itemView.getWidth(); + mStartRatio = ViewCompat.getTranslationX(mViewHolder.textView) / mMaxX; + mFinalText = finalText; + mValueAnimator = ValueAnimator.ofFloat(0f, 1f); + mValueAnimator.addUpdateListener(this); + mValueAnimator.addListener(this); + mValueAnimator.setDuration(duration); + mValueAnimator.setTarget(mViewHolder.itemView); + } + + void setFraction(float fraction) { + fraction = mStartRatio + (1f - mStartRatio) * fraction; + if (fraction < .5f) { + ViewCompat.setTranslationX(mViewHolder.textView, fraction * mMaxX); + ViewCompat.setAlpha(mViewHolder.textView, 1f - fraction); + } else { + ViewCompat.setTranslationX(mViewHolder.textView, (1f - fraction) * mMaxX); + ViewCompat.setAlpha(mViewHolder.textView, fraction); + maybeSetFinalText(); + } + } + + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + setFraction(valueAnimator.getAnimatedFraction()); + } + + public void start() { + mValueAnimator.start(); + } + + @Override + public void onAnimationEnd(Animator animation) { + maybeSetFinalText(); + ViewCompat.setAlpha(mViewHolder.textView, 1f); + } + + public void maybeSetFinalText() { + if (mFinalText != null) { + mViewHolder.textView.setText(mFinalText); + mFinalText = null; + } + } + + public void end() { + mValueAnimator.cancel(); + } + + @Override + public void onAnimationStart(Animator animation) { + } + + @Override + public void onAnimationCancel(Animator animation) { + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + } + + private static class MyItemInfo extends DefaultItemAnimator.ItemHolderInfo { + CharSequence text; } @Override @@ -114,6 +351,7 @@ public class AnimatedRecyclerView extends Activity { return super.onOptionsItemSelected(item); } + @SuppressWarnings("unused") public void checkboxClicked(View view) { ViewGroup parent = (ViewGroup) view.getParent(); boolean selected = ((CheckBox) view).isChecked(); @@ -121,6 +359,7 @@ public class AnimatedRecyclerView extends Activity { mAdapter.selectItem(holder, selected); } + @SuppressWarnings("unused") public void itemClicked(View view) { ViewGroup parent = (ViewGroup) view; MyViewHolder holder = (MyViewHolder) mRecyclerView.getChildViewHolder(parent); @@ -462,20 +701,19 @@ public class AnimatedRecyclerView extends Activity { @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { String itemText = mData.get(position); - ((MyViewHolder) holder).textView.setText(itemText); - ((MyViewHolder) holder).expandedText.setText("More text for the expanded version"); + MyViewHolder myViewHolder = (MyViewHolder) holder; + myViewHolder.boundText = itemText; + myViewHolder.textView.setText(itemText); boolean selected = false; if (mSelected.get(itemText) != null) { selected = mSelected.get(itemText); } - ((MyViewHolder) holder).checkBox.setChecked(selected); + myViewHolder.checkBox.setChecked(selected); Boolean expanded = mExpanded.get(itemText); - if (expanded != null && expanded) { - ((MyViewHolder) holder).expandedText.setVisibility(View.VISIBLE); - ((MyViewHolder) holder).textView.setVisibility(View.GONE); + if (Boolean.TRUE.equals(expanded)) { + myViewHolder.textView.setText("More text for the expanded version"); } else { - ((MyViewHolder) holder).expandedText.setVisibility(View.GONE); - ((MyViewHolder) holder).textView.setVisibility(View.VISIBLE); + myViewHolder.textView.setText(itemText); } } @@ -484,28 +722,22 @@ public class AnimatedRecyclerView extends Activity { return mData.size(); } - public void selectItem(String itemText, boolean selected) { - mSelected.put(itemText, selected); - } - public void selectItem(MyViewHolder holder, boolean selected) { - mSelected.put((String) holder.textView.getText().toString(), selected); + mSelected.put(holder.boundText, selected); } public void toggleExpanded(MyViewHolder holder) { - String text = (String) holder.textView.getText(); - mExpanded.put(text, !mExpanded.get(text)); + mExpanded.put(holder.boundText, !mExpanded.get(holder.boundText)); } } static class MyViewHolder extends RecyclerView.ViewHolder { - public TextView expandedText; public TextView textView; public CheckBox checkBox; + public String boundText; public MyViewHolder(View v) { super(v); - expandedText = (TextView) v.findViewById(R.id.expandedText); textView = (TextView) v.findViewById(R.id.text); checkBox = (CheckBox) v.findViewById(R.id.selected); } diff --git a/samples/Support7Demos/src/com/example/android/supportv7/widget/BaseLayoutManagerActivity.java b/samples/Support7Demos/src/com/example/android/supportv7/widget/BaseLayoutManagerActivity.java index 782da523b..002a5745f 100644 --- a/samples/Support7Demos/src/com/example/android/supportv7/widget/BaseLayoutManagerActivity.java +++ b/samples/Support7Demos/src/com/example/android/supportv7/widget/BaseLayoutManagerActivity.java @@ -25,6 +25,7 @@ import com.example.android.supportv7.widget.util.ConfigViewHolder; import android.app.Activity; import android.content.Context; import android.os.Bundle; +import android.support.v7.widget.DefaultItemAnimator; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; @@ -69,7 +70,7 @@ abstract public class BaseLayoutManagerActivity<T extends RecyclerView.LayoutMan mLayoutManager = createLayoutManager(); mRecyclerView.setLayoutManager(mLayoutManager); mRecyclerView.setAdapter(createAdapter()); - mRecyclerView.getItemAnimator().setSupportsChangeAnimations(true); + ((DefaultItemAnimator)mRecyclerView.getItemAnimator()).setSupportsChangeAnimations(true); onRecyclerViewInit(mRecyclerView); } |