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