Skip to content

Instantly share code, notes, and snippets.

@DanteAndroid
Created February 27, 2022 04:21
Show Gist options
  • Save DanteAndroid/9b99947d4a8cfd2dd4bd7e2134312cf6 to your computer and use it in GitHub Desktop.
Save DanteAndroid/9b99947d4a8cfd2dd4bd7e2134312cf6 to your computer and use it in GitHub Desktop.
ReyclerView item animator - Color Text Change
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package examples.android.com.recyclerviewanimations;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ArgbEvaluator;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.support.annotation.NonNull;
import android.support.v4.util.ArrayMap;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.LinearLayout;
import android.widget.RadioGroup;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
RecyclerView mRecyclerView;
RadioGroup mRadioGroup;
CheckBox mPredictiveCB, mCustomCB;
DefaultItemAnimator mDefaultItemAnimator = new DefaultItemAnimator();
MyChangeAnimator mChangeAnimator = new MyChangeAnimator();
ArgbEvaluator mColorEvaluator = new ArgbEvaluator();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRadioGroup = (RadioGroup) findViewById(R.id.radioGroup);
mPredictiveCB = (CheckBox) findViewById(R.id.predictiveCB);
mCustomCB = (CheckBox) findViewById(R.id.customCB);
mRecyclerView = (RecyclerView) findViewById(R.id.recyclerview);
mRecyclerView.setLayoutManager(new MyLinearLayoutManager(this));
mRecyclerView.setAdapter(new RVAdapter());
mCustomCB.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
mRecyclerView.setItemAnimator(isChecked ? mChangeAnimator : mDefaultItemAnimator);
}
});
}
/**
* Custom adapter that supplies view holders to the RecyclerView. Our view holders
* contain a simple LinearLayout (with a background color) and a TextView (displaying
* the value of the container's bg color).
*/
private class RVAdapter extends RecyclerView.Adapter {
ArrayList<Integer> mColors = new ArrayList<>();
public RVAdapter() {
generateData();
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
final MyViewHolder myHolder = (MyViewHolder) holder;
int color = mColors.get(position);
myHolder.container.setBackgroundColor(color);
myHolder.textView.setText("#" + Integer.toHexString(color));
}
@Override
public int getItemCount() {
return mColors.size();
}
private void deleteItem(View view) {
int position = mRecyclerView.getChildAdapterPosition(view);
if (position != RecyclerView.NO_POSITION) {
mColors.remove(position);
notifyItemRemoved(position);
}
}
private void addItem(View view) {
int position = mRecyclerView.getChildAdapterPosition(view);
if (position != RecyclerView.NO_POSITION) {
int color = generateColor();
mColors.add(position, color);
notifyItemInserted(position);
}
}
private void changeItem(View view) {
int position = mRecyclerView.getChildAdapterPosition(view);
if (position != RecyclerView.NO_POSITION) {
int color = generateColor();
mColors.set(position, color);
notifyItemChanged(position);
}
}
private View.OnClickListener mItemAction = new View.OnClickListener() {
@Override
public void onClick(View v) {
switch (mRadioGroup.getCheckedRadioButtonId()) {
case R.id.deleteRB:
deleteItem(v);
break;
case R.id.addRB:
addItem(v);
break;
case R.id.changeRB:
changeItem(v);
break;
}
}
};
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View container = getLayoutInflater().inflate(R.layout.item_layout, parent, false);
container.setOnClickListener(mItemAction);
return new MyViewHolder(container);
}
private int generateColor() {
int red = ((int) (Math.random() * 200));
int green = ((int) (Math.random() * 200));
int blue = ((int) (Math.random() * 200));
return Color.rgb(red, green, blue);
}
private void generateData() {
for (int i = 0; i < 100; ++i) {
mColors.add(generateColor());
}
}
}
/**
* Simple extension of LinearLayoutManager for the sole purpose of showing what happens
* when predictive animations (which are enabled by default in LinearLayoutManager) are
* not enabled. This behavior is toggled via a checkbox in the UI.
*/
private class MyLinearLayoutManager extends LinearLayoutManager {
public MyLinearLayoutManager(Context context) {
super(context);
}
@Override
public boolean supportsPredictiveItemAnimations() {
return mPredictiveCB.isChecked();
}
}
/**
* Custom ItemAnimator that handles change animations. The default animator simple cross-
* fades the old and new ViewHolder items. Our custom animation re-uses the same
* ViewHolder and animates the contents of the views that it contains. The animation
* consists of a color fade through black and a text rotation. The animation also handles
* interruption, when a new change event happens on an item that is currently being
* animated.
*/
private class MyChangeAnimator extends DefaultItemAnimator {
// stateless interpolators re-used for every change animation
private AccelerateInterpolator mAccelerateInterpolator = new AccelerateInterpolator();
private DecelerateInterpolator mDecelerateInterpolator = new DecelerateInterpolator();
// Maps to hold running animators. These are used when running a new change
// animation on an item that is already being animated. mRunningAnimators is
// used to cancel the previous animation. mAnimatorMap is used to construct
// the new change animation based on where the previous one was at when it
// was interrupted.
private ArrayMap<RecyclerView.ViewHolder, AnimatorInfo> mAnimatorMap = new ArrayMap<>();
@Override
public boolean canReuseUpdatedViewHolder(RecyclerView.ViewHolder viewHolder) {
// This allows our custom change animation on the contents of the holder instead
// of the default behavior of replacing the viewHolder entirely
return true;
}
@NonNull
private ItemHolderInfo getItemHolderInfo(MyViewHolder viewHolder, ColorTextInfo info) {
final MyViewHolder myHolder = viewHolder;
final int bgColor = ((ColorDrawable) myHolder.container.getBackground()).getColor();
info.color = bgColor;
info.text = (String) myHolder.textView.getText();
return info;
}
@NonNull
@Override
public ItemHolderInfo recordPreLayoutInformation(RecyclerView.State state,
RecyclerView.ViewHolder viewHolder, int changeFlags, List<Object> payloads) {
ColorTextInfo info = (ColorTextInfo) super.recordPreLayoutInformation(state, viewHolder,
changeFlags, payloads);
return getItemHolderInfo((MyViewHolder) viewHolder, info);
}
@NonNull
@Override
public ItemHolderInfo recordPostLayoutInformation(@NonNull RecyclerView.State state,
@NonNull RecyclerView.ViewHolder viewHolder) {
ColorTextInfo info = (ColorTextInfo) super.recordPostLayoutInformation(state, viewHolder);
return getItemHolderInfo((MyViewHolder) viewHolder, info);
}
@Override
public ItemHolderInfo obtainHolderInfo() {
return new ColorTextInfo();
}
/**
* Custom ItemHolderInfo class that holds color and text information used in
* our custom change animation
*/
private class ColorTextInfo extends ItemHolderInfo {
int color;
String text;
}
/**
* Holds child animator objects for any change animation. Used when a new change
* animation interrupts one already in progress; the new one is constructed to start
* from where the previous one was at when the interruption occurred.
*/
private class AnimatorInfo {
Animator overallAnim;
ObjectAnimator fadeToBlackAnim, fadeFromBlackAnim, oldTextRotator, newTextRotator;
public AnimatorInfo(Animator overallAnim,
ObjectAnimator fadeToBlackAnim, ObjectAnimator fadeFromBlackAnim,
ObjectAnimator oldTextRotator, ObjectAnimator newTextRotator) {
this.overallAnim = overallAnim;
this.fadeToBlackAnim = fadeToBlackAnim;
this.fadeFromBlackAnim = fadeFromBlackAnim;
this.oldTextRotator = oldTextRotator;
this.newTextRotator = newTextRotator;
}
}
/**
* Custom change animation. Fade to black on the container background, then back
* up to the new bg coolor. Meanwhile, the text rotates, switching along the way.
* If a new change animation occurs on an item that is currently animating
* a change, we stop the previous change and start the new one where the old
* one left off.
*/
@Override
public boolean animateChange(@NonNull final RecyclerView.ViewHolder oldHolder,
@NonNull final RecyclerView.ViewHolder newHolder,
@NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
if (oldHolder != newHolder) {
// use default behavior if not re-using view holders
return super.animateChange(oldHolder, newHolder, preInfo, postInfo);
}
final MyViewHolder viewHolder = (MyViewHolder) newHolder;
// Get the pre/post change values; these are what we are animating between
ColorTextInfo oldInfo = (ColorTextInfo) preInfo;
ColorTextInfo newInfo = (ColorTextInfo) postInfo;
int oldColor = oldInfo.color;
int newColor = newInfo.color;
final String oldText = oldInfo.text;
final String newText = newInfo.text;
// These are the objects whose values will be animated
LinearLayout newContainer = viewHolder.container;
final TextView newTextView = viewHolder.textView;
// Check to see if there's a change animation already running on this item
AnimatorInfo runningInfo = mAnimatorMap.get(newHolder);
long prevAnimPlayTime = 0;
boolean firstHalf = false;
if (runningInfo != null) {
// The information we need to construct the new animators is whether we
// are in the 'first half' (fading to black and rotating the old text out)
// and how far we are in whichever half is running
firstHalf = runningInfo.oldTextRotator != null &&
runningInfo.oldTextRotator.isRunning();
prevAnimPlayTime = firstHalf ?
runningInfo.oldTextRotator.getCurrentPlayTime() :
runningInfo.newTextRotator.getCurrentPlayTime();
// done with previous animation - cancel it
runningInfo.overallAnim.cancel();
}
// Construct the fade to/from black animation
ObjectAnimator fadeToBlack = null, fadeFromBlack;
if (runningInfo == null || firstHalf) {
// The first part of the animation fades to black. Skip this phase
// if we're interrupting an animation that was already in the second phase.
int startColor = oldColor;
if (runningInfo != null) {
startColor = (Integer) runningInfo.fadeToBlackAnim.getAnimatedValue();
}
fadeToBlack = ObjectAnimator.ofInt(newContainer, "backgroundColor",
startColor, Color.BLACK);
fadeToBlack.setEvaluator(mColorEvaluator);
if (runningInfo != null) {
// Seek to appropriate time in new animator if we were already
// running a previous animation
fadeToBlack.setCurrentPlayTime(prevAnimPlayTime);
}
}
// Second phase of animation fades from black to the new bg color
fadeFromBlack = ObjectAnimator.ofInt(newContainer, "backgroundColor",
Color.BLACK, newColor);
fadeFromBlack.setEvaluator(mColorEvaluator);
if (runningInfo != null && !firstHalf) {
// Seek to appropriate time in new animator if we were already
// running a previous animation
fadeFromBlack.setCurrentPlayTime(prevAnimPlayTime);
}
// Set up an animation to play both the first (if non-null) and second phases
AnimatorSet bgAnim = new AnimatorSet();
if (fadeToBlack != null) {
bgAnim.playSequentially(fadeToBlack, fadeFromBlack);
} else {
bgAnim.play(fadeFromBlack);
}
// The other part of the animation rotates the text, switching it to the
// new value half-way through (when it is perpendicular to the user)
ObjectAnimator oldTextRotate = null, newTextRotate;
if (runningInfo == null || firstHalf) {
// The first part of the animation rotates text to be perpendicular to user.
// Skip this phase if we're interrupting an animation that was already
// in the second phase.
oldTextRotate = ObjectAnimator.ofFloat(newTextView, View.ROTATION_X, 0, 90);
oldTextRotate.setInterpolator(mAccelerateInterpolator);
if (runningInfo != null) {
oldTextRotate.setCurrentPlayTime(prevAnimPlayTime);
}
oldTextRotate.addListener(new AnimatorListenerAdapter() {
boolean mCanceled = false;
@Override
public void onAnimationStart(Animator animation) {
// text was changed as part of the item change notification. Change
// it back for the first phase of the animation
newTextView.setText(oldText);
}
@Override
public void onAnimationCancel(Animator animation) {
mCanceled = true;
}
@Override
public void onAnimationEnd(Animator animation) {
if (!mCanceled) {
// Set it to the new text when the old rotator ends - this is when
// it is perpendicular to the user (thus making the switch
// invisible)
newTextView.setText(newText);
}
}
});
}
// Second half of text rotation rotates from perpendicular to 0
newTextRotate = ObjectAnimator.ofFloat(newTextView, View.ROTATION_X, -90, 0);
newTextRotate.setInterpolator(mDecelerateInterpolator);
if (runningInfo != null && !firstHalf) {
// If we're interrupting a previous second-phase animation, seek to that time
newTextRotate.setCurrentPlayTime(prevAnimPlayTime);
}
// Choreograph first and second half. First half may be null if we interrupted
// a second-phase animation
AnimatorSet textAnim = new AnimatorSet();
if (oldTextRotate != null) {
textAnim.playSequentially(oldTextRotate, newTextRotate);
} else {
textAnim.play(newTextRotate);
}
// Choreograph both animations: color fading and text rotating
AnimatorSet changeAnim = new AnimatorSet();
changeAnim.playTogether(bgAnim, textAnim);
changeAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
dispatchAnimationFinished(newHolder);
mAnimatorMap.remove(newHolder);
}
});
changeAnim.start();
// Store info about this animation to be re-used if a succeeding change event
// occurs while it's still running
AnimatorInfo runningAnimInfo = new AnimatorInfo(changeAnim, fadeToBlack, fadeFromBlack,
oldTextRotate, newTextRotate);
mAnimatorMap.put(newHolder, runningAnimInfo);
return true;
}
@Override
public void endAnimation(RecyclerView.ViewHolder item) {
super.endAnimation(item);
if (!mAnimatorMap.isEmpty()) {
final int numRunning = mAnimatorMap.size();
for (int i = numRunning; i >= 0; i--) {
if (item == mAnimatorMap.keyAt(i)) {
mAnimatorMap.valueAt(i).overallAnim.cancel();
}
}
}
}
@Override
public boolean isRunning() {
return super.isRunning() || !mAnimatorMap.isEmpty();
}
@Override
public void endAnimations() {
super.endAnimations();
if (!mAnimatorMap.isEmpty()) {
final int numRunning = mAnimatorMap.size();
for (int i = numRunning; i >= 0; i--) {
mAnimatorMap.valueAt(i).overallAnim.cancel();
}
}
}
}
static class MyViewHolder extends RecyclerView.ViewHolder {
public TextView textView;
public LinearLayout container;
public MyViewHolder(View v) {
super(v);
container = (LinearLayout) v;
textView = (TextView) v.findViewById(R.id.textview);
}
@Override
public String toString() {
return super.toString() + " \"" + textView.getText() + "\"";
}
}
}
@DanteAndroid
Copy link
Author

DanteAndroid commented Feb 27, 2022

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment