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 deleteItem(View view)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 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 mItems.remove(position); 160 mAdapter.notifyItemRemoved(position); 161 } 162 addAtPosition(int position, String text)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 addDeleteItem(View view)170 public void addDeleteItem(View view) { 171 addItem(view); 172 deleteItem(view); 173 } 174 deleteAddItem(View view)175 public void deleteAddItem(View view) { 176 deleteItem(view); 177 addItem(view); 178 } 179 addItem(View view)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 MyLayoutManager(Context c)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 supportsPredictiveItemAnimations()198 public boolean supportsPredictiveItemAnimations() { 199 return mPredictiveAnimationsEnabled; 200 } 201 202 @Override onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)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 generateDefaultLayoutParams()272 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 273 return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 274 ViewGroup.LayoutParams.WRAP_CONTENT); 275 } 276 277 @Override canScrollVertically()278 public boolean canScrollVertically() { 279 return true; 280 } 281 282 @Override scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state)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 onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler, RecyclerView.State state)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 recycleViewsOutOfBounds(RecyclerView.Recycler recycler)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 onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount)415 public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { 416 if (positionStart < mFirstPosition) { 417 mFirstPosition += itemCount; 418 } 419 } 420 421 @Override onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount)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 MyAdapter(List<String> data)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 onCreateViewHolder(ViewGroup parent, int viewType)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 onBindViewHolder(RecyclerView.ViewHolder holder, int position)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 getItemCount()478 public int getItemCount() { 479 return mData.size(); 480 } 481 selectItem(String itemText, boolean selected)482 public void selectItem(String itemText, boolean selected) { 483 mSelected.put(itemText, selected); 484 } 485 selectItem(MyViewHolder holder, boolean selected)486 public void selectItem(MyViewHolder holder, boolean selected) { 487 mSelected.put((String) holder.textView.getText().toString(), selected); 488 } 489 toggleExpanded(MyViewHolder holder)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 MyViewHolder(View v)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 toString()509 public String toString() { 510 return super.toString() + " \"" + textView.getText() + "\""; 511 } 512 } 513 } 514