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