RecyclerView in place animations

Bug: 22507896
Change-Id: I0344348011c412ea00b97eec97fa8d566bb09c7e
diff --git a/samples/Support7Demos/res/layout/animated_recycler_view.xml b/samples/Support7Demos/res/layout/animated_recycler_view.xml
index 29a23e2..e5ff037 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 3cab6fb..90aa080e 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 accf739..85c1303 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 af5c653..6714f6d 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.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 @@
     boolean mAnimationsEnabled = true;
     boolean mPredictiveAnimationsEnabled = true;
     RecyclerView.ItemAnimator mCachedAnimator = null;
+    boolean mEnableInPlaceChange = true;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -57,7 +65,9 @@
 
         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 @@
             }
         });
 
-        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 @@
         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 @@
         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 @@
         @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 @@
             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 782da52..002a574 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 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 @@
         mLayoutManager = createLayoutManager();
         mRecyclerView.setLayoutManager(mLayoutManager);
         mRecyclerView.setAdapter(createAdapter());
-        mRecyclerView.getItemAnimator().setSupportsChangeAnimations(true);
+        ((DefaultItemAnimator)mRecyclerView.getItemAnimator()).setSupportsChangeAnimations(true);
         onRecyclerViewInit(mRecyclerView);
     }