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