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 com.example.android.supportv7.R;
     19 
     20 import android.animation.Animator;
     21 import android.animation.ValueAnimator;
     22 import android.annotation.TargetApi;
     23 import android.app.Activity;
     24 import android.content.Context;
     25 import android.os.Build;
     26 import android.os.Bundle;
     27 import android.support.v4.util.ArrayMap;
     28 import android.support.v4.view.MenuItemCompat;
     29 import android.support.v4.view.ViewCompat;
     30 import android.support.v4.view.ViewPropertyAnimatorListener;
     31 import android.support.v7.widget.DefaultItemAnimator;
     32 import android.support.v7.widget.RecyclerView;
     33 import android.util.DisplayMetrics;
     34 import android.util.TypedValue;
     35 import android.view.Menu;
     36 import android.view.MenuItem;
     37 import android.view.View;
     38 import android.view.ViewGroup;
     39 import android.widget.CheckBox;
     40 import android.widget.CompoundButton;
     41 import android.widget.TextView;
     42 
     43 import java.util.ArrayList;
     44 import java.util.List;
     45 
     46 public 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