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