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