blob: 6714f6d0256336b344dd62f21b4d1c2c6f90606d [file] [log] [blame]
/*
* 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.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;
import android.view.Menu;
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.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;
boolean mAnimationsEnabled = true;
boolean mPredictiveAnimationsEnabled = true;
RecyclerView.ItemAnimator mCachedAnimator = null;
boolean mEnableInPlaceChange = true;
@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);
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,
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);
CheckBox enableAnimations = (CheckBox) findViewById(R.id.enableAnimations);
enableAnimations.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked && mRecyclerView.getItemAnimator() == null) {
mRecyclerView.setItemAnimator(mCachedAnimator);
} else if (!isChecked && mRecyclerView.getItemAnimator() != null) {
mRecyclerView.setItemAnimator(null);
}
mAnimationsEnabled = isChecked;
}
});
CheckBox enablePredictiveAnimations =
(CheckBox) findViewById(R.id.enablePredictiveAnimations);
enablePredictiveAnimations.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
mPredictiveAnimationsEnabled = isChecked;
}
});
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 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
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);
}
@SuppressWarnings("unused")
public void checkboxClicked(View view) {
ViewGroup parent = (ViewGroup) view.getParent();
boolean selected = ((CheckBox) view).isChecked();
MyViewHolder holder = (MyViewHolder) mRecyclerView.getChildViewHolder(parent);
mAdapter.selectItem(holder, selected);
}
@SuppressWarnings("unused")
public void itemClicked(View view) {
ViewGroup parent = (ViewGroup) view;
MyViewHolder holder = (MyViewHolder) mRecyclerView.getChildViewHolder(parent);
final int position = holder.getAdapterPosition();
if (position == RecyclerView.NO_POSITION) {
return;
}
mAdapter.toggleExpanded(holder);
mAdapter.notifyItemChanged(position);
}
public void deleteSelectedItems(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) {
if(position < mItems.size()) {
mItems.remove(position);
mAdapter.notifyItemRemoved(position);
}
}
private void addAtPosition(int position, String text) {
if (position > mItems.size()) {
position = mItems.size();
}
mItems.add(position, text);
mAdapter.mSelected.put(text, Boolean.FALSE);
mAdapter.mExpanded.put(text, Boolean.FALSE);
mAdapter.notifyItemInserted(position);
}
public void addDeleteItem(View view) {
addItem(view);
deleteSelectedItems(view);
}
public void deleteAddItem(View view) {
deleteSelectedItems(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 boolean supportsPredictiveItemAnimations() {
return mPredictiveAnimationsEnabled;
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, 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 = state.getItemCount();
for (int i = 0; mFirstPosition + i < count && top < parentBottom; i++, top = bottom) {
View v = recycler.getViewForPosition(mFirstPosition + i);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) v.getLayoutParams();
addView(v);
measureChild(v, 0, 0);
bottom = top + v.getMeasuredHeight();
v.layout(left, top, right, bottom);
if (mPredictiveAnimationsEnabled && params.isItemRemoved()) {
parentBottom += v.getHeight();
}
}
if (mAnimationsEnabled && mPredictiveAnimationsEnabled && !state.isPreLayout()) {
// 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.getViewLayoutPosition();
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();
if (params.isItemRemoved()) {
continue;
}
int position = params.getViewLayoutPosition();
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.Recycler recycler,
RecyclerView.State state) {
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(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 && state.getItemCount() > mFirstPosition + getChildCount()) {
View v = recycler.getViewForPosition(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.Recycler recycler, RecyclerView.State state) {
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(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() < state.getItemCount() &&
newViewsHeight < mScrollDistance) {
View v = recycler.getViewForPosition(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;
ArrayMap<String, Boolean> mSelected = new ArrayMap<String, Boolean>();
ArrayMap<String, Boolean> mExpanded = new ArrayMap<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);
mExpanded.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 myViewHolder = (MyViewHolder) holder;
myViewHolder.boundText = itemText;
myViewHolder.textView.setText(itemText);
boolean selected = false;
if (mSelected.get(itemText) != null) {
selected = mSelected.get(itemText);
}
myViewHolder.checkBox.setChecked(selected);
Boolean expanded = mExpanded.get(itemText);
if (Boolean.TRUE.equals(expanded)) {
myViewHolder.textView.setText("More text for the expanded version");
} else {
myViewHolder.textView.setText(itemText);
}
}
@Override
public int getItemCount() {
return mData.size();
}
public void selectItem(MyViewHolder holder, boolean selected) {
mSelected.put(holder.boundText, selected);
}
public void toggleExpanded(MyViewHolder holder) {
mExpanded.put(holder.boundText, !mExpanded.get(holder.boundText));
}
}
static class MyViewHolder extends RecyclerView.ViewHolder {
public TextView textView;
public CheckBox checkBox;
public String boundText;
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() + "\"";
}
}
}