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 deleteSelectedItems(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         if(position < mItems.size()) {
    160             mItems.remove(position);
    161             mAdapter.notifyItemRemoved(position);
    162         }
    163     }
    164 
    165     private void addAtPosition(int position, String text) {
    166         if (position > mItems.size()) {
    167             position = mItems.size();
    168         }
    169         mItems.add(position, text);
    170         mAdapter.mSelected.put(text, Boolean.FALSE);
    171         mAdapter.mExpanded.put(text, Boolean.FALSE);
    172         mAdapter.notifyItemInserted(position);
    173     }
    174 
    175     public void addDeleteItem(View view) {
    176         addItem(view);
    177         deleteSelectedItems(view);
    178     }
    179 
    180     public void deleteAddItem(View view) {
    181         deleteSelectedItems(view);
    182         addItem(view);
    183     }
    184 
    185     public void addItem(View view) {
    186         addAtPosition(3, "Added Item #" + mNumItemsAdded++);
    187     }
    188 
    189     /**
    190      * A basic ListView-style LayoutManager.
    191      */
    192     class MyLayoutManager extends RecyclerView.LayoutManager {
    193         private static final String TAG = "MyLayoutManager";
    194         private int mFirstPosition;
    195         private final int mScrollDistance;
    196 
    197         public MyLayoutManager(Context c) {
    198             final DisplayMetrics dm = c.getResources().getDisplayMetrics();
    199             mScrollDistance = (int) (SCROLL_DISTANCE * dm.density + 0.5f);
    200         }
    201 
    202         @Override
    203         public boolean supportsPredictiveItemAnimations() {
    204             return mPredictiveAnimationsEnabled;
    205         }
    206 
    207         @Override
    208         public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    209             int parentBottom = getHeight() - getPaddingBottom();
    210 
    211             final View oldTopView = getChildCount() > 0 ? getChildAt(0) : null;
    212             int oldTop = getPaddingTop();
    213             if (oldTopView != null) {
    214                 oldTop = Math.min(oldTopView.getTop(), oldTop);
    215             }
    216 
    217             // Note that we add everything to the scrap, but we do not clean it up;
    218             // that is handled by the RecyclerView after this method returns
    219             detachAndScrapAttachedViews(recycler);
    220 
    221             int top = oldTop;
    222             int bottom = top;
    223             final int left = getPaddingLeft();
    224             final int right = getWidth() - getPaddingRight();
    225 
    226             int count = state.getItemCount();
    227             for (int i = 0; mFirstPosition + i < count && top < parentBottom; i++, top = bottom) {
    228                 View v = recycler.getViewForPosition(mFirstPosition + i);
    229 
    230                 RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) v.getLayoutParams();
    231                 addView(v);
    232                 measureChild(v, 0, 0);
    233                 bottom = top + v.getMeasuredHeight();
    234                 v.layout(left, top, right, bottom);
    235                 if (mPredictiveAnimationsEnabled && params.isItemRemoved()) {
    236                     parentBottom += v.getHeight();
    237                 }
    238             }
    239 
    240             if (mAnimationsEnabled && mPredictiveAnimationsEnabled && !state.isPreLayout()) {
    241                 // Now that we've run a full layout, figure out which views were not used
    242                 // (cached in previousViews). For each of these views, position it where
    243                 // it would go, according to its position relative to the visible
    244                 // positions in the list. This information will be used by RecyclerView to
    245                 // record post-layout positions of these items for the purposes of animating them
    246                 // out of view
    247 
    248                 View lastVisibleView = getChildAt(getChildCount() - 1);
    249                 if (lastVisibleView != null) {
    250                     RecyclerView.LayoutParams lastParams =
    251                             (RecyclerView.LayoutParams) lastVisibleView.getLayoutParams();
    252                     int lastPosition = lastParams.getViewLayoutPosition();
    253                     final List<RecyclerView.ViewHolder> previousViews = recycler.getScrapList();
    254                     count = previousViews.size();
    255                     for (int i = 0; i < count; ++i) {
    256                         View view = previousViews.get(i).itemView;
    257                         RecyclerView.LayoutParams params =
    258                                 (RecyclerView.LayoutParams) view.getLayoutParams();
    259                         if (params.isItemRemoved()) {
    260                             continue;
    261                         }
    262                         int position = params.getViewLayoutPosition();
    263                         int newTop;
    264                         if (position < mFirstPosition) {
    265                             newTop = view.getHeight() * (position - mFirstPosition);
    266                         } else {
    267                             newTop = lastVisibleView.getTop() + view.getHeight() *
    268                                     (position - lastPosition);
    269                         }
    270                         view.offsetTopAndBottom(newTop - view.getTop());
    271                     }
    272                 }
    273             }
    274         }
    275 
    276         @Override
    277         public RecyclerView.LayoutParams generateDefaultLayoutParams() {
    278             return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
    279                     ViewGroup.LayoutParams.WRAP_CONTENT);
    280         }
    281 
    282         @Override
    283         public boolean canScrollVertically() {
    284             return true;
    285         }
    286 
    287         @Override
    288         public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
    289                 RecyclerView.State state) {
    290             if (getChildCount() == 0) {
    291                 return 0;
    292             }
    293 
    294             int scrolled = 0;
    295             final int left = getPaddingLeft();
    296             final int right = getWidth() - getPaddingRight();
    297             if (dy < 0) {
    298                 while (scrolled > dy) {
    299                     final View topView = getChildAt(0);
    300                     final int hangingTop = Math.max(-topView.getTop(), 0);
    301                     final int scrollBy = Math.min(scrolled - dy, hangingTop);
    302                     scrolled -= scrollBy;
    303                     offsetChildrenVertical(scrollBy);
    304                     if (mFirstPosition > 0 && scrolled > dy) {
    305                         mFirstPosition--;
    306                         View v = recycler.getViewForPosition(mFirstPosition);
    307                         addView(v, 0);
    308                         measureChild(v, 0, 0);
    309                         final int bottom = topView.getTop(); // TODO decorated top?
    310                         final int top = bottom - v.getMeasuredHeight();
    311                         v.layout(left, top, right, bottom);
    312                     } else {
    313                         break;
    314                     }
    315                 }
    316             } else if (dy > 0) {
    317                 final int parentHeight = getHeight();
    318                 while (scrolled < dy) {
    319                     final View bottomView = getChildAt(getChildCount() - 1);
    320                     final int hangingBottom = Math.max(bottomView.getBottom() - parentHeight, 0);
    321                     final int scrollBy = -Math.min(dy - scrolled, hangingBottom);
    322                     scrolled -= scrollBy;
    323                     offsetChildrenVertical(scrollBy);
    324                     if (scrolled < dy && state.getItemCount() > mFirstPosition + getChildCount()) {
    325                         View v = recycler.getViewForPosition(mFirstPosition + getChildCount());
    326                         final int top = getChildAt(getChildCount() - 1).getBottom();
    327                         addView(v);
    328                         measureChild(v, 0, 0);
    329                         final int bottom = top + v.getMeasuredHeight();
    330                         v.layout(left, top, right, bottom);
    331                     } else {
    332                         break;
    333                     }
    334                 }
    335             }
    336             recycleViewsOutOfBounds(recycler);
    337             return scrolled;
    338         }
    339 
    340         @Override
    341         public View onFocusSearchFailed(View focused, int direction,
    342                 RecyclerView.Recycler recycler, RecyclerView.State state) {
    343             final int oldCount = getChildCount();
    344 
    345             if (oldCount == 0) {
    346                 return null;
    347             }
    348 
    349             final int left = getPaddingLeft();
    350             final int right = getWidth() - getPaddingRight();
    351 
    352             View toFocus = null;
    353             int newViewsHeight = 0;
    354             if (direction == View.FOCUS_UP || direction == View.FOCUS_BACKWARD) {
    355                 while (mFirstPosition > 0 && newViewsHeight < mScrollDistance) {
    356                     mFirstPosition--;
    357                     View v = recycler.getViewForPosition(mFirstPosition);
    358                     final int bottom = getChildAt(0).getTop(); // TODO decorated top?
    359                     addView(v, 0);
    360                     measureChild(v, 0, 0);
    361                     final int top = bottom - v.getMeasuredHeight();
    362                     v.layout(left, top, right, bottom);
    363                     if (v.isFocusable()) {
    364                         toFocus = v;
    365                         break;
    366                     }
    367                 }
    368             }
    369             if (direction == View.FOCUS_DOWN || direction == View.FOCUS_FORWARD) {
    370                 while (mFirstPosition + getChildCount() < state.getItemCount() &&
    371                         newViewsHeight < mScrollDistance) {
    372                     View v = recycler.getViewForPosition(mFirstPosition + getChildCount());
    373                     final int top = getChildAt(getChildCount() - 1).getBottom();
    374                     addView(v);
    375                     measureChild(v, 0, 0);
    376                     final int bottom = top + v.getMeasuredHeight();
    377                     v.layout(left, top, right, bottom);
    378                     if (v.isFocusable()) {
    379                         toFocus = v;
    380                         break;
    381                     }
    382                 }
    383             }
    384 
    385             return toFocus;
    386         }
    387 
    388         public void recycleViewsOutOfBounds(RecyclerView.Recycler recycler) {
    389             final int childCount = getChildCount();
    390             final int parentWidth = getWidth();
    391             final int parentHeight = getHeight();
    392             boolean foundFirst = false;
    393             int first = 0;
    394             int last = 0;
    395             for (int i = 0; i < childCount; i++) {
    396                 final View v = getChildAt(i);
    397                 if (v.hasFocus() || (v.getRight() >= 0 && v.getLeft() <= parentWidth &&
    398                         v.getBottom() >= 0 && v.getTop() <= parentHeight)) {
    399                     if (!foundFirst) {
    400                         first = i;
    401                         foundFirst = true;
    402                     }
    403                     last = i;
    404                 }
    405             }
    406             for (int i = childCount - 1; i > last; i--) {
    407                 removeAndRecycleViewAt(i, recycler);
    408             }
    409             for (int i = first - 1; i >= 0; i--) {
    410                 removeAndRecycleViewAt(i, recycler);
    411             }
    412             if (getChildCount() == 0) {
    413                 mFirstPosition = 0;
    414             } else {
    415                 mFirstPosition += first;
    416             }
    417         }
    418 
    419         @Override
    420         public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
    421             if (positionStart < mFirstPosition) {
    422                 mFirstPosition += itemCount;
    423             }
    424         }
    425 
    426         @Override
    427         public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
    428             if (positionStart < mFirstPosition) {
    429                 mFirstPosition -= itemCount;
    430             }
    431         }
    432     }
    433 
    434     class MyAdapter extends RecyclerView.Adapter {
    435         private int mBackground;
    436         List<String> mData;
    437         ArrayMap<String, Boolean> mSelected = new ArrayMap<String, Boolean>();
    438         ArrayMap<String, Boolean> mExpanded = new ArrayMap<String, Boolean>();
    439 
    440         public MyAdapter(List<String> data) {
    441             TypedValue val = new TypedValue();
    442             AnimatedRecyclerView.this.getTheme().resolveAttribute(
    443                     R.attr.selectableItemBackground, val, true);
    444             mBackground = val.resourceId;
    445             mData = data;
    446             for (String itemText : mData) {
    447                 mSelected.put(itemText, Boolean.FALSE);
    448                 mExpanded.put(itemText, Boolean.FALSE);
    449             }
    450         }
    451 
    452         @Override
    453         public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    454             MyViewHolder h = new MyViewHolder(getLayoutInflater().inflate(R.layout.selectable_item,
    455                     null));
    456             h.textView.setMinimumHeight(128);
    457             h.textView.setFocusable(true);
    458             h.textView.setBackgroundResource(mBackground);
    459             return h;
    460         }
    461 
    462         @Override
    463         public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    464             String itemText = mData.get(position);
    465             ((MyViewHolder) holder).textView.setText(itemText);
    466             ((MyViewHolder) holder).expandedText.setText("More text for the expanded version");
    467             boolean selected = false;
    468             if (mSelected.get(itemText) != null) {
    469                 selected = mSelected.get(itemText);
    470             }
    471             ((MyViewHolder) holder).checkBox.setChecked(selected);
    472             Boolean expanded = mExpanded.get(itemText);
    473             if (expanded != null && expanded) {
    474                 ((MyViewHolder) holder).expandedText.setVisibility(View.VISIBLE);
    475                 ((MyViewHolder) holder).textView.setVisibility(View.GONE);
    476             } else {
    477                 ((MyViewHolder) holder).expandedText.setVisibility(View.GONE);
    478                 ((MyViewHolder) holder).textView.setVisibility(View.VISIBLE);
    479             }
    480         }
    481 
    482         @Override
    483         public int getItemCount() {
    484             return mData.size();
    485         }
    486 
    487         public void selectItem(String itemText, boolean selected) {
    488             mSelected.put(itemText, selected);
    489         }
    490 
    491         public void selectItem(MyViewHolder holder, boolean selected) {
    492             mSelected.put((String) holder.textView.getText().toString(), selected);
    493         }
    494 
    495         public void toggleExpanded(MyViewHolder holder) {
    496             String text = (String) holder.textView.getText();
    497             mExpanded.put(text, !mExpanded.get(text));
    498         }
    499     }
    500 
    501     static class MyViewHolder extends RecyclerView.ViewHolder {
    502         public TextView expandedText;
    503         public TextView textView;
    504         public CheckBox checkBox;
    505 
    506         public MyViewHolder(View v) {
    507             super(v);
    508             expandedText = (TextView) v.findViewById(R.id.expandedText);
    509             textView = (TextView) v.findViewById(R.id.text);
    510             checkBox = (CheckBox) v.findViewById(R.id.selected);
    511         }
    512 
    513         @Override
    514         public String toString() {
    515             return super.toString() + " \"" + textView.getText() + "\"";
    516         }
    517     }
    518 }
    519