/* * 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() + "\""; } } }