• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
onCreate(Bundle savedInstanceState)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
onCreateOptionsMenu(Menu menu)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
onOptionsItemSelected(MenuItem item)112     public boolean onOptionsItemSelected(MenuItem item) {
113         mRecyclerView.requestLayout();
114         return super.onOptionsItemSelected(item);
115     }
116 
checkboxClicked(View view)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 
itemClicked(View view)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 
deleteSelectedItems(View view)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 
generateNewText()148     private String generateNewText() {
149         return "Added Item #" + mNumItemsAdded++;
150     }
151 
d1a2d3(View view)152     public void d1a2d3(View view) {
153         removeAtPosition(1);
154         addAtPosition(2, "Added Item #" + mNumItemsAdded++);
155         removeAtPosition(3);
156     }
157 
removeAtPosition(int position)158     private void removeAtPosition(int position) {
159         if(position < mItems.size()) {
160             mItems.remove(position);
161             mAdapter.notifyItemRemoved(position);
162         }
163     }
164 
addAtPosition(int position, String text)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 
addDeleteItem(View view)175     public void addDeleteItem(View view) {
176         addItem(view);
177         deleteSelectedItems(view);
178     }
179 
deleteAddItem(View view)180     public void deleteAddItem(View view) {
181         deleteSelectedItems(view);
182         addItem(view);
183     }
184 
addItem(View view)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 
MyLayoutManager(Context c)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
supportsPredictiveItemAnimations()203         public boolean supportsPredictiveItemAnimations() {
204             return mPredictiveAnimationsEnabled;
205         }
206 
207         @Override
onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)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
generateDefaultLayoutParams()277         public RecyclerView.LayoutParams generateDefaultLayoutParams() {
278             return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
279                     ViewGroup.LayoutParams.WRAP_CONTENT);
280         }
281 
282         @Override
canScrollVertically()283         public boolean canScrollVertically() {
284             return true;
285         }
286 
287         @Override
scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state)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
onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler, RecyclerView.State state)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 
recycleViewsOutOfBounds(RecyclerView.Recycler recycler)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
onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount)420         public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
421             if (positionStart < mFirstPosition) {
422                 mFirstPosition += itemCount;
423             }
424         }
425 
426         @Override
onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount)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 
MyAdapter(List<String> data)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
onCreateViewHolder(ViewGroup parent, int viewType)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
onBindViewHolder(RecyclerView.ViewHolder holder, int position)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
getItemCount()483         public int getItemCount() {
484             return mData.size();
485         }
486 
selectItem(String itemText, boolean selected)487         public void selectItem(String itemText, boolean selected) {
488             mSelected.put(itemText, selected);
489         }
490 
selectItem(MyViewHolder holder, boolean selected)491         public void selectItem(MyViewHolder holder, boolean selected) {
492             mSelected.put((String) holder.textView.getText().toString(), selected);
493         }
494 
toggleExpanded(MyViewHolder holder)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 
MyViewHolder(View v)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
toString()514         public String toString() {
515             return super.toString() + " \"" + textView.getText() + "\"";
516         }
517     }
518 }
519