Add demo for animating RecyclerView actions
Change-Id: Ieb14cfb627663f507c24205d697347a1da389094
diff --git a/samples/Support7Demos/AndroidManifest.xml b/samples/Support7Demos/AndroidManifest.xml
index 0e9ed0b..6f9b237 100644
--- a/samples/Support7Demos/AndroidManifest.xml
+++ b/samples/Support7Demos/AndroidManifest.xml
@@ -175,8 +175,17 @@
<!-- RecyclerView samples -->
<activity android:name=".widget.RecyclerViewActivity"
- android:label="@string/recycler_view"
- android:theme="@style/Theme.AppCompat">
+ android:label="@string/recycler_view"
+ android:theme="@style/Theme.AppCompat">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="com.example.android.supportv7.SAMPLE_CODE" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name=".widget.AnimatedRecyclerView"
+ android:label="@string/animated_recycler_view"
+ android:theme="@style/Theme.AppCompat">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="com.example.android.supportv7.SAMPLE_CODE" />
diff --git a/samples/Support7Demos/res/layout/animated_recycler_view.xml b/samples/Support7Demos/res/layout/animated_recycler_view.xml
new file mode 100644
index 0000000..f927c46
--- /dev/null
+++ b/samples/Support7Demos/res/layout/animated_recycler_view.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <Button
+ android:id="@+id/deleteButton"
+ android:layout_weight=".5"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:onClick="deleteItem"
+ android:text="@string/delete_item"/>
+
+ <Button
+ android:layout_weight=".5"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:onClick="addItem"
+ android:text="@string/add_item"/>
+
+ <Button
+ android:layout_weight=".5"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:onClick="addDeleteItem"
+ android:text="@string/add_delete_item"/>
+
+ <Button
+ android:layout_weight=".5"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:onClick="deleteAddItem"
+ android:text="@string/delete_add_item"/>
+
+ <Button
+ android:layout_weight=".5"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:onClick="d1a2d3"
+ android:text="@string/d1a2d3"/>
+
+ </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/samples/Support7Demos/res/layout/selectable_item.xml b/samples/Support7Demos/res/layout/selectable_item.xml
new file mode 100644
index 0000000..65a23cc
--- /dev/null
+++ b/samples/Support7Demos/res/layout/selectable_item.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ 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"/>
+
+ <TextView
+ android:id="@+id/text"
+ android:layout_width="wrap_content"
+ 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 5852d0a..b836718 100644
--- a/samples/Support7Demos/res/values/strings.xml
+++ b/samples/Support7Demos/res/values/strings.xml
@@ -95,10 +95,17 @@
<string name="sample_media_route_activity_local">Local Playback</string>
<string name="sample_media_route_activity_presentation">Local Playback on Presentation Display</string>
- <string name="recycler_view">RecyclerView</string>
+ <string name="recycler_view">RecyclerView/RecyclerViewActivity</string>
+ <string name="animated_recycler_view">RecyclerView/AnimatedRecyclerView</string>
<string name="linear_layout_manager">Linear Layout Manager</string>
<string name="checkbox_orientation">Horz.</string>
<string name="checkbox_reverse">Rev.</string>
<string name="checkbox_layout_dir">Layout Dir</string>
<string name="checkbox_stack_from_end">Stack From End</string>
+ <string name="add_item">Add</string>
+ <string name="delete_item">Delete</string>
+ <string name="add_delete_item">A+D</string>
+ <string name="delete_add_item">D+A</string>
+ <string name="d1a2d3">d1a2d3</string>
+
</resources>
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/widget/AnimatedRecyclerView.java b/samples/Support7Demos/src/com/example/android/supportv7/widget/AnimatedRecyclerView.java
new file mode 100644
index 0000000..61949ed
--- /dev/null
+++ b/samples/Support7Demos/src/com/example/android/supportv7/widget/AnimatedRecyclerView.java
@@ -0,0 +1,445 @@
+/*
+ * Copyright (C) 2014 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 com.example.android.supportv7.widget;
+
+import com.example.android.supportv7.R;
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v4.view.MenuItemCompat;
+import android.support.v7.widget.RecyclerView;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+public class AnimatedRecyclerView extends Activity {
+
+ private static final int SCROLL_DISTANCE = 80; // dp
+
+ private RecyclerView mRecyclerView;
+
+ private int mNumItemsAdded = 0;
+ ArrayList<String> mItems = new ArrayList<String>();
+ MyAdapter mAdapter;
+
+ static final boolean USE_CUSTOM_ANIMATIONS = false;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.animated_recycler_view);
+
+ ViewGroup container = (ViewGroup) findViewById(R.id.container);
+ mRecyclerView = new RecyclerView(this);
+ mRecyclerView.setLayoutManager(new MyLayoutManager(this));
+ mRecyclerView.setHasFixedSize(true);
+ mRecyclerView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ for (int i = 0; i < 6; ++i) {
+ mItems.add("Item #" + i);
+ }
+ mAdapter = new MyAdapter(mItems);
+ mRecyclerView.setAdapter(mAdapter);
+ container.addView(mRecyclerView);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ MenuItemCompat.setShowAsAction(menu.add("Layout"), MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ mRecyclerView.requestLayout();
+ return super.onOptionsItemSelected(item);
+ }
+
+ public void checkboxClicked(View view) {
+ ViewGroup parent = (ViewGroup) view.getParent();
+ boolean selected = ((CheckBox) view).isChecked();
+ int index = mRecyclerView.indexOfChild(parent);
+ MyViewHolder holder = (MyViewHolder) mRecyclerView.getViewHolderForChildAt(index);
+ mAdapter.selectItem(holder, selected);
+ }
+
+ public void deleteItem(View view) {
+ int numItems = mItems.size();
+ if (numItems > 0) {
+ for (int i = numItems - 1; i >= 0; --i) {
+ final String itemText = mItems.get(i);
+ boolean selected = mAdapter.mSelected.get(itemText);
+ if (selected) {
+ removeAtPosition(i);
+ }
+ }
+ }
+ }
+
+ private String generateNewText() {
+ return "Added Item #" + mNumItemsAdded++;
+ }
+
+ public void d1a2d3(View view) {
+ removeAtPosition(1);
+ addAtPosition(2, "Added Item #" + mNumItemsAdded++);
+ removeAtPosition(3);
+ }
+
+ private void removeAtPosition(int position) {
+ mItems.remove(position);
+ mAdapter.notifyItemRemoved(position);
+ }
+
+ private void addAtPosition(int position, String text) {
+ mItems.add(position, text);
+ mAdapter.mSelected.put(text, Boolean.FALSE);
+ mAdapter.notifyItemInserted(position);
+ }
+
+ public void addDeleteItem(View view) {
+ addItem(view);
+ deleteItem(view);
+ }
+
+ public void deleteAddItem(View view) {
+ deleteItem(view);
+ addItem(view);
+ }
+
+ public void addItem(View view) {
+ addAtPosition(3, "Added Item #" + mNumItemsAdded++);
+ }
+
+ /**
+ * A basic ListView-style LayoutManager.
+ */
+ class MyLayoutManager extends RecyclerView.LayoutManager {
+ private static final String TAG = "MyLayoutManager";
+ private int mFirstPosition;
+ private final int mScrollDistance;
+
+ public MyLayoutManager(Context c) {
+ final DisplayMetrics dm = c.getResources().getDisplayMetrics();
+ mScrollDistance = (int) (SCROLL_DISTANCE * dm.density + 0.5f);
+ }
+
+ @Override
+ public void onLayoutChildren(RecyclerView.Adapter adapter, RecyclerView.Recycler recycler,
+ boolean structureChanged, RecyclerView.State state) {
+ int parentBottom = getHeight() - getPaddingBottom();
+
+ final View oldTopView = getChildCount() > 0 ? getChildAt(0) : null;
+ int oldTop = getPaddingTop();
+ if (oldTopView != null) {
+ oldTop = Math.min(oldTopView.getTop(), oldTop);
+ }
+
+ // Note that we add everything to the scrap, but we do not clean it up;
+ // that is handled by the RecyclerView after this method returns
+ detachAndScrapAttachedViews(recycler);
+
+ int top = oldTop;
+ int bottom = top;
+ final int left = getPaddingLeft();
+ final int right = getWidth() - getPaddingRight();
+
+ int count = adapter.getItemCount();
+ for (int i = 0; mFirstPosition + i < count && top < parentBottom; i++, top = bottom) {
+ View v = recycler.getViewForPosition(adapter, mFirstPosition + i);
+ if (v == null) {
+ continue;
+ }
+
+ RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) v.getLayoutParams();
+ if (!params.isItemRemoved()) {
+ addView(v);
+ }
+ measureChild(v, 0, 0);
+ bottom = top + v.getMeasuredHeight();
+ v.layout(left, top, right, bottom);
+ if (params.isItemRemoved()) {
+ parentBottom += v.getHeight();
+ count += 1;
+ }
+ }
+
+ // Now that we've run a full layout, figure out which views were not used
+ // (cached in previousViews). For each of these views, position it where
+ // it would go, according to its position relative to the visible
+ // positions in the list. This information will be used by RecyclerView to
+ // record post-layout positions of these items for the purposes of animating them
+ // out of view
+
+ View lastVisibleView = getChildAt(getChildCount() - 1);
+ if (lastVisibleView != null) {
+ RecyclerView.LayoutParams lastParams =
+ (RecyclerView.LayoutParams) lastVisibleView.getLayoutParams();
+ int lastPosition = lastParams.getViewPosition();
+ final List<RecyclerView.ViewHolder> previousViews = recycler.getScrapList();
+ count = previousViews.size();
+ for (int i = 0; i < count; ++i) {
+ View view = previousViews.get(i).itemView;
+ RecyclerView.LayoutParams params =
+ (RecyclerView.LayoutParams) view.getLayoutParams();
+ int position = params.getViewPosition();
+ int newTop;
+ if (position < mFirstPosition) {
+ newTop = view.getHeight() * (position - mFirstPosition);
+ } else {
+ newTop = lastVisibleView.getTop() + view.getHeight() *
+ (position - lastPosition);
+ }
+ view.offsetTopAndBottom(newTop - view.getTop());
+ }
+ }
+ }
+
+ @Override
+ public RecyclerView.LayoutParams generateDefaultLayoutParams() {
+ return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ public boolean canScrollVertically() {
+ return true;
+ }
+
+ @Override
+ public int scrollVerticallyBy(int dy, RecyclerView.Adapter adapter,
+ RecyclerView.Recycler recycler) {
+ if (getChildCount() == 0) {
+ return 0;
+ }
+
+ int scrolled = 0;
+ final int left = getPaddingLeft();
+ final int right = getWidth() - getPaddingRight();
+ if (dy < 0) {
+ while (scrolled > dy) {
+ final View topView = getChildAt(0);
+ final int hangingTop = Math.max(-topView.getTop(), 0);
+ final int scrollBy = Math.min(scrolled - dy, hangingTop);
+ scrolled -= scrollBy;
+ offsetChildrenVertical(scrollBy);
+ if (mFirstPosition > 0 && scrolled > dy) {
+ mFirstPosition--;
+ View v = recycler.getViewForPosition(adapter, mFirstPosition);
+ addView(v, 0);
+ measureChild(v, 0, 0);
+ final int bottom = topView.getTop(); // TODO decorated top?
+ final int top = bottom - v.getMeasuredHeight();
+ v.layout(left, top, right, bottom);
+ } else {
+ break;
+ }
+ }
+ } else if (dy > 0) {
+ final int parentHeight = getHeight();
+ while (scrolled < dy) {
+ final View bottomView = getChildAt(getChildCount() - 1);
+ final int hangingBottom = Math.max(bottomView.getBottom() - parentHeight, 0);
+ final int scrollBy = -Math.min(dy - scrolled, hangingBottom);
+ scrolled -= scrollBy;
+ offsetChildrenVertical(scrollBy);
+ if (scrolled < dy && getItemCount() > mFirstPosition + getChildCount()) {
+ View v = recycler.getViewForPosition(adapter,
+ mFirstPosition + getChildCount());
+ final int top = getChildAt(getChildCount() - 1).getBottom();
+ addView(v);
+ measureChild(v, 0, 0);
+ final int bottom = top + v.getMeasuredHeight();
+ v.layout(left, top, right, bottom);
+ } else {
+ break;
+ }
+ }
+ }
+ recycleViewsOutOfBounds(recycler);
+ return scrolled;
+ }
+
+ @Override
+ public View onFocusSearchFailed(View focused, int direction,
+ RecyclerView.Adapter adapter, RecyclerView.Recycler recycler) {
+ final int oldCount = getChildCount();
+
+ if (oldCount == 0) {
+ return null;
+ }
+
+ final int left = getPaddingLeft();
+ final int right = getWidth() - getPaddingRight();
+
+ View toFocus = null;
+ int newViewsHeight = 0;
+ if (direction == View.FOCUS_UP || direction == View.FOCUS_BACKWARD) {
+ while (mFirstPosition > 0 && newViewsHeight < mScrollDistance) {
+ mFirstPosition--;
+ View v = recycler.getViewForPosition(adapter, mFirstPosition);
+ final int bottom = getChildAt(0).getTop(); // TODO decorated top?
+ addView(v, 0);
+ measureChild(v, 0, 0);
+ final int top = bottom - v.getMeasuredHeight();
+ v.layout(left, top, right, bottom);
+ if (v.isFocusable()) {
+ toFocus = v;
+ break;
+ }
+ }
+ }
+ if (direction == View.FOCUS_DOWN || direction == View.FOCUS_FORWARD) {
+ while (mFirstPosition + getChildCount() < getItemCount() &&
+ newViewsHeight < mScrollDistance) {
+ View v = recycler.getViewForPosition(adapter, mFirstPosition + getChildCount());
+ final int top = getChildAt(getChildCount() - 1).getBottom();
+ addView(v);
+ measureChild(v, 0, 0);
+ final int bottom = top + v.getMeasuredHeight();
+ v.layout(left, top, right, bottom);
+ if (v.isFocusable()) {
+ toFocus = v;
+ break;
+ }
+ }
+ }
+
+ return toFocus;
+ }
+
+ public void recycleViewsOutOfBounds(RecyclerView.Recycler recycler) {
+ final int childCount = getChildCount();
+ final int parentWidth = getWidth();
+ final int parentHeight = getHeight();
+ boolean foundFirst = false;
+ int first = 0;
+ int last = 0;
+ for (int i = 0; i < childCount; i++) {
+ final View v = getChildAt(i);
+ if (v.hasFocus() || (v.getRight() >= 0 && v.getLeft() <= parentWidth &&
+ v.getBottom() >= 0 && v.getTop() <= parentHeight)) {
+ if (!foundFirst) {
+ first = i;
+ foundFirst = true;
+ }
+ last = i;
+ }
+ }
+ for (int i = childCount - 1; i > last; i--) {
+ removeAndRecycleViewAt(i, recycler);
+ }
+ for (int i = first - 1; i >= 0; i--) {
+ removeAndRecycleViewAt(i, recycler);
+ }
+ if (getChildCount() == 0) {
+ mFirstPosition = 0;
+ } else {
+ mFirstPosition += first;
+ }
+ }
+
+ @Override
+ public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
+ if (positionStart < mFirstPosition) {
+ mFirstPosition += itemCount;
+ }
+ }
+
+ @Override
+ public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
+ if (positionStart < mFirstPosition) {
+ mFirstPosition -= itemCount;
+ }
+ }
+ }
+
+ class MyAdapter extends RecyclerView.Adapter {
+ private int mBackground;
+ List<String> mData;
+ HashMap<String, Boolean> mSelected = new HashMap<String, Boolean>();
+
+ public MyAdapter(List<String> data) {
+ TypedValue val = new TypedValue();
+ AnimatedRecyclerView.this.getTheme().resolveAttribute(
+ R.attr.selectableItemBackground, val, true);
+ mBackground = val.resourceId;
+ mData = data;
+ for (String itemText : mData) {
+ mSelected.put(itemText, Boolean.FALSE);
+ }
+ }
+
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ MyViewHolder h = new MyViewHolder(getLayoutInflater().inflate(R.layout.selectable_item,
+ null));
+ h.textView.setMinimumHeight(128);
+ h.textView.setFocusable(true);
+ h.textView.setBackgroundResource(mBackground);
+ return h;
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ String itemText = mData.get(position);
+ ((MyViewHolder) holder).textView.setText(itemText);
+ boolean selected = false;
+ if (mSelected.get(itemText) != null) {
+ selected = mSelected.get(itemText);
+ }
+ ((MyViewHolder) holder).checkBox.setChecked(selected);
+ }
+
+ @Override
+ public int getItemCount() {
+ 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(), selected);
+ }
+ }
+
+ static class MyViewHolder extends RecyclerView.ViewHolder {
+ public TextView textView;
+ public CheckBox checkBox;
+
+ public MyViewHolder(View v) {
+ super(v);
+ textView = (TextView) v.findViewById(R.id.text);
+ checkBox = (CheckBox) v.findViewById(R.id.selected);
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + " \"" + textView.getText() + "\"";
+ }
+ }}
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/widget/RecyclerViewActivity.java b/samples/Support7Demos/src/com/example/android/supportv7/widget/RecyclerViewActivity.java
index 223da65..c5ab703 100644
--- a/samples/Support7Demos/src/com/example/android/supportv7/widget/RecyclerViewActivity.java
+++ b/samples/Support7Demos/src/com/example/android/supportv7/widget/RecyclerViewActivity.java
@@ -102,6 +102,12 @@
}
@Override
+ public void onLayoutChildren(RecyclerView.Adapter adapter, RecyclerView.Recycler recycler,
+ boolean structureChanged, RecyclerView.State state) {
+ layoutChildren(adapter, recycler, structureChanged);
+ }
+
+ @Override
public void layoutChildren(RecyclerView.Adapter adapter, RecyclerView.Recycler recycler,
boolean structureChanged) {
final int parentBottom = getHeight() - getPaddingBottom();