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.support.v4.util.ArrayMap;
     19 import android.widget.CompoundButton;
     20 import com.example.android.supportv7.R;
     21 import android.app.Activity;
     22 import android.content.Context;
     23 import android.os.Bundle;
     24 import android.support.v4.view.MenuItemCompat;
     25 import android.support.v7.widget.RecyclerView;
     26 import android.util.DisplayMetrics;
     27 import android.util.TypedValue;
     28 import android.view.Menu;
     29 import android.view.MenuItem;
     30 import android.view.View;
     31 import android.view.ViewGroup;
     32 import android.widget.CheckBox;
     33 import android.widget.TextView;
     34 
     35 import java.util.ArrayList;
     36 import java.util.HashMap;
     37 import java.util.List;
     38 
     39 public class AnimatedRecyclerView extends Activity {
     40 
     41     private static final int SCROLL_DISTANCE = 80; // dp
     42 
     43     private RecyclerView mRecyclerView;
     44 
     45     private int mNumItemsAdded = 0;
     46     ArrayList<String> mItems = new ArrayList<String>();
     47     MyAdapter mAdapter;
     48 
     49     boolean mAnimationsEnabled = true;
     50     boolean mPredictiveAnimationsEnabled = true;
     51     RecyclerView.ItemAnimator mCachedAnimator = null;
     52 
     53     @Override
     54     protected void onCreate(Bundle savedInstanceState) {
     55         super.onCreate(savedInstanceState);
     56         setContentView(R.layout.animated_recycler_view);
     57 
     58         ViewGroup container = (ViewGroup) findViewById(R.id.container);
     59         mRecyclerView = new RecyclerView(this);
     60         mCachedAnimator = mRecyclerView.getItemAnimator();
     61         mRecyclerView.setLayoutManager(new MyLayoutManager(this));
     62         mRecyclerView.setHasFixedSize(true);
     63         mRecyclerView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
     64                 ViewGroup.LayoutParams.MATCH_PARENT));
     65         for (int i = 0; i < 6; ++i) {
     66             mItems.add("Item #" + i);
     67         }
     68         mAdapter = new MyAdapter(mItems);
     69         mRecyclerView.setAdapter(mAdapter);
     70         container.addView(mRecyclerView);
     71 
     72         CheckBox enableAnimations = (CheckBox) findViewById(R.id.enableAnimations);
     73         enableAnimations.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
     74             @Override
     75             public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
     76                 if (isChecked && mRecyclerView.getItemAnimator() == null) {
     77                     mRecyclerView.setItemAnimator(mCachedAnimator);
     78                 } else if (!isChecked && mRecyclerView.getItemAnimator() != null) {
     79                     mRecyclerView.setItemAnimator(null);
     80                 }
     81                 mAnimationsEnabled = isChecked;
     82             }
     83         });
     84 
     85         CheckBox enablePredictiveAnimations =
     86                 (CheckBox) findViewById(R.id.enablePredictiveAnimations);
     87         enablePredictiveAnimations.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
     88             @Override
     89             public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
     90                 mPredictiveAnimationsEnabled = isChecked;
     91             }
     92         });
     93 
     94         CheckBox enableChangeAnimations =
     95                 (CheckBox) findViewById(R.id.enableChangeAnimations);
     96         enableChangeAnimations.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
     97             @Override
     98             public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
     99                 mCachedAnimator.setSupportsChangeAnimations(isChecked);
    100             }
    101         });
    102     }
    103 
    104     @Override
    105     public boolean onCreateOptionsMenu(Menu menu) {
    106         super.onCreateOptionsMenu(menu);
    107         MenuItemCompat.setShowAsAction(menu.add("Layout"), MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
    108         return true;
    109     }
    110 
    111     @Override
    112     public boolean onOptionsItemSelected(MenuItem item) {
    113         mRecyclerView.requestLayout();
    114         return super.onOptionsItemSelected(item);
    115     }
    116 
    117     public void checkboxClicked(View view) {
    118         ViewGroup parent = (ViewGroup) view.getParent();
    119         boolean selected = ((CheckBox) view).isChecked();
    120         MyViewHolder holder = (MyViewHolder) mRecyclerView.getChildViewHolder(parent);
    121         mAdapter.selectItem(holder, selected);
    122     }
    123 
    124     public void itemClicked(View view) {
    125         ViewGroup parent = (ViewGroup) view;
    126         MyViewHolder holder = (MyViewHolder) mRecyclerView.getChildViewHolder(parent);
    127         final int position = holder.getAdapterPosition();
    128         if (position == RecyclerView.NO_POSITION) {
    129             return;
    130         }
    131         mAdapter.toggleExpanded(holder);
    132         mAdapter.notifyItemChanged(position);
    133     }
    134 
    135     public void deleteItem(View view) {
    136         int numItems = mItems.size();
    137         if (numItems > 0) {
    138             for (int i = numItems - 1; i >= 0; --i) {
    139                 final String itemText = mItems.get(i);
    140                 boolean selected = mAdapter.mSelected.get(itemText);
    141                 if (selected) {
    142                     removeAtPosition(i);
    143                 }
    144             }
    145         }
    146     }
    147 
    148     private String generateNewText() {
    149         return "Added Item #" + mNumItemsAdded++;
    150     }
    151 
    152     public void d1a2d3(View view) {
    153         removeAtPosition(1);
    154         addAtPosition(2, "Added Item #" + mNumItemsAdded++);
    155         removeAtPosition(3);
    156     }
    157 
    158     private void removeAtPosition(int position) {
    159         mItems.remove(position);
    160         mAdapter.notifyItemRemoved(position);
    161     }
    162 
    163     private void addAtPosition(int position, String text) {
    164         mItems.add(position, text);
    165         mAdapter.mSelected.put(text, Boolean.FALSE);
    166         mAdapter.mExpanded.put(text, Boolean.FALSE);
    167         mAdapter.notifyItemInserted(position);
    168     }
    169 
    170     public void addDeleteItem(View view) {
    171         addItem(view);
    172         deleteItem(view);
    173     }
    174 
    175     public void deleteAddItem(View view) {
    176         deleteItem(view);
    177         addItem(view);
    178     }
    179 
    180     public void addItem(View view) {
    181         addAtPosition(3, "Added Item #" + mNumItemsAdded++);
    182     }
    183 
    184     /**
    185      * A basic ListView-style LayoutManager.
    186      */
    187     class MyLayoutManager extends RecyclerView.LayoutManager {
    188         private static final String TAG = "MyLayoutManager";
    189         private int mFirstPosition;
    190         private final int mScrollDistance;
    191 
    192         public MyLayoutManager(Context c) {
    193             final DisplayMetrics dm = c.getResources().getDisplayMetrics();
    194             mScrollDistance = (int) (SCROLL_DISTANCE * dm.density + 0.5f);
    195         }
    196 
    197         @Override
    198         public boolean supportsPredictiveItemAnimations() {
    199             return mPredictiveAnimationsEnabled;
    200         }
    201 
    202         @Override
    203         public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    204             int parentBottom = getHeight() - getPaddingBottom();
    205 
    206             final View oldTopView = getChildCount() > 0 ? getChildAt(0) : null;
    207             int oldTop = getPaddingTop();
    208             if (oldTopView != null) {
    209                 oldTop = Math.min(oldTopView.getTop(), oldTop);
    210             }
    211 
    212             // Note that we add everything to the scrap, but we do not clean it up;
    213             // that is handled by the RecyclerView after this method returns
    214             detachAndScrapAttachedViews(recycler);
    215 
    216             int top = oldTop;
    217             int bottom = top;
    218             final int left = getPaddingLeft();
    219             final int right = getWidth() - getPaddingRight();
    220 
    221             int count = state.getItemCount();
    222             for (int i = 0; mFirstPosition + i < count && top < parentBottom; i++, top = bottom) {
    223                 View v = recycler.getViewForPosition(mFirstPosition + i);
    224 
    225                 RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) v.getLayoutParams();
    226                 addView(v);
    227                 measureChild(v, 0, 0);
    228                 bottom = top + v.getMeasuredHeight();
    229                 v.layout(left, top, right, bottom);
    230                 if (mPredictiveAnimationsEnabled && params.isItemRemoved()) {
    231                     parentBottom += v.getHeight();
    232                 }
    233             }
    234 
    235             if (mAnimationsEnabled && mPredictiveAnimationsEnabled && !state.isPreLayout()) {
    236                 // Now that we've run a full layout, figure out which views were not used
    237                 // (cached in previousViews). For each of these views, position it where
    238                 // it would go, according to its position relative to the visible
    239                 // positions in the list. This information will be used by RecyclerView to
    240                 // record post-layout positions of these items for the purposes of animating them
    241                 // out of view
    242 
    243                 View lastVisibleView = getChildAt(getChildCount() - 1);
    244                 if (lastVisibleView != null) {
    245                     RecyclerView.LayoutParams lastParams =
    246                             (RecyclerView.LayoutParams) lastVisibleView.getLayoutParams();
    247                     int lastPosition = lastParams.getViewLayoutPosition();
    248                     final List<RecyclerView.ViewHolder> previousViews = recycler.getScrapList();
    249                     count = previousViews.size();
    250                     for (int i = 0; i < count; ++i) {
    251                         View view = previousViews.get(i).itemView;
    252                         RecyclerView.LayoutParams params =
    253                                 (RecyclerView.LayoutParams) view.getLayoutParams();
    254                         if (params.isItemRemoved()) {
    255                             continue;
    256                         }
    257                         int position = params.getViewLayoutPosition();
    258                         int newTop;
    259                         if (position < mFirstPosition) {
    260                             newTop = view.getHeight() * (position - mFirstPosition);
    261                         } else {
    262                             newTop = lastVisibleView.getTop() + view.getHeight() *
    263                                     (position - lastPosition);
    264                         }
    265                         view.offsetTopAndBottom(newTop - view.getTop());
    266                     }
    267                 }
    268             }
    269         }
    270 
    271         @Override
    272         public RecyclerView.LayoutParams generateDefaultLayoutParams() {
    273             return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
    274                     ViewGroup.LayoutParams.WRAP_CONTENT);
    275         }
    276 
    277         @Override
    278         public boolean canScrollVertically() {
    279             return true;
    280         }
    281 
    282         @Override
    283         public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
    284                 RecyclerView.State state) {
    285             if (getChildCount() == 0) {
    286                 return 0;
    287             }
    288 
    289             int scrolled = 0;
    290             final int left = getPaddingLeft();
    291             final int right = getWidth() - getPaddingRight();
    292             if (dy < 0) {
    293                 while (scrolled > dy) {
    294                     final View topView = getChildAt(0);
    295                     final int hangingTop = Math.max(-topView.getTop(), 0);
    296                     final int scrollBy = Math.min(scrolled - dy, hangingTop);
    297                     scrolled -= scrollBy;
    298                     offsetChildrenVertical(scrollBy);
    299                     if (mFirstPosition > 0 && scrolled > dy) {
    300                         mFirstPosition--;
    301                         View v = recycler.getViewForPosition(mFirstPosition);
    302                         addView(v, 0);
    303                         measureChild(v, 0, 0);
    304                         final int bottom = topView.getTop(); // TODO decorated top?
    305                         final int top = bottom - v.getMeasuredHeight();
    306                         v.layout(left, top, right, bottom);
    307                     } else {
    308                         break;
    309                     }
    310                 }
    311             } else if (dy > 0) {
    312                 final int parentHeight = getHeight();
    313                 while (scrolled < dy) {
    314                     final View bottomView = getChildAt(getChildCount() - 1);
    315                     final int hangingBottom = Math.max(bottomView.getBottom() - parentHeight, 0);
    316                     final int scrollBy = -Math.min(dy - scrolled, hangingBottom);
    317                     scrolled -= scrollBy;
    318                     offsetChildrenVertical(scrollBy);
    319                     if (scrolled < dy && state.getItemCount() > mFirstPosition + getChildCount()) {
    320                         View v = recycler.getViewForPosition(mFirstPosition + getChildCount());
    321                         final int top = getChildAt(getChildCount() - 1).getBottom();
    322                         addView(v);
    323                         measureChild(v, 0, 0);
    324                         final int bottom = top + v.getMeasuredHeight();
    325                         v.layout(left, top, right, bottom);
    326                     } else {
    327                         break;
    328                     }
    329                 }
    330             }
    331             recycleViewsOutOfBounds(recycler);
    332             return scrolled;
    333         }
    334 
    335         @Override
    336         public View onFocusSearchFailed(View focused, int direction,
    337                 RecyclerView.Recycler recycler, RecyclerView.State state) {
    338             final int oldCount = getChildCount();
    339 
    340             if (oldCount == 0) {
    341                 return null;
    342             }
    343 
    344             final int left = getPaddingLeft();
    345             final int right = getWidth() - getPaddingRight();
    346 
    347             View toFocus = null;
    348             int newViewsHeight = 0;
    349             if (direction == View.FOCUS_UP || direction == View.FOCUS_BACKWARD) {
    350                 while (mFirstPosition > 0 && newViewsHeight < mScrollDistance) {
    351                     mFirstPosition--;
    352                     View v = recycler.getViewForPosition(mFirstPosition);
    353                     final int bottom = getChildAt(0).getTop(); // TODO decorated top?
    354                     addView(v, 0);
    355                     measureChild(v, 0, 0);
    356                     final int top = bottom - v.getMeasuredHeight();
    357                     v.layout(left, top, right, bottom);
    358                     if (v.isFocusable()) {
    359                         toFocus = v;
    360                         break;
    361                     }
    362                 }
    363             }
    364             if (direction == View.FOCUS_DOWN || direction == View.FOCUS_FORWARD) {
    365                 while (mFirstPosition + getChildCount() < state.getItemCount() &&
    366                         newViewsHeight < mScrollDistance) {
    367                     View v = recycler.getViewForPosition(mFirstPosition + getChildCount());
    368                     final int top = getChildAt(getChildCount() - 1).getBottom();
    369                     addView(v);
    370                     measureChild(v, 0, 0);
    371                     final int bottom = top + v.getMeasuredHeight();
    372                     v.layout(left, top, right, bottom);
    373                     if (v.isFocusable()) {
    374                         toFocus = v;
    375                         break;
    376                     }
    377                 }
    378             }
    379 
    380             return toFocus;
    381         }
    382 
    383         public void recycleViewsOutOfBounds(RecyclerView.Recycler recycler) {
    384             final int childCount = getChildCount();
    385             final int parentWidth = getWidth();
    386             final int parentHeight = getHeight();
    387             boolean foundFirst = false;
    388             int first = 0;
    389             int last = 0;
    390             for (int i = 0; i < childCount; i++) {
    391                 final View v = getChildAt(i);
    392                 if (v.hasFocus() || (v.getRight() >= 0 && v.getLeft() <= parentWidth &&
    393                         v.getBottom() >= 0 && v.getTop() <= parentHeight)) {
    394                     if (!foundFirst) {
    395                         first = i;
    396                         foundFirst = true;
    397                     }
    398                     last = i;
    399                 }
    400             }
    401             for (int i = childCount - 1; i > last; i--) {
    402                 removeAndRecycleViewAt(i, recycler);
    403             }
    404             for (int i = first - 1; i >= 0; i--) {
    405                 removeAndRecycleViewAt(i, recycler);
    406             }
    407             if (getChildCount() == 0) {
    408                 mFirstPosition = 0;
    409             } else {
    410                 mFirstPosition += first;
    411             }
    412         }
    413 
    414         @Override
    415         public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
    416             if (positionStart < mFirstPosition) {
    417                 mFirstPosition += itemCount;
    418             }
    419         }
    420 
    421         @Override
    422         public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
    423             if (positionStart < mFirstPosition) {
    424                 mFirstPosition -= itemCount;
    425             }
    426         }
    427     }
    428 
    429     class MyAdapter extends RecyclerView.Adapter {
    430         private int mBackground;
    431         List<String> mData;
    432         ArrayMap<String, Boolean> mSelected = new ArrayMap<String, Boolean>();
    433         ArrayMap<String, Boolean> mExpanded = new ArrayMap<String, Boolean>();
    434 
    435         public MyAdapter(List<String> data) {
    436             TypedValue val = new TypedValue();
    437             AnimatedRecyclerView.this.getTheme().resolveAttribute(
    438                     R.attr.selectableItemBackground, val, true);
    439             mBackground = val.resourceId;
    440             mData = data;
    441             for (String itemText : mData) {
    442                 mSelected.put(itemText, Boolean.FALSE);
    443                 mExpanded.put(itemText, Boolean.FALSE);
    444             }
    445         }
    446 
    447         @Override
    448         public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    449             MyViewHolder h = new MyViewHolder(getLayoutInflater().inflate(R.layout.selectable_item,
    450                     null));
    451             h.textView.setMinimumHeight(128);
    452             h.textView.setFocusable(true);
    453             h.textView.setBackgroundResource(mBackground);
    454             return h;
    455         }
    456 
    457         @Override
    458         public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    459             String itemText = mData.get(position);
    460             ((MyViewHolder) holder).textView.setText(itemText);
    461             ((MyViewHolder) holder).expandedText.setText("More text for the expanded version");
    462             boolean selected = false;
    463             if (mSelected.get(itemText) != null) {
    464                 selected = mSelected.get(itemText);
    465             }
    466             ((MyViewHolder) holder).checkBox.setChecked(selected);
    467             Boolean expanded = mExpanded.get(itemText);
    468             if (expanded != null && expanded) {
    469                 ((MyViewHolder) holder).expandedText.setVisibility(View.VISIBLE);
    470                 ((MyViewHolder) holder).textView.setVisibility(View.GONE);
    471             } else {
    472                 ((MyViewHolder) holder).expandedText.setVisibility(View.GONE);
    473                 ((MyViewHolder) holder).textView.setVisibility(View.VISIBLE);
    474             }
    475         }
    476 
    477         @Override
    478         public int getItemCount() {
    479             return mData.size();
    480         }
    481 
    482         public void selectItem(String itemText, boolean selected) {
    483             mSelected.put(itemText, selected);
    484         }
    485 
    486         public void selectItem(MyViewHolder holder, boolean selected) {
    487             mSelected.put((String) holder.textView.getText().toString(), selected);
    488         }
    489 
    490         public void toggleExpanded(MyViewHolder holder) {
    491             String text = (String) holder.textView.getText();
    492             mExpanded.put(text, !mExpanded.get(text));
    493         }
    494     }
    495 
    496     static class MyViewHolder extends RecyclerView.ViewHolder {
    497         public TextView expandedText;
    498         public TextView textView;
    499         public CheckBox checkBox;
    500 
    501         public MyViewHolder(View v) {
    502             super(v);
    503             expandedText = (TextView) v.findViewById(R.id.expandedText);
    504             textView = (TextView) v.findViewById(R.id.text);
    505             checkBox = (CheckBox) v.findViewById(R.id.selected);
    506         }
    507 
    508         @Override
    509         public String toString() {
    510             return super.toString() + " \"" + textView.getText() + "\"";
    511         }
    512     }
    513 }
    514