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.androidx.widget; 17 18 import android.animation.Animator; 19 import android.animation.AnimatorListenerAdapter; 20 import android.animation.ValueAnimator; 21 import android.app.Activity; 22 import android.content.Context; 23 import android.os.Bundle; 24 import android.util.DisplayMetrics; 25 import android.util.TypedValue; 26 import android.view.Menu; 27 import android.view.MenuItem; 28 import android.view.View; 29 import android.view.ViewGroup; 30 import android.widget.CheckBox; 31 import android.widget.CompoundButton; 32 import android.widget.TextView; 33 34 import androidx.collection.ArrayMap; 35 import androidx.recyclerview.widget.DefaultItemAnimator; 36 import androidx.recyclerview.widget.RecyclerView; 37 38 import com.example.androidx.R; 39 40 import org.jspecify.annotations.NonNull; 41 42 import java.util.ArrayList; 43 import java.util.List; 44 45 public class AnimatedRecyclerView extends Activity { 46 47 private static final int SCROLL_DISTANCE = 80; // dp 48 49 private RecyclerView mRecyclerView; 50 51 private int mNumItemsAdded = 0; 52 ArrayList<String> mItems = new ArrayList<String>(); 53 MyAdapter mAdapter; 54 55 boolean mAnimationsEnabled = true; 56 boolean mPredictiveAnimationsEnabled = true; 57 RecyclerView.ItemAnimator mCachedAnimator = null; 58 boolean mEnableInPlaceChange = true; 59 60 @Override onCreate(Bundle savedInstanceState)61 protected void onCreate(Bundle savedInstanceState) { 62 super.onCreate(savedInstanceState); 63 setContentView(R.layout.animated_recycler_view); 64 65 ViewGroup container = findViewById(R.id.container); 66 mRecyclerView = new RecyclerView(this); 67 mCachedAnimator = createAnimator(); 68 mCachedAnimator.setChangeDuration(2000); 69 mRecyclerView.setItemAnimator(mCachedAnimator); 70 mRecyclerView.setLayoutManager(new MyLayoutManager(this)); 71 mRecyclerView.setHasFixedSize(true); 72 mRecyclerView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 73 ViewGroup.LayoutParams.MATCH_PARENT)); 74 for (int i = 0; i < 6; ++i) { 75 mItems.add("Item #" + i); 76 } 77 mAdapter = new MyAdapter(mItems); 78 mRecyclerView.setAdapter(mAdapter); 79 container.addView(mRecyclerView); 80 81 CheckBox enableAnimations = findViewById(R.id.enableAnimations); 82 enableAnimations.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 83 @Override 84 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 85 if (isChecked && mRecyclerView.getItemAnimator() == null) { 86 mRecyclerView.setItemAnimator(mCachedAnimator); 87 } else if (!isChecked && mRecyclerView.getItemAnimator() != null) { 88 mRecyclerView.setItemAnimator(null); 89 } 90 mAnimationsEnabled = isChecked; 91 } 92 }); 93 94 CheckBox enablePredictiveAnimations = 95 findViewById(R.id.enablePredictiveAnimations); 96 enablePredictiveAnimations.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 97 @Override 98 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 99 mPredictiveAnimationsEnabled = isChecked; 100 } 101 }); 102 103 CheckBox enableInPlaceChange = findViewById(R.id.enableInPlaceChange); 104 enableInPlaceChange.setChecked(mEnableInPlaceChange); 105 enableInPlaceChange.setOnCheckedChangeListener( 106 new CompoundButton.OnCheckedChangeListener() { 107 @Override 108 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 109 mEnableInPlaceChange = isChecked; 110 } 111 }); 112 } 113 createAnimator()114 private RecyclerView.ItemAnimator createAnimator() { 115 return new DefaultItemAnimator() { 116 List<ItemChangeAnimator> mPendingChangeAnimations = new ArrayList<>(); 117 ArrayMap<RecyclerView.ViewHolder, ItemChangeAnimator> mRunningAnimations 118 = new ArrayMap<>(); 119 ArrayMap<MyViewHolder, Long> mPendingSettleList = new ArrayMap<>(); 120 121 @Override 122 public void runPendingAnimations() { 123 super.runPendingAnimations(); 124 for (ItemChangeAnimator anim : mPendingChangeAnimations) { 125 anim.start(); 126 mRunningAnimations.put(anim.mViewHolder, anim); 127 } 128 mPendingChangeAnimations.clear(); 129 for (int i = mPendingSettleList.size() - 1; i >=0; i--) { 130 final MyViewHolder vh = mPendingSettleList.keyAt(i); 131 final long duration = mPendingSettleList.valueAt(i); 132 vh.textView.animate().translationX(0f).alpha(1f) 133 .setDuration(duration).setListener( 134 new AnimatorListenerAdapter() { 135 @Override 136 public void onAnimationStart(Animator animator) { 137 dispatchAnimationStarted(vh); 138 } 139 140 @Override 141 public void onAnimationEnd(Animator animator) { 142 vh.textView.setTranslationX(0f); 143 vh.textView.setAlpha(1f); 144 dispatchAnimationFinished(vh); 145 } 146 147 @Override 148 public void onAnimationCancel(Animator animator) { 149 150 } 151 }).start(); 152 } 153 mPendingSettleList.clear(); 154 } 155 156 @Override 157 public @NonNull ItemHolderInfo recordPreLayoutInformation( 158 RecyclerView.@NonNull State state, RecyclerView.@NonNull ViewHolder viewHolder, 159 @AdapterChanges int changeFlags, @NonNull List<Object> payloads) { 160 MyItemInfo info = (MyItemInfo) super 161 .recordPreLayoutInformation(state, viewHolder, changeFlags, payloads); 162 info.text = ((MyViewHolder) viewHolder).textView.getText(); 163 return info; 164 } 165 166 @Override 167 public @NonNull ItemHolderInfo recordPostLayoutInformation( 168 RecyclerView.@NonNull State state, 169 RecyclerView.@NonNull ViewHolder viewHolder) { 170 MyItemInfo info = (MyItemInfo) super.recordPostLayoutInformation(state, viewHolder); 171 info.text = ((MyViewHolder) viewHolder).textView.getText(); 172 return info; 173 } 174 175 176 @Override 177 public boolean canReuseUpdatedViewHolder(RecyclerView.@NonNull ViewHolder viewHolder) { 178 return mEnableInPlaceChange; 179 } 180 181 @Override 182 public void endAnimation(RecyclerView.@NonNull ViewHolder item) { 183 super.endAnimation(item); 184 for (int i = mPendingChangeAnimations.size() - 1; i >= 0; i--) { 185 ItemChangeAnimator anim = mPendingChangeAnimations.get(i); 186 if (anim.mViewHolder == item) { 187 mPendingChangeAnimations.remove(i); 188 anim.setFraction(1f); 189 dispatchChangeFinished(item, true); 190 } 191 } 192 for (int i = mRunningAnimations.size() - 1; i >= 0; i--) { 193 ItemChangeAnimator animator = mRunningAnimations.get(item); 194 if (animator != null) { 195 animator.end(); 196 mRunningAnimations.removeAt(i); 197 } 198 } 199 for (int i = mPendingSettleList.size() - 1; i >= 0; i--) { 200 final MyViewHolder vh = mPendingSettleList.keyAt(i); 201 if (vh == item) { 202 mPendingSettleList.removeAt(i); 203 dispatchChangeFinished(item, true); 204 } 205 } 206 } 207 208 @Override 209 public boolean animateChange(RecyclerView.@NonNull ViewHolder oldHolder, 210 RecyclerView.@NonNull ViewHolder newHolder, 211 @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { 212 if (oldHolder != newHolder) { 213 return super.animateChange(oldHolder, newHolder, preLayoutInfo, postLayoutInfo); 214 } 215 return animateChangeApiHoneycombMr1(oldHolder, preLayoutInfo, postLayoutInfo); 216 } 217 218 private boolean animateChangeApiHoneycombMr1(RecyclerView.ViewHolder oldHolder, 219 ItemHolderInfo preInfo, ItemHolderInfo postInfo) { 220 endAnimation(oldHolder); 221 MyItemInfo pre = (MyItemInfo) preInfo; 222 MyItemInfo post = (MyItemInfo) postInfo; 223 MyViewHolder vh = (MyViewHolder) oldHolder; 224 225 CharSequence finalText = post.text; 226 227 if (pre.text.toString().contentEquals(post.text)) { 228 // same content. Just translate back to 0 229 final long duration = (long) (getChangeDuration() 230 * (vh.textView.getTranslationX() / vh.textView.getWidth())); 231 mPendingSettleList.put(vh, duration); 232 // we set it here because previous endAnimation would set it to other value. 233 vh.textView.setText(finalText); 234 } else { 235 // different content, get out and come back. 236 vh.textView.setText(pre.text); 237 final ItemChangeAnimator anim = new ItemChangeAnimator(vh, finalText, 238 getChangeDuration()) { 239 @Override 240 public void onAnimationEnd(Animator animation) { 241 setFraction(1f); 242 dispatchChangeFinished(mViewHolder, true); 243 } 244 245 @Override 246 public void onAnimationStart(Animator animation) { 247 dispatchChangeStarting(mViewHolder, true); 248 } 249 }; 250 mPendingChangeAnimations.add(anim); 251 } 252 return true; 253 } 254 255 @Override 256 public @NonNull ItemHolderInfo obtainHolderInfo() { 257 return new MyItemInfo(); 258 } 259 }; 260 } 261 262 abstract private static class ItemChangeAnimator implements 263 ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener { 264 CharSequence mFinalText; 265 ValueAnimator mValueAnimator; 266 MyViewHolder mViewHolder; 267 final float mMaxX; 268 final float mStartRatio; ItemChangeAnimator(MyViewHolder viewHolder, CharSequence finalText, long duration)269 public ItemChangeAnimator(MyViewHolder viewHolder, CharSequence finalText, long duration) { 270 mViewHolder = viewHolder; 271 mMaxX = mViewHolder.itemView.getWidth(); 272 mStartRatio = mViewHolder.textView.getTranslationX() / mMaxX; 273 mFinalText = finalText; 274 mValueAnimator = ValueAnimator.ofFloat(0f, 1f); 275 mValueAnimator.addUpdateListener(this); 276 mValueAnimator.addListener(this); 277 mValueAnimator.setDuration(duration); 278 mValueAnimator.setTarget(mViewHolder.itemView); 279 } 280 setFraction(float fraction)281 void setFraction(float fraction) { 282 fraction = mStartRatio + (1f - mStartRatio) * fraction; 283 if (fraction < .5f) { 284 mViewHolder.textView.setTranslationX(fraction * mMaxX); 285 mViewHolder.textView.setAlpha(1f - fraction); 286 } else { 287 mViewHolder.textView.setTranslationX((1f - fraction) * mMaxX); 288 mViewHolder.textView.setAlpha(fraction); 289 maybeSetFinalText(); 290 } 291 } 292 293 @Override onAnimationUpdate(ValueAnimator valueAnimator)294 public void onAnimationUpdate(ValueAnimator valueAnimator) { 295 setFraction(valueAnimator.getAnimatedFraction()); 296 } 297 start()298 public void start() { 299 mValueAnimator.start(); 300 } 301 302 @Override onAnimationEnd(Animator animation)303 public void onAnimationEnd(Animator animation) { 304 maybeSetFinalText(); 305 mViewHolder.textView.setAlpha(1f); 306 } 307 maybeSetFinalText()308 public void maybeSetFinalText() { 309 if (mFinalText != null) { 310 mViewHolder.textView.setText(mFinalText); 311 mFinalText = null; 312 } 313 } 314 end()315 public void end() { 316 mValueAnimator.cancel(); 317 } 318 319 @Override onAnimationStart(Animator animation)320 public void onAnimationStart(Animator animation) { 321 } 322 323 @Override onAnimationCancel(Animator animation)324 public void onAnimationCancel(Animator animation) { 325 } 326 327 @Override onAnimationRepeat(Animator animation)328 public void onAnimationRepeat(Animator animation) { 329 } 330 } 331 332 private static class MyItemInfo extends DefaultItemAnimator.ItemHolderInfo { 333 CharSequence text; 334 } 335 336 @Override onCreateOptionsMenu(Menu menu)337 public boolean onCreateOptionsMenu(Menu menu) { 338 super.onCreateOptionsMenu(menu); 339 menu.add("Layout").setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); 340 return true; 341 } 342 343 @Override onOptionsItemSelected(MenuItem item)344 public boolean onOptionsItemSelected(MenuItem item) { 345 mRecyclerView.requestLayout(); 346 return super.onOptionsItemSelected(item); 347 } 348 349 @SuppressWarnings("unused") checkboxClicked(View view)350 public void checkboxClicked(View view) { 351 ViewGroup parent = (ViewGroup) view.getParent(); 352 boolean selected = ((CheckBox) view).isChecked(); 353 MyViewHolder holder = (MyViewHolder) mRecyclerView.getChildViewHolder(parent); 354 mAdapter.selectItem(holder, selected); 355 } 356 357 @SuppressWarnings("unused") itemClicked(View view)358 public void itemClicked(View view) { 359 ViewGroup parent = (ViewGroup) view; 360 MyViewHolder holder = (MyViewHolder) mRecyclerView.getChildViewHolder(parent); 361 final int position = holder.getBindingAdapterPosition(); 362 if (position == RecyclerView.NO_POSITION) { 363 return; 364 } 365 mAdapter.toggleExpanded(holder); 366 mAdapter.notifyItemChanged(position); 367 } 368 deleteSelectedItems(View view)369 public void deleteSelectedItems(View view) { 370 int numItems = mItems.size(); 371 if (numItems > 0) { 372 for (int i = numItems - 1; i >= 0; --i) { 373 final String itemText = mItems.get(i); 374 boolean selected = mAdapter.mSelected.get(itemText); 375 if (selected) { 376 removeAtPosition(i); 377 } 378 } 379 } 380 } 381 d1a2d3(View view)382 public void d1a2d3(View view) { 383 removeAtPosition(1); 384 addAtPosition(2, "Added Item #" + mNumItemsAdded++); 385 removeAtPosition(3); 386 } 387 removeAtPosition(int position)388 private void removeAtPosition(int position) { 389 if(position < mItems.size()) { 390 mItems.remove(position); 391 mAdapter.notifyItemRemoved(position); 392 } 393 } 394 addAtPosition(int position, String text)395 private void addAtPosition(int position, String text) { 396 if (position > mItems.size()) { 397 position = mItems.size(); 398 } 399 mItems.add(position, text); 400 mAdapter.mSelected.put(text, Boolean.FALSE); 401 mAdapter.mExpanded.put(text, Boolean.FALSE); 402 mAdapter.notifyItemInserted(position); 403 } 404 addDeleteItem(View view)405 public void addDeleteItem(View view) { 406 addItem(view); 407 deleteSelectedItems(view); 408 } 409 deleteAddItem(View view)410 public void deleteAddItem(View view) { 411 deleteSelectedItems(view); 412 addItem(view); 413 } 414 addItem(View view)415 public void addItem(View view) { 416 addAtPosition(3, "Added Item #" + mNumItemsAdded++); 417 } 418 419 /** 420 * A basic ListView-style LayoutManager. 421 */ 422 class MyLayoutManager extends RecyclerView.LayoutManager { 423 private static final String TAG = "MyLayoutManager"; 424 private int mFirstPosition; 425 private final int mScrollDistance; 426 MyLayoutManager(Context c)427 public MyLayoutManager(Context c) { 428 final DisplayMetrics dm = c.getResources().getDisplayMetrics(); 429 mScrollDistance = (int) (SCROLL_DISTANCE * dm.density + 0.5f); 430 } 431 432 @Override supportsPredictiveItemAnimations()433 public boolean supportsPredictiveItemAnimations() { 434 return mPredictiveAnimationsEnabled; 435 } 436 437 @Override onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)438 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 439 int parentBottom = getHeight() - getPaddingBottom(); 440 441 final View oldTopView = getChildCount() > 0 ? getChildAt(0) : null; 442 int oldTop = getPaddingTop(); 443 if (oldTopView != null) { 444 oldTop = Math.min(oldTopView.getTop(), oldTop); 445 } 446 447 // Note that we add everything to the scrap, but we do not clean it up; 448 // that is handled by the RecyclerView after this method returns 449 detachAndScrapAttachedViews(recycler); 450 451 int top = oldTop; 452 int bottom = top; 453 final int left = getPaddingLeft(); 454 final int right = getWidth() - getPaddingRight(); 455 456 int count = state.getItemCount(); 457 for (int i = 0; mFirstPosition + i < count && top < parentBottom; i++, top = bottom) { 458 View v = recycler.getViewForPosition(mFirstPosition + i); 459 460 RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) v.getLayoutParams(); 461 addView(v); 462 measureChild(v, 0, 0); 463 bottom = top + v.getMeasuredHeight(); 464 v.layout(left, top, right, bottom); 465 if (mPredictiveAnimationsEnabled && params.isItemRemoved()) { 466 parentBottom += v.getHeight(); 467 } 468 } 469 470 if (mAnimationsEnabled && mPredictiveAnimationsEnabled && !state.isPreLayout()) { 471 // Now that we've run a full layout, figure out which views were not used 472 // (cached in previousViews). For each of these views, position it where 473 // it would go, according to its position relative to the visible 474 // positions in the list. This information will be used by RecyclerView to 475 // record post-layout positions of these items for the purposes of animating them 476 // out of view 477 478 View lastVisibleView = getChildAt(getChildCount() - 1); 479 if (lastVisibleView != null) { 480 RecyclerView.LayoutParams lastParams = 481 (RecyclerView.LayoutParams) lastVisibleView.getLayoutParams(); 482 int lastPosition = lastParams.getViewLayoutPosition(); 483 final List<RecyclerView.ViewHolder> previousViews = recycler.getScrapList(); 484 count = previousViews.size(); 485 for (int i = 0; i < count; ++i) { 486 View view = previousViews.get(i).itemView; 487 RecyclerView.LayoutParams params = 488 (RecyclerView.LayoutParams) view.getLayoutParams(); 489 if (params.isItemRemoved()) { 490 continue; 491 } 492 int position = params.getViewLayoutPosition(); 493 int newTop; 494 if (position < mFirstPosition) { 495 newTop = view.getHeight() * (position - mFirstPosition); 496 } else { 497 newTop = lastVisibleView.getTop() + view.getHeight() * 498 (position - lastPosition); 499 } 500 view.offsetTopAndBottom(newTop - view.getTop()); 501 } 502 } 503 } 504 } 505 506 @Override generateDefaultLayoutParams()507 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 508 return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 509 ViewGroup.LayoutParams.WRAP_CONTENT); 510 } 511 512 @Override canScrollVertically()513 public boolean canScrollVertically() { 514 return true; 515 } 516 517 @Override scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state)518 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 519 RecyclerView.State state) { 520 if (getChildCount() == 0) { 521 return 0; 522 } 523 524 int scrolled = 0; 525 final int left = getPaddingLeft(); 526 final int right = getWidth() - getPaddingRight(); 527 if (dy < 0) { 528 while (scrolled > dy) { 529 final View topView = getChildAt(0); 530 final int hangingTop = Math.max(-topView.getTop(), 0); 531 final int scrollBy = Math.min(scrolled - dy, hangingTop); 532 scrolled -= scrollBy; 533 offsetChildrenVertical(scrollBy); 534 if (mFirstPosition > 0 && scrolled > dy) { 535 mFirstPosition--; 536 View v = recycler.getViewForPosition(mFirstPosition); 537 addView(v, 0); 538 measureChild(v, 0, 0); 539 final int bottom = topView.getTop(); // TODO decorated top? 540 final int top = bottom - v.getMeasuredHeight(); 541 v.layout(left, top, right, bottom); 542 } else { 543 break; 544 } 545 } 546 } else if (dy > 0) { 547 final int parentHeight = getHeight(); 548 while (scrolled < dy) { 549 final View bottomView = getChildAt(getChildCount() - 1); 550 final int hangingBottom = Math.max(bottomView.getBottom() - parentHeight, 0); 551 final int scrollBy = -Math.min(dy - scrolled, hangingBottom); 552 scrolled -= scrollBy; 553 offsetChildrenVertical(scrollBy); 554 if (scrolled < dy && state.getItemCount() > mFirstPosition + getChildCount()) { 555 View v = recycler.getViewForPosition(mFirstPosition + getChildCount()); 556 final int top = getChildAt(getChildCount() - 1).getBottom(); 557 addView(v); 558 measureChild(v, 0, 0); 559 final int bottom = top + v.getMeasuredHeight(); 560 v.layout(left, top, right, bottom); 561 } else { 562 break; 563 } 564 } 565 } 566 recycleViewsOutOfBounds(recycler); 567 return scrolled; 568 } 569 570 @Override onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler, RecyclerView.State state)571 public View onFocusSearchFailed(View focused, int direction, 572 RecyclerView.Recycler recycler, RecyclerView.State state) { 573 final int oldCount = getChildCount(); 574 575 if (oldCount == 0) { 576 return null; 577 } 578 579 final int left = getPaddingLeft(); 580 final int right = getWidth() - getPaddingRight(); 581 582 View toFocus = null; 583 int newViewsHeight = 0; 584 if (direction == View.FOCUS_UP || direction == View.FOCUS_BACKWARD) { 585 while (mFirstPosition > 0 && newViewsHeight < mScrollDistance) { 586 mFirstPosition--; 587 View v = recycler.getViewForPosition(mFirstPosition); 588 final int bottom = getChildAt(0).getTop(); // TODO decorated top? 589 addView(v, 0); 590 measureChild(v, 0, 0); 591 final int top = bottom - v.getMeasuredHeight(); 592 v.layout(left, top, right, bottom); 593 if (v.isFocusable()) { 594 toFocus = v; 595 break; 596 } 597 } 598 } 599 if (direction == View.FOCUS_DOWN || direction == View.FOCUS_FORWARD) { 600 while (mFirstPosition + getChildCount() < state.getItemCount() && 601 newViewsHeight < mScrollDistance) { 602 View v = recycler.getViewForPosition(mFirstPosition + getChildCount()); 603 final int top = getChildAt(getChildCount() - 1).getBottom(); 604 addView(v); 605 measureChild(v, 0, 0); 606 final int bottom = top + v.getMeasuredHeight(); 607 v.layout(left, top, right, bottom); 608 if (v.isFocusable()) { 609 toFocus = v; 610 break; 611 } 612 } 613 } 614 615 return toFocus; 616 } 617 recycleViewsOutOfBounds(RecyclerView.Recycler recycler)618 public void recycleViewsOutOfBounds(RecyclerView.Recycler recycler) { 619 final int childCount = getChildCount(); 620 final int parentWidth = getWidth(); 621 final int parentHeight = getHeight(); 622 boolean foundFirst = false; 623 int first = 0; 624 int last = 0; 625 for (int i = 0; i < childCount; i++) { 626 final View v = getChildAt(i); 627 if (v.hasFocus() || (v.getRight() >= 0 && v.getLeft() <= parentWidth && 628 v.getBottom() >= 0 && v.getTop() <= parentHeight)) { 629 if (!foundFirst) { 630 first = i; 631 foundFirst = true; 632 } 633 last = i; 634 } 635 } 636 for (int i = childCount - 1; i > last; i--) { 637 removeAndRecycleViewAt(i, recycler); 638 } 639 for (int i = first - 1; i >= 0; i--) { 640 removeAndRecycleViewAt(i, recycler); 641 } 642 if (getChildCount() == 0) { 643 mFirstPosition = 0; 644 } else { 645 mFirstPosition += first; 646 } 647 } 648 649 @Override onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount)650 public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { 651 if (positionStart < mFirstPosition) { 652 mFirstPosition += itemCount; 653 } 654 } 655 656 @Override onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount)657 public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) { 658 if (positionStart < mFirstPosition) { 659 mFirstPosition -= itemCount; 660 } 661 } 662 } 663 664 class MyAdapter extends RecyclerView.Adapter { 665 private int mBackground; 666 List<String> mData; 667 ArrayMap<String, Boolean> mSelected = new ArrayMap<String, Boolean>(); 668 ArrayMap<String, Boolean> mExpanded = new ArrayMap<String, Boolean>(); 669 MyAdapter(List<String> data)670 public MyAdapter(List<String> data) { 671 TypedValue val = new TypedValue(); 672 AnimatedRecyclerView.this.getTheme().resolveAttribute( 673 androidx.appcompat.R.attr.selectableItemBackground, val, true); 674 mBackground = val.resourceId; 675 mData = data; 676 for (String itemText : mData) { 677 mSelected.put(itemText, Boolean.FALSE); 678 mExpanded.put(itemText, Boolean.FALSE); 679 } 680 } 681 682 @Override onCreateViewHolder(ViewGroup parent, int viewType)683 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 684 MyViewHolder h = new MyViewHolder(getLayoutInflater().inflate(R.layout.selectable_item, 685 null)); 686 h.textView.setMinimumHeight(128); 687 h.textView.setFocusable(true); 688 h.textView.setBackgroundResource(mBackground); 689 return h; 690 } 691 692 @Override onBindViewHolder(RecyclerView.ViewHolder holder, int position)693 public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { 694 String itemText = mData.get(position); 695 MyViewHolder myViewHolder = (MyViewHolder) holder; 696 myViewHolder.boundText = itemText; 697 myViewHolder.textView.setText(itemText); 698 boolean selected = false; 699 if (mSelected.get(itemText) != null) { 700 selected = mSelected.get(itemText); 701 } 702 myViewHolder.checkBox.setChecked(selected); 703 Boolean expanded = mExpanded.get(itemText); 704 if (Boolean.TRUE.equals(expanded)) { 705 myViewHolder.textView.setText("More text for the expanded version"); 706 } else { 707 myViewHolder.textView.setText(itemText); 708 } 709 } 710 711 @Override getItemCount()712 public int getItemCount() { 713 return mData.size(); 714 } 715 selectItem(MyViewHolder holder, boolean selected)716 public void selectItem(MyViewHolder holder, boolean selected) { 717 mSelected.put(holder.boundText, selected); 718 } 719 toggleExpanded(MyViewHolder holder)720 public void toggleExpanded(MyViewHolder holder) { 721 mExpanded.put(holder.boundText, !mExpanded.get(holder.boundText)); 722 } 723 } 724 725 static class MyViewHolder extends RecyclerView.ViewHolder { 726 public TextView textView; 727 public CheckBox checkBox; 728 public String boundText; 729 MyViewHolder(View v)730 public MyViewHolder(View v) { 731 super(v); 732 textView = (TextView) v.findViewById(R.id.text); 733 checkBox = (CheckBox) v.findViewById(R.id.selected); 734 } 735 736 @Override toString()737 public String toString() { 738 return super.toString() + " \"" + textView.getText() + "\""; 739 } 740 } 741 } 742