1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.example.android.supportv7.widget;
17 
18 import android.animation.Animator;
19 import android.animation.AnimatorListenerAdapter;
20 import android.animation.ValueAnimator;
21 import android.app.Activity;
22 import android.content.Context;
23 import android.os.Bundle;
24 import android.util.DisplayMetrics;
25 import android.util.TypedValue;
26 import android.view.Menu;
27 import android.view.MenuItem;
28 import android.view.View;
29 import android.view.ViewGroup;
30 import android.widget.CheckBox;
31 import android.widget.CompoundButton;
32 import android.widget.TextView;
33 
34 import androidx.collection.ArrayMap;
35 import androidx.recyclerview.widget.DefaultItemAnimator;
36 import androidx.recyclerview.widget.RecyclerView;
37 
38 import com.example.android.supportv7.R;
39 
40 import java.util.ArrayList;
41 import java.util.List;
42 
43 public class AnimatedRecyclerView extends Activity {
44 
45     private static final int SCROLL_DISTANCE = 80; // dp
46 
47     private RecyclerView mRecyclerView;
48 
49     private int mNumItemsAdded = 0;
50     ArrayList<String> mItems = new ArrayList<String>();
51     MyAdapter mAdapter;
52 
53     boolean mAnimationsEnabled = true;
54     boolean mPredictiveAnimationsEnabled = true;
55     RecyclerView.ItemAnimator mCachedAnimator = null;
56     boolean mEnableInPlaceChange = true;
57 
58     @Override
onCreate(Bundle savedInstanceState)59     protected void onCreate(Bundle savedInstanceState) {
60         super.onCreate(savedInstanceState);
61         setContentView(R.layout.animated_recycler_view);
62 
63         ViewGroup container = findViewById(R.id.container);
64         mRecyclerView = new RecyclerView(this);
65         mCachedAnimator = createAnimator();
66         mCachedAnimator.setChangeDuration(2000);
67         mRecyclerView.setItemAnimator(mCachedAnimator);
68         mRecyclerView.setLayoutManager(new MyLayoutManager(this));
69         mRecyclerView.setHasFixedSize(true);
70         mRecyclerView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
71                 ViewGroup.LayoutParams.MATCH_PARENT));
72         for (int i = 0; i < 6; ++i) {
73             mItems.add("Item #" + i);
74         }
75         mAdapter = new MyAdapter(mItems);
76         mRecyclerView.setAdapter(mAdapter);
77         container.addView(mRecyclerView);
78 
79         CheckBox enableAnimations = findViewById(R.id.enableAnimations);
80         enableAnimations.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
81             @Override
82             public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
83                 if (isChecked && mRecyclerView.getItemAnimator() == null) {
84                     mRecyclerView.setItemAnimator(mCachedAnimator);
85                 } else if (!isChecked && mRecyclerView.getItemAnimator() != null) {
86                     mRecyclerView.setItemAnimator(null);
87                 }
88                 mAnimationsEnabled = isChecked;
89             }
90         });
91 
92         CheckBox enablePredictiveAnimations =
93                 findViewById(R.id.enablePredictiveAnimations);
94         enablePredictiveAnimations.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
95             @Override
96             public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
97                 mPredictiveAnimationsEnabled = isChecked;
98             }
99         });
100 
101         CheckBox enableInPlaceChange = findViewById(R.id.enableInPlaceChange);
102         enableInPlaceChange.setChecked(mEnableInPlaceChange);
103         enableInPlaceChange.setOnCheckedChangeListener(
104                 new CompoundButton.OnCheckedChangeListener() {
105                     @Override
106                     public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
107                         mEnableInPlaceChange = isChecked;
108                     }
109                 });
110     }
111 
createAnimator()112     private RecyclerView.ItemAnimator createAnimator() {
113         return new DefaultItemAnimator() {
114             List<ItemChangeAnimator> mPendingChangeAnimations = new ArrayList<>();
115             ArrayMap<RecyclerView.ViewHolder, ItemChangeAnimator> mRunningAnimations
116                     = new ArrayMap<>();
117             ArrayMap<MyViewHolder, Long> mPendingSettleList = new ArrayMap<>();
118 
119             @Override
120             public void runPendingAnimations() {
121                 super.runPendingAnimations();
122                 for (ItemChangeAnimator anim : mPendingChangeAnimations) {
123                     anim.start();
124                     mRunningAnimations.put(anim.mViewHolder, anim);
125                 }
126                 mPendingChangeAnimations.clear();
127                 for (int i = mPendingSettleList.size() - 1; i >=0; i--) {
128                     final MyViewHolder vh = mPendingSettleList.keyAt(i);
129                     final long duration = mPendingSettleList.valueAt(i);
130                     vh.textView.animate().translationX(0f).alpha(1f)
131                             .setDuration(duration).setListener(
132                                     new AnimatorListenerAdapter() {
133                                         @Override
134                                         public void onAnimationStart(Animator animator) {
135                                             dispatchAnimationStarted(vh);
136                                         }
137 
138                                         @Override
139                                         public void onAnimationEnd(Animator animator) {
140                                             vh.textView.setTranslationX(0f);
141                                             vh.textView.setAlpha(1f);
142                                             dispatchAnimationFinished(vh);
143                                         }
144 
145                                         @Override
146                                         public void onAnimationCancel(Animator animator) {
147 
148                                         }
149                                     }).start();
150                 }
151                 mPendingSettleList.clear();
152             }
153 
154             @Override
155             public ItemHolderInfo recordPreLayoutInformation(RecyclerView.State state,
156                     RecyclerView.ViewHolder viewHolder,
157                     @AdapterChanges int changeFlags, List<Object> payloads) {
158                 MyItemInfo info = (MyItemInfo) super
159                         .recordPreLayoutInformation(state, viewHolder, changeFlags, payloads);
160                 info.text = ((MyViewHolder) viewHolder).textView.getText();
161                 return info;
162             }
163 
164             @Override
165             public ItemHolderInfo recordPostLayoutInformation(RecyclerView.State state,
166                     RecyclerView.ViewHolder viewHolder) {
167                 MyItemInfo info = (MyItemInfo) super.recordPostLayoutInformation(state, viewHolder);
168                 info.text = ((MyViewHolder) viewHolder).textView.getText();
169                 return info;
170             }
171 
172 
173             @Override
174             public boolean canReuseUpdatedViewHolder(RecyclerView.ViewHolder viewHolder) {
175                 return mEnableInPlaceChange;
176             }
177 
178             @Override
179             public void endAnimation(RecyclerView.ViewHolder item) {
180                 super.endAnimation(item);
181                 for (int i = mPendingChangeAnimations.size() - 1; i >= 0; i--) {
182                     ItemChangeAnimator anim = mPendingChangeAnimations.get(i);
183                     if (anim.mViewHolder == item) {
184                         mPendingChangeAnimations.remove(i);
185                         anim.setFraction(1f);
186                         dispatchChangeFinished(item, true);
187                     }
188                 }
189                 for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
190                     ItemChangeAnimator animator = mRunningAnimations.get(item);
191                     if (animator != null) {
192                         animator.end();
193                         mRunningAnimations.removeAt(i);
194                     }
195                 }
196                 for (int  i = mPendingSettleList.size() - 1; i >= 0; i--) {
197                     final MyViewHolder vh = mPendingSettleList.keyAt(i);
198                     if (vh == item) {
199                         mPendingSettleList.removeAt(i);
200                         dispatchChangeFinished(item, true);
201                     }
202                 }
203             }
204 
205             @Override
206             public boolean animateChange(RecyclerView.ViewHolder oldHolder,
207                     RecyclerView.ViewHolder newHolder, ItemHolderInfo preInfo,
208                     ItemHolderInfo postInfo) {
209                 if (oldHolder != newHolder) {
210                     return super.animateChange(oldHolder, newHolder, preInfo, postInfo);
211                 }
212                 return animateChangeApiHoneycombMr1(oldHolder, newHolder, preInfo, postInfo);
213             }
214 
215             private boolean animateChangeApiHoneycombMr1(RecyclerView.ViewHolder oldHolder,
216                     RecyclerView.ViewHolder newHolder,
217                     ItemHolderInfo preInfo, ItemHolderInfo postInfo) {
218                 endAnimation(oldHolder);
219                 MyItemInfo pre = (MyItemInfo) preInfo;
220                 MyItemInfo post = (MyItemInfo) postInfo;
221                 MyViewHolder vh = (MyViewHolder) oldHolder;
222 
223                 CharSequence finalText = post.text;
224 
225                 if (pre.text.equals(post.text)) {
226                     // same content. Just translate back to 0
227                     final long duration = (long) (getChangeDuration()
228                             * (vh.textView.getTranslationX() / vh.textView.getWidth()));
229                     mPendingSettleList.put(vh, duration);
230                     // we set it here because previous endAnimation would set it to other value.
231                     vh.textView.setText(finalText);
232                 } else {
233                     // different content, get out and come back.
234                     vh.textView.setText(pre.text);
235                     final ItemChangeAnimator anim = new ItemChangeAnimator(vh, finalText,
236                             getChangeDuration()) {
237                         @Override
238                         public void onAnimationEnd(Animator animation) {
239                             setFraction(1f);
240                             dispatchChangeFinished(mViewHolder, true);
241                         }
242 
243                         @Override
244                         public void onAnimationStart(Animator animation) {
245                             dispatchChangeStarting(mViewHolder, true);
246                         }
247                     };
248                     mPendingChangeAnimations.add(anim);
249                 }
250                 return true;
251             }
252 
253             @Override
254             public ItemHolderInfo obtainHolderInfo() {
255                 return new MyItemInfo();
256             }
257         };
258     }
259 
260     abstract private static class ItemChangeAnimator implements
261             ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener {
262         CharSequence mFinalText;
263         ValueAnimator mValueAnimator;
264         MyViewHolder mViewHolder;
265         final float mMaxX;
266         final float mStartRatio;
ItemChangeAnimator(MyViewHolder viewHolder, CharSequence finalText, long duration)267         public ItemChangeAnimator(MyViewHolder viewHolder, CharSequence finalText, long duration) {
268             mViewHolder = viewHolder;
269             mMaxX = mViewHolder.itemView.getWidth();
270             mStartRatio = mViewHolder.textView.getTranslationX() / mMaxX;
271             mFinalText = finalText;
272             mValueAnimator = ValueAnimator.ofFloat(0f, 1f);
273             mValueAnimator.addUpdateListener(this);
274             mValueAnimator.addListener(this);
275             mValueAnimator.setDuration(duration);
276             mValueAnimator.setTarget(mViewHolder.itemView);
277         }
278 
setFraction(float fraction)279         void setFraction(float fraction) {
280             fraction = mStartRatio + (1f - mStartRatio) * fraction;
281             if (fraction < .5f) {
282                 mViewHolder.textView.setTranslationX(fraction * mMaxX);
283                 mViewHolder.textView.setAlpha(1f - fraction);
284             } else {
285                 mViewHolder.textView.setTranslationX((1f - fraction) * mMaxX);
286                 mViewHolder.textView.setAlpha(fraction);
287                 maybeSetFinalText();
288             }
289         }
290 
291         @Override
onAnimationUpdate(ValueAnimator valueAnimator)292         public void onAnimationUpdate(ValueAnimator valueAnimator) {
293             setFraction(valueAnimator.getAnimatedFraction());
294         }
295 
start()296         public void start() {
297             mValueAnimator.start();
298         }
299 
300         @Override
onAnimationEnd(Animator animation)301         public void onAnimationEnd(Animator animation) {
302             maybeSetFinalText();
303             mViewHolder.textView.setAlpha(1f);
304         }
305 
maybeSetFinalText()306         public void maybeSetFinalText() {
307             if (mFinalText != null) {
308                 mViewHolder.textView.setText(mFinalText);
309                 mFinalText = null;
310             }
311         }
312 
end()313         public void end() {
314             mValueAnimator.cancel();
315         }
316 
317         @Override
onAnimationStart(Animator animation)318         public void onAnimationStart(Animator animation) {
319         }
320 
321         @Override
onAnimationCancel(Animator animation)322         public void onAnimationCancel(Animator animation) {
323         }
324 
325         @Override
onAnimationRepeat(Animator animation)326         public void onAnimationRepeat(Animator animation) {
327         }
328     }
329 
330     private static class MyItemInfo extends DefaultItemAnimator.ItemHolderInfo {
331         CharSequence text;
332     }
333 
334     @Override
onCreateOptionsMenu(Menu menu)335     public boolean onCreateOptionsMenu(Menu menu) {
336         super.onCreateOptionsMenu(menu);
337         menu.add("Layout").setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
338         return true;
339     }
340 
341     @Override
onOptionsItemSelected(MenuItem item)342     public boolean onOptionsItemSelected(MenuItem item) {
343         mRecyclerView.requestLayout();
344         return super.onOptionsItemSelected(item);
345     }
346 
347     @SuppressWarnings("unused")
checkboxClicked(View view)348     public void checkboxClicked(View view) {
349         ViewGroup parent = (ViewGroup) view.getParent();
350         boolean selected = ((CheckBox) view).isChecked();
351         MyViewHolder holder = (MyViewHolder) mRecyclerView.getChildViewHolder(parent);
352         mAdapter.selectItem(holder, selected);
353     }
354 
355     @SuppressWarnings("unused")
itemClicked(View view)356     public void itemClicked(View view) {
357         ViewGroup parent = (ViewGroup) view;
358         MyViewHolder holder = (MyViewHolder) mRecyclerView.getChildViewHolder(parent);
359         final int position = holder.getAdapterPosition();
360         if (position == RecyclerView.NO_POSITION) {
361             return;
362         }
363         mAdapter.toggleExpanded(holder);
364         mAdapter.notifyItemChanged(position);
365     }
366 
deleteSelectedItems(View view)367     public void deleteSelectedItems(View view) {
368         int numItems = mItems.size();
369         if (numItems > 0) {
370             for (int i = numItems - 1; i >= 0; --i) {
371                 final String itemText = mItems.get(i);
372                 boolean selected = mAdapter.mSelected.get(itemText);
373                 if (selected) {
374                     removeAtPosition(i);
375                 }
376             }
377         }
378     }
379 
generateNewText()380     private String generateNewText() {
381         return "Added Item #" + mNumItemsAdded++;
382     }
383 
d1a2d3(View view)384     public void d1a2d3(View view) {
385         removeAtPosition(1);
386         addAtPosition(2, "Added Item #" + mNumItemsAdded++);
387         removeAtPosition(3);
388     }
389 
removeAtPosition(int position)390     private void removeAtPosition(int position) {
391         if(position < mItems.size()) {
392             mItems.remove(position);
393             mAdapter.notifyItemRemoved(position);
394         }
395     }
396 
addAtPosition(int position, String text)397     private void addAtPosition(int position, String text) {
398         if (position > mItems.size()) {
399             position = mItems.size();
400         }
401         mItems.add(position, text);
402         mAdapter.mSelected.put(text, Boolean.FALSE);
403         mAdapter.mExpanded.put(text, Boolean.FALSE);
404         mAdapter.notifyItemInserted(position);
405     }
406 
addDeleteItem(View view)407     public void addDeleteItem(View view) {
408         addItem(view);
409         deleteSelectedItems(view);
410     }
411 
deleteAddItem(View view)412     public void deleteAddItem(View view) {
413         deleteSelectedItems(view);
414         addItem(view);
415     }
416 
addItem(View view)417     public void addItem(View view) {
418         addAtPosition(3, "Added Item #" + mNumItemsAdded++);
419     }
420 
421     /**
422      * A basic ListView-style LayoutManager.
423      */
424     class MyLayoutManager extends RecyclerView.LayoutManager {
425         private static final String TAG = "MyLayoutManager";
426         private int mFirstPosition;
427         private final int mScrollDistance;
428 
MyLayoutManager(Context c)429         public MyLayoutManager(Context c) {
430             final DisplayMetrics dm = c.getResources().getDisplayMetrics();
431             mScrollDistance = (int) (SCROLL_DISTANCE * dm.density + 0.5f);
432         }
433 
434         @Override
supportsPredictiveItemAnimations()435         public boolean supportsPredictiveItemAnimations() {
436             return mPredictiveAnimationsEnabled;
437         }
438 
439         @Override
onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)440         public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
441             int parentBottom = getHeight() - getPaddingBottom();
442 
443             final View oldTopView = getChildCount() > 0 ? getChildAt(0) : null;
444             int oldTop = getPaddingTop();
445             if (oldTopView != null) {
446                 oldTop = Math.min(oldTopView.getTop(), oldTop);
447             }
448 
449             // Note that we add everything to the scrap, but we do not clean it up;
450             // that is handled by the RecyclerView after this method returns
451             detachAndScrapAttachedViews(recycler);
452 
453             int top = oldTop;
454             int bottom = top;
455             final int left = getPaddingLeft();
456             final int right = getWidth() - getPaddingRight();
457 
458             int count = state.getItemCount();
459             for (int i = 0; mFirstPosition + i < count && top < parentBottom; i++, top = bottom) {
460                 View v = recycler.getViewForPosition(mFirstPosition + i);
461 
462                 RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) v.getLayoutParams();
463                 addView(v);
464                 measureChild(v, 0, 0);
465                 bottom = top + v.getMeasuredHeight();
466                 v.layout(left, top, right, bottom);
467                 if (mPredictiveAnimationsEnabled && params.isItemRemoved()) {
468                     parentBottom += v.getHeight();
469                 }
470             }
471 
472             if (mAnimationsEnabled && mPredictiveAnimationsEnabled && !state.isPreLayout()) {
473                 // Now that we've run a full layout, figure out which views were not used
474                 // (cached in previousViews). For each of these views, position it where
475                 // it would go, according to its position relative to the visible
476                 // positions in the list. This information will be used by RecyclerView to
477                 // record post-layout positions of these items for the purposes of animating them
478                 // out of view
479 
480                 View lastVisibleView = getChildAt(getChildCount() - 1);
481                 if (lastVisibleView != null) {
482                     RecyclerView.LayoutParams lastParams =
483                             (RecyclerView.LayoutParams) lastVisibleView.getLayoutParams();
484                     int lastPosition = lastParams.getViewLayoutPosition();
485                     final List<RecyclerView.ViewHolder> previousViews = recycler.getScrapList();
486                     count = previousViews.size();
487                     for (int i = 0; i < count; ++i) {
488                         View view = previousViews.get(i).itemView;
489                         RecyclerView.LayoutParams params =
490                                 (RecyclerView.LayoutParams) view.getLayoutParams();
491                         if (params.isItemRemoved()) {
492                             continue;
493                         }
494                         int position = params.getViewLayoutPosition();
495                         int newTop;
496                         if (position < mFirstPosition) {
497                             newTop = view.getHeight() * (position - mFirstPosition);
498                         } else {
499                             newTop = lastVisibleView.getTop() + view.getHeight() *
500                                     (position - lastPosition);
501                         }
502                         view.offsetTopAndBottom(newTop - view.getTop());
503                     }
504                 }
505             }
506         }
507 
508         @Override
generateDefaultLayoutParams()509         public RecyclerView.LayoutParams generateDefaultLayoutParams() {
510             return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
511                     ViewGroup.LayoutParams.WRAP_CONTENT);
512         }
513 
514         @Override
canScrollVertically()515         public boolean canScrollVertically() {
516             return true;
517         }
518 
519         @Override
scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state)520         public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
521                 RecyclerView.State state) {
522             if (getChildCount() == 0) {
523                 return 0;
524             }
525 
526             int scrolled = 0;
527             final int left = getPaddingLeft();
528             final int right = getWidth() - getPaddingRight();
529             if (dy < 0) {
530                 while (scrolled > dy) {
531                     final View topView = getChildAt(0);
532                     final int hangingTop = Math.max(-topView.getTop(), 0);
533                     final int scrollBy = Math.min(scrolled - dy, hangingTop);
534                     scrolled -= scrollBy;
535                     offsetChildrenVertical(scrollBy);
536                     if (mFirstPosition > 0 && scrolled > dy) {
537                         mFirstPosition--;
538                         View v = recycler.getViewForPosition(mFirstPosition);
539                         addView(v, 0);
540                         measureChild(v, 0, 0);
541                         final int bottom = topView.getTop(); // TODO decorated top?
542                         final int top = bottom - v.getMeasuredHeight();
543                         v.layout(left, top, right, bottom);
544                     } else {
545                         break;
546                     }
547                 }
548             } else if (dy > 0) {
549                 final int parentHeight = getHeight();
550                 while (scrolled < dy) {
551                     final View bottomView = getChildAt(getChildCount() - 1);
552                     final int hangingBottom = Math.max(bottomView.getBottom() - parentHeight, 0);
553                     final int scrollBy = -Math.min(dy - scrolled, hangingBottom);
554                     scrolled -= scrollBy;
555                     offsetChildrenVertical(scrollBy);
556                     if (scrolled < dy && state.getItemCount() > mFirstPosition + getChildCount()) {
557                         View v = recycler.getViewForPosition(mFirstPosition + getChildCount());
558                         final int top = getChildAt(getChildCount() - 1).getBottom();
559                         addView(v);
560                         measureChild(v, 0, 0);
561                         final int bottom = top + v.getMeasuredHeight();
562                         v.layout(left, top, right, bottom);
563                     } else {
564                         break;
565                     }
566                 }
567             }
568             recycleViewsOutOfBounds(recycler);
569             return scrolled;
570         }
571 
572         @Override
onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler, RecyclerView.State state)573         public View onFocusSearchFailed(View focused, int direction,
574                 RecyclerView.Recycler recycler, RecyclerView.State state) {
575             final int oldCount = getChildCount();
576 
577             if (oldCount == 0) {
578                 return null;
579             }
580 
581             final int left = getPaddingLeft();
582             final int right = getWidth() - getPaddingRight();
583 
584             View toFocus = null;
585             int newViewsHeight = 0;
586             if (direction == View.FOCUS_UP || direction == View.FOCUS_BACKWARD) {
587                 while (mFirstPosition > 0 && newViewsHeight < mScrollDistance) {
588                     mFirstPosition--;
589                     View v = recycler.getViewForPosition(mFirstPosition);
590                     final int bottom = getChildAt(0).getTop(); // TODO decorated top?
591                     addView(v, 0);
592                     measureChild(v, 0, 0);
593                     final int top = bottom - v.getMeasuredHeight();
594                     v.layout(left, top, right, bottom);
595                     if (v.isFocusable()) {
596                         toFocus = v;
597                         break;
598                     }
599                 }
600             }
601             if (direction == View.FOCUS_DOWN || direction == View.FOCUS_FORWARD) {
602                 while (mFirstPosition + getChildCount() < state.getItemCount() &&
603                         newViewsHeight < mScrollDistance) {
604                     View v = recycler.getViewForPosition(mFirstPosition + getChildCount());
605                     final int top = getChildAt(getChildCount() - 1).getBottom();
606                     addView(v);
607                     measureChild(v, 0, 0);
608                     final int bottom = top + v.getMeasuredHeight();
609                     v.layout(left, top, right, bottom);
610                     if (v.isFocusable()) {
611                         toFocus = v;
612                         break;
613                     }
614                 }
615             }
616 
617             return toFocus;
618         }
619 
recycleViewsOutOfBounds(RecyclerView.Recycler recycler)620         public void recycleViewsOutOfBounds(RecyclerView.Recycler recycler) {
621             final int childCount = getChildCount();
622             final int parentWidth = getWidth();
623             final int parentHeight = getHeight();
624             boolean foundFirst = false;
625             int first = 0;
626             int last = 0;
627             for (int i = 0; i < childCount; i++) {
628                 final View v = getChildAt(i);
629                 if (v.hasFocus() || (v.getRight() >= 0 && v.getLeft() <= parentWidth &&
630                         v.getBottom() >= 0 && v.getTop() <= parentHeight)) {
631                     if (!foundFirst) {
632                         first = i;
633                         foundFirst = true;
634                     }
635                     last = i;
636                 }
637             }
638             for (int i = childCount - 1; i > last; i--) {
639                 removeAndRecycleViewAt(i, recycler);
640             }
641             for (int i = first - 1; i >= 0; i--) {
642                 removeAndRecycleViewAt(i, recycler);
643             }
644             if (getChildCount() == 0) {
645                 mFirstPosition = 0;
646             } else {
647                 mFirstPosition += first;
648             }
649         }
650 
651         @Override
onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount)652         public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
653             if (positionStart < mFirstPosition) {
654                 mFirstPosition += itemCount;
655             }
656         }
657 
658         @Override
onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount)659         public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
660             if (positionStart < mFirstPosition) {
661                 mFirstPosition -= itemCount;
662             }
663         }
664     }
665 
666     class MyAdapter extends RecyclerView.Adapter {
667         private int mBackground;
668         List<String> mData;
669         ArrayMap<String, Boolean> mSelected = new ArrayMap<String, Boolean>();
670         ArrayMap<String, Boolean> mExpanded = new ArrayMap<String, Boolean>();
671 
MyAdapter(List<String> data)672         public MyAdapter(List<String> data) {
673             TypedValue val = new TypedValue();
674             AnimatedRecyclerView.this.getTheme().resolveAttribute(
675                     R.attr.selectableItemBackground, val, true);
676             mBackground = val.resourceId;
677             mData = data;
678             for (String itemText : mData) {
679                 mSelected.put(itemText, Boolean.FALSE);
680                 mExpanded.put(itemText, Boolean.FALSE);
681             }
682         }
683 
684         @Override
onCreateViewHolder(ViewGroup parent, int viewType)685         public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
686             MyViewHolder h = new MyViewHolder(getLayoutInflater().inflate(R.layout.selectable_item,
687                     null));
688             h.textView.setMinimumHeight(128);
689             h.textView.setFocusable(true);
690             h.textView.setBackgroundResource(mBackground);
691             return h;
692         }
693 
694         @Override
onBindViewHolder(RecyclerView.ViewHolder holder, int position)695         public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
696             String itemText = mData.get(position);
697             MyViewHolder myViewHolder = (MyViewHolder) holder;
698             myViewHolder.boundText = itemText;
699             myViewHolder.textView.setText(itemText);
700             boolean selected = false;
701             if (mSelected.get(itemText) != null) {
702                 selected = mSelected.get(itemText);
703             }
704             myViewHolder.checkBox.setChecked(selected);
705             Boolean expanded = mExpanded.get(itemText);
706             if (Boolean.TRUE.equals(expanded)) {
707                 myViewHolder.textView.setText("More text for the expanded version");
708             } else {
709                 myViewHolder.textView.setText(itemText);
710             }
711         }
712 
713         @Override
getItemCount()714         public int getItemCount() {
715             return mData.size();
716         }
717 
selectItem(MyViewHolder holder, boolean selected)718         public void selectItem(MyViewHolder holder, boolean selected) {
719             mSelected.put(holder.boundText, selected);
720         }
721 
toggleExpanded(MyViewHolder holder)722         public void toggleExpanded(MyViewHolder holder) {
723             mExpanded.put(holder.boundText, !mExpanded.get(holder.boundText));
724         }
725     }
726 
727     static class MyViewHolder extends RecyclerView.ViewHolder {
728         public TextView textView;
729         public CheckBox checkBox;
730         public String boundText;
731 
MyViewHolder(View v)732         public MyViewHolder(View v) {
733             super(v);
734             textView = (TextView) v.findViewById(R.id.text);
735             checkBox = (CheckBox) v.findViewById(R.id.selected);
736         }
737 
738         @Override
toString()739         public String toString() {
740             return super.toString() + " \"" + textView.getText() + "\"";
741         }
742     }
743 }
744