mirror of
https://github.com/sudoxnym/habitica-android.git
synced 2026-07-26 16:22:39 +00:00
improve task moving animation. Fixes #1211
This commit is contained in:
parent
f588220df6
commit
40c3665c3a
3 changed files with 617 additions and 660 deletions
|
|
@ -45,6 +45,7 @@ import javax.inject.Named
|
||||||
|
|
||||||
open class TaskRecyclerViewFragment : BaseFragment(), androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener {
|
open class TaskRecyclerViewFragment : BaseFragment(), androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener {
|
||||||
var recyclerAdapter: TaskRecyclerViewAdapter? = null
|
var recyclerAdapter: TaskRecyclerViewAdapter? = null
|
||||||
|
var itemAnimator = SafeDefaultItemAnimator()
|
||||||
@field:[Inject Named(AppModule.NAMED_USER_ID)]
|
@field:[Inject Named(AppModule.NAMED_USER_ID)]
|
||||||
lateinit var userID: String
|
lateinit var userID: String
|
||||||
@Inject
|
@Inject
|
||||||
|
|
@ -187,11 +188,15 @@ open class TaskRecyclerViewFragment : BaseFragment(), androidx.swiperefreshlayou
|
||||||
val movingTaskID = movingTaskID
|
val movingTaskID = movingTaskID
|
||||||
if (fromPosition != null && movingTaskID != null) {
|
if (fromPosition != null && movingTaskID != null) {
|
||||||
recyclerAdapter?.ignoreUpdates = true
|
recyclerAdapter?.ignoreUpdates = true
|
||||||
|
itemAnimator.skipAnimations = true
|
||||||
compositeSubscription.add(taskRepository.updateTaskPosition(classType ?: "", movingTaskID, viewHolder.adapterPosition)
|
compositeSubscription.add(taskRepository.updateTaskPosition(classType ?: "", movingTaskID, viewHolder.adapterPosition)
|
||||||
.delay(1, TimeUnit.SECONDS)
|
.delay(1, TimeUnit.SECONDS)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(Consumer { recyclerAdapter?.ignoreUpdates = false
|
.subscribe(Consumer {
|
||||||
recyclerAdapter?.notifyDataSetChanged()}, RxErrorHandler.handleEmptyError()))
|
recyclerAdapter?.ignoreUpdates = false
|
||||||
|
recyclerAdapter?.notifyDataSetChanged()
|
||||||
|
itemAnimator.skipAnimations = false
|
||||||
|
}, RxErrorHandler.handleEmptyError()))
|
||||||
}
|
}
|
||||||
this.fromPosition = null
|
this.fromPosition = null
|
||||||
this.movingTaskID = null
|
this.movingTaskID = null
|
||||||
|
|
@ -249,7 +254,7 @@ open class TaskRecyclerViewFragment : BaseFragment(), androidx.swiperefreshlayou
|
||||||
|
|
||||||
val bottomPadding = (recyclerView.paddingBottom + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 60f, resources.displayMetrics)).toInt()
|
val bottomPadding = (recyclerView.paddingBottom + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 60f, resources.displayMetrics)).toInt()
|
||||||
recyclerView.setPadding(0, 0, 0, bottomPadding)
|
recyclerView.setPadding(0, 0, 0, bottomPadding)
|
||||||
recyclerView.itemAnimator = SafeDefaultItemAnimator()
|
recyclerView.itemAnimator = itemAnimator
|
||||||
|
|
||||||
refreshLayout.setOnRefreshListener(this)
|
refreshLayout.setOnRefreshListener(this)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,657 +0,0 @@
|
||||||
package com.habitrpg.android.habitica.ui.helpers;
|
|
||||||
|
|
||||||
import android.animation.Animator;
|
|
||||||
import android.animation.AnimatorListenerAdapter;
|
|
||||||
import android.animation.TimeInterpolator;
|
|
||||||
import android.animation.ValueAnimator;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.core.view.ViewCompat;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewPropertyAnimator;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by phillip on 02.10.17.
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class SafeDefaultItemAnimator extends SimpleItemAnimator {
|
|
||||||
private static final boolean DEBUG = false;
|
|
||||||
|
|
||||||
private static TimeInterpolator sDefaultInterpolator;
|
|
||||||
|
|
||||||
private ArrayList<RecyclerView.ViewHolder> mPendingRemovals = new ArrayList<>();
|
|
||||||
private ArrayList<RecyclerView.ViewHolder> mPendingAdditions = new ArrayList<>();
|
|
||||||
private ArrayList<SafeDefaultItemAnimator.MoveInfo> mPendingMoves = new ArrayList<>();
|
|
||||||
private ArrayList<SafeDefaultItemAnimator.ChangeInfo> mPendingChanges = new ArrayList<>();
|
|
||||||
|
|
||||||
ArrayList<ArrayList<RecyclerView.ViewHolder>> mAdditionsList = new ArrayList<>();
|
|
||||||
ArrayList<ArrayList<SafeDefaultItemAnimator.MoveInfo>> mMovesList = new ArrayList<>();
|
|
||||||
ArrayList<ArrayList<SafeDefaultItemAnimator.ChangeInfo>> mChangesList = new ArrayList<>();
|
|
||||||
|
|
||||||
ArrayList<RecyclerView.ViewHolder> mAddAnimations = new ArrayList<>();
|
|
||||||
ArrayList<RecyclerView.ViewHolder> mMoveAnimations = new ArrayList<>();
|
|
||||||
ArrayList<RecyclerView.ViewHolder> mRemoveAnimations = new ArrayList<>();
|
|
||||||
ArrayList<RecyclerView.ViewHolder> mChangeAnimations = new ArrayList<>();
|
|
||||||
|
|
||||||
private static class MoveInfo {
|
|
||||||
public RecyclerView.ViewHolder holder;
|
|
||||||
public int fromX, fromY, toX, toY;
|
|
||||||
|
|
||||||
MoveInfo(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
|
|
||||||
this.holder = holder;
|
|
||||||
this.fromX = fromX;
|
|
||||||
this.fromY = fromY;
|
|
||||||
this.toX = toX;
|
|
||||||
this.toY = toY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class ChangeInfo {
|
|
||||||
public RecyclerView.ViewHolder oldHolder, newHolder;
|
|
||||||
public int fromX, fromY, toX, toY;
|
|
||||||
private ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder) {
|
|
||||||
this.oldHolder = oldHolder;
|
|
||||||
this.newHolder = newHolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
|
|
||||||
int fromX, int fromY, int toX, int toY) {
|
|
||||||
this(oldHolder, newHolder);
|
|
||||||
this.fromX = fromX;
|
|
||||||
this.fromY = fromY;
|
|
||||||
this.toX = toX;
|
|
||||||
this.toY = toY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "ChangeInfo{"
|
|
||||||
+ "oldHolder=" + oldHolder
|
|
||||||
+ ", newHolder=" + newHolder
|
|
||||||
+ ", fromX=" + fromX
|
|
||||||
+ ", fromY=" + fromY
|
|
||||||
+ ", toX=" + toX
|
|
||||||
+ ", toY=" + toY
|
|
||||||
+ '}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void runPendingAnimations() {
|
|
||||||
boolean removalsPending = !mPendingRemovals.isEmpty();
|
|
||||||
boolean movesPending = !mPendingMoves.isEmpty();
|
|
||||||
boolean changesPending = !mPendingChanges.isEmpty();
|
|
||||||
boolean additionsPending = !mPendingAdditions.isEmpty();
|
|
||||||
if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
|
|
||||||
// nothing to animate
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// First, remove stuff
|
|
||||||
for (RecyclerView.ViewHolder holder : mPendingRemovals) {
|
|
||||||
animateRemoveImpl(holder);
|
|
||||||
}
|
|
||||||
mPendingRemovals.clear();
|
|
||||||
// Next, move stuff
|
|
||||||
if (movesPending) {
|
|
||||||
final ArrayList<SafeDefaultItemAnimator.MoveInfo> moves = new ArrayList<>();
|
|
||||||
moves.addAll(mPendingMoves);
|
|
||||||
mMovesList.add(moves);
|
|
||||||
mPendingMoves.clear();
|
|
||||||
Runnable mover = new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
for (SafeDefaultItemAnimator.MoveInfo moveInfo : moves) {
|
|
||||||
animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
|
|
||||||
moveInfo.toX, moveInfo.toY);
|
|
||||||
}
|
|
||||||
moves.clear();
|
|
||||||
mMovesList.remove(moves);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (removalsPending) {
|
|
||||||
RecyclerView.ViewHolder holder = moves.get(0).holder;
|
|
||||||
if (holder != null) {
|
|
||||||
View view = holder.itemView;
|
|
||||||
ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mover.run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Next, change stuff, to run in parallel with move animations
|
|
||||||
if (changesPending) {
|
|
||||||
final ArrayList<SafeDefaultItemAnimator.ChangeInfo> changes = new ArrayList<>();
|
|
||||||
changes.addAll(mPendingChanges);
|
|
||||||
mChangesList.add(changes);
|
|
||||||
mPendingChanges.clear();
|
|
||||||
Runnable changer = new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
for (SafeDefaultItemAnimator.ChangeInfo change : changes) {
|
|
||||||
animateChangeImpl(change);
|
|
||||||
}
|
|
||||||
changes.clear();
|
|
||||||
mChangesList.remove(changes);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (removalsPending) {
|
|
||||||
RecyclerView.ViewHolder holder = changes.get(0).oldHolder;
|
|
||||||
if (holder != null) {
|
|
||||||
ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
changer.run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Next, add stuff
|
|
||||||
if (additionsPending) {
|
|
||||||
final ArrayList<RecyclerView.ViewHolder> additions = new ArrayList<>();
|
|
||||||
additions.addAll(mPendingAdditions);
|
|
||||||
mAdditionsList.add(additions);
|
|
||||||
mPendingAdditions.clear();
|
|
||||||
Runnable adder = new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
for (RecyclerView.ViewHolder holder : additions) {
|
|
||||||
animateAddImpl(holder);
|
|
||||||
}
|
|
||||||
additions.clear();
|
|
||||||
mAdditionsList.remove(additions);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (removalsPending || movesPending || changesPending) {
|
|
||||||
long removeDuration = removalsPending ? getRemoveDuration() : 0;
|
|
||||||
long moveDuration = movesPending ? getMoveDuration() : 0;
|
|
||||||
long changeDuration = changesPending ? getChangeDuration() : 0;
|
|
||||||
long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
|
|
||||||
if (additions.get(0) != null) {
|
|
||||||
View view = additions.get(0).itemView;
|
|
||||||
ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
adder.run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean animateRemove(final RecyclerView.ViewHolder holder) {
|
|
||||||
resetAnimation(holder);
|
|
||||||
mPendingRemovals.add(holder);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
|
|
||||||
final View view = holder.itemView;
|
|
||||||
final ViewPropertyAnimator animation = view.animate();
|
|
||||||
mRemoveAnimations.add(holder);
|
|
||||||
animation.setDuration(getRemoveDuration()).alpha(0).setListener(
|
|
||||||
new AnimatorListenerAdapter() {
|
|
||||||
@Override
|
|
||||||
public void onAnimationStart(Animator animator) {
|
|
||||||
dispatchRemoveStarting(holder);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationEnd(Animator animator) {
|
|
||||||
animation.setListener(null);
|
|
||||||
view.setAlpha(1);
|
|
||||||
dispatchRemoveFinished(holder);
|
|
||||||
mRemoveAnimations.remove(holder);
|
|
||||||
dispatchFinishedWhenDone();
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean animateAdd(final RecyclerView.ViewHolder holder) {
|
|
||||||
resetAnimation(holder);
|
|
||||||
holder.itemView.setAlpha(0);
|
|
||||||
mPendingAdditions.add(holder);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void animateAddImpl(final RecyclerView.ViewHolder holder) {
|
|
||||||
final View view = holder.itemView;
|
|
||||||
final ViewPropertyAnimator animation = view.animate();
|
|
||||||
mAddAnimations.add(holder);
|
|
||||||
animation.alpha(1).setDuration(getAddDuration())
|
|
||||||
.setListener(new AnimatorListenerAdapter() {
|
|
||||||
@Override
|
|
||||||
public void onAnimationStart(Animator animator) {
|
|
||||||
dispatchAddStarting(holder);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationCancel(Animator animator) {
|
|
||||||
view.setAlpha(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationEnd(Animator animator) {
|
|
||||||
animation.setListener(null);
|
|
||||||
dispatchAddFinished(holder);
|
|
||||||
mAddAnimations.remove(holder);
|
|
||||||
dispatchFinishedWhenDone();
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean animateMove(final RecyclerView.ViewHolder holder, int fromX, int fromY,
|
|
||||||
int toX, int toY) {
|
|
||||||
final View view = holder.itemView;
|
|
||||||
fromX += (int) holder.itemView.getTranslationX();
|
|
||||||
fromY += (int) holder.itemView.getTranslationY();
|
|
||||||
resetAnimation(holder);
|
|
||||||
int deltaX = toX - fromX;
|
|
||||||
int deltaY = toY - fromY;
|
|
||||||
if (deltaX == 0 && deltaY == 0) {
|
|
||||||
dispatchMoveFinished(holder);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (deltaX != 0) {
|
|
||||||
view.setTranslationX(-deltaX);
|
|
||||||
}
|
|
||||||
if (deltaY != 0) {
|
|
||||||
view.setTranslationY(-deltaY);
|
|
||||||
}
|
|
||||||
mPendingMoves.add(new SafeDefaultItemAnimator.MoveInfo(holder, fromX, fromY, toX, toY));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void animateMoveImpl(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
|
|
||||||
final View view = holder.itemView;
|
|
||||||
final int deltaX = toX - fromX;
|
|
||||||
final int deltaY = toY - fromY;
|
|
||||||
if (deltaX != 0) {
|
|
||||||
view.animate().translationX(0);
|
|
||||||
}
|
|
||||||
if (deltaY != 0) {
|
|
||||||
view.animate().translationY(0);
|
|
||||||
}
|
|
||||||
// TODO: make EndActions end listeners instead, since end actions aren't called when
|
|
||||||
// vpas are canceled (and can't end them. why?)
|
|
||||||
// need listener functionality in VPACompat for this. Ick.
|
|
||||||
final ViewPropertyAnimator animation = view.animate();
|
|
||||||
mMoveAnimations.add(holder);
|
|
||||||
animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() {
|
|
||||||
@Override
|
|
||||||
public void onAnimationStart(Animator animator) {
|
|
||||||
dispatchMoveStarting(holder);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationCancel(Animator animator) {
|
|
||||||
if (deltaX != 0) {
|
|
||||||
view.setTranslationX(0);
|
|
||||||
}
|
|
||||||
if (deltaY != 0) {
|
|
||||||
view.setTranslationY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationEnd(Animator animator) {
|
|
||||||
animation.setListener(null);
|
|
||||||
dispatchMoveFinished(holder);
|
|
||||||
mMoveAnimations.remove(holder);
|
|
||||||
dispatchFinishedWhenDone();
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
|
|
||||||
int fromX, int fromY, int toX, int toY) {
|
|
||||||
if (oldHolder == newHolder) {
|
|
||||||
// Don't know how to run change animations when the same view holder is re-used.
|
|
||||||
// run a move animation to handle position changes.
|
|
||||||
return animateMove(oldHolder, fromX, fromY, toX, toY);
|
|
||||||
}
|
|
||||||
final float prevTranslationX = oldHolder.itemView.getTranslationX();
|
|
||||||
final float prevTranslationY = oldHolder.itemView.getTranslationY();
|
|
||||||
final float prevAlpha = oldHolder.itemView.getAlpha();
|
|
||||||
resetAnimation(oldHolder);
|
|
||||||
int deltaX = (int) (toX - fromX - prevTranslationX);
|
|
||||||
int deltaY = (int) (toY - fromY - prevTranslationY);
|
|
||||||
// recover prev translation state after ending animation
|
|
||||||
oldHolder.itemView.setTranslationX(prevTranslationX);
|
|
||||||
oldHolder.itemView.setTranslationY(prevTranslationY);
|
|
||||||
oldHolder.itemView.setAlpha(prevAlpha);
|
|
||||||
if (newHolder != null) {
|
|
||||||
// carry over translation values
|
|
||||||
resetAnimation(newHolder);
|
|
||||||
newHolder.itemView.setTranslationX(-deltaX);
|
|
||||||
newHolder.itemView.setTranslationY(-deltaY);
|
|
||||||
newHolder.itemView.setAlpha(0);
|
|
||||||
}
|
|
||||||
mPendingChanges.add(new SafeDefaultItemAnimator.ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void animateChangeImpl(final SafeDefaultItemAnimator.ChangeInfo changeInfo) {
|
|
||||||
final RecyclerView.ViewHolder holder = changeInfo.oldHolder;
|
|
||||||
final View view = holder == null ? null : holder.itemView;
|
|
||||||
final RecyclerView.ViewHolder newHolder = changeInfo.newHolder;
|
|
||||||
final View newView = newHolder != null ? newHolder.itemView : null;
|
|
||||||
if (view != null) {
|
|
||||||
final ViewPropertyAnimator oldViewAnim = view.animate().setDuration(
|
|
||||||
getChangeDuration());
|
|
||||||
mChangeAnimations.add(changeInfo.oldHolder);
|
|
||||||
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
|
|
||||||
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
|
|
||||||
oldViewAnim.alpha(0).setListener(new AnimatorListenerAdapter() {
|
|
||||||
@Override
|
|
||||||
public void onAnimationStart(Animator animator) {
|
|
||||||
dispatchChangeStarting(changeInfo.oldHolder, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationEnd(Animator animator) {
|
|
||||||
oldViewAnim.setListener(null);
|
|
||||||
view.setAlpha(1);
|
|
||||||
view.setTranslationX(0);
|
|
||||||
view.setTranslationY(0);
|
|
||||||
dispatchChangeFinished(changeInfo.oldHolder, true);
|
|
||||||
mChangeAnimations.remove(changeInfo.oldHolder);
|
|
||||||
dispatchFinishedWhenDone();
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
if (newView != null) {
|
|
||||||
final ViewPropertyAnimator newViewAnimation = newView.animate();
|
|
||||||
mChangeAnimations.add(changeInfo.newHolder);
|
|
||||||
newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration())
|
|
||||||
.alpha(1).setListener(new AnimatorListenerAdapter() {
|
|
||||||
@Override
|
|
||||||
public void onAnimationStart(Animator animator) {
|
|
||||||
dispatchChangeStarting(changeInfo.newHolder, false);
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public void onAnimationEnd(Animator animator) {
|
|
||||||
newViewAnimation.setListener(null);
|
|
||||||
newView.setAlpha(1);
|
|
||||||
newView.setTranslationX(0);
|
|
||||||
newView.setTranslationY(0);
|
|
||||||
dispatchChangeFinished(changeInfo.newHolder, false);
|
|
||||||
mChangeAnimations.remove(changeInfo.newHolder);
|
|
||||||
dispatchFinishedWhenDone();
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void endChangeAnimation(List<SafeDefaultItemAnimator.ChangeInfo> infoList, RecyclerView.ViewHolder item) {
|
|
||||||
for (int i = infoList.size() - 1; i >= 0; i--) {
|
|
||||||
SafeDefaultItemAnimator.ChangeInfo changeInfo = infoList.get(i);
|
|
||||||
if (endChangeAnimationIfNecessary(changeInfo, item)) {
|
|
||||||
if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
|
|
||||||
infoList.remove(changeInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void endChangeAnimationIfNecessary(SafeDefaultItemAnimator.ChangeInfo changeInfo) {
|
|
||||||
if (changeInfo.oldHolder != null) {
|
|
||||||
endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder);
|
|
||||||
}
|
|
||||||
if (changeInfo.newHolder != null) {
|
|
||||||
endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private boolean endChangeAnimationIfNecessary(SafeDefaultItemAnimator.ChangeInfo changeInfo, RecyclerView.ViewHolder item) {
|
|
||||||
boolean oldItem = false;
|
|
||||||
if (changeInfo.newHolder == item) {
|
|
||||||
changeInfo.newHolder = null;
|
|
||||||
} else if (changeInfo.oldHolder == item) {
|
|
||||||
changeInfo.oldHolder = null;
|
|
||||||
oldItem = true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
item.itemView.setAlpha(1);
|
|
||||||
item.itemView.setTranslationX(0);
|
|
||||||
item.itemView.setTranslationY(0);
|
|
||||||
dispatchChangeFinished(item, oldItem);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void endAnimation(RecyclerView.ViewHolder item) {
|
|
||||||
final View view = item.itemView;
|
|
||||||
// this will trigger end callback which should set properties to their target values.
|
|
||||||
view.animate().cancel();
|
|
||||||
// TODO if some other animations are chained to end, how do we cancel them as well?
|
|
||||||
for (int i = mPendingMoves.size() - 1; i >= 0; i--) {
|
|
||||||
SafeDefaultItemAnimator.MoveInfo moveInfo = mPendingMoves.get(i);
|
|
||||||
if (moveInfo.holder == item) {
|
|
||||||
view.setTranslationY(0);
|
|
||||||
view.setTranslationX(0);
|
|
||||||
dispatchMoveFinished(item);
|
|
||||||
mPendingMoves.remove(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
endChangeAnimation(mPendingChanges, item);
|
|
||||||
if (mPendingRemovals.remove(item)) {
|
|
||||||
view.setAlpha(1);
|
|
||||||
dispatchRemoveFinished(item);
|
|
||||||
}
|
|
||||||
if (mPendingAdditions.remove(item)) {
|
|
||||||
view.setAlpha(1);
|
|
||||||
dispatchAddFinished(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = mChangesList.size() - 1; i >= 0; i--) {
|
|
||||||
ArrayList<SafeDefaultItemAnimator.ChangeInfo> changes = mChangesList.get(i);
|
|
||||||
endChangeAnimation(changes, item);
|
|
||||||
if (changes.isEmpty()) {
|
|
||||||
mChangesList.remove(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (int i = mMovesList.size() - 1; i >= 0; i--) {
|
|
||||||
ArrayList<SafeDefaultItemAnimator.MoveInfo> moves = mMovesList.get(i);
|
|
||||||
for (int j = moves.size() - 1; j >= 0; j--) {
|
|
||||||
SafeDefaultItemAnimator.MoveInfo moveInfo = moves.get(j);
|
|
||||||
if (moveInfo.holder == item) {
|
|
||||||
view.setTranslationY(0);
|
|
||||||
view.setTranslationX(0);
|
|
||||||
dispatchMoveFinished(item);
|
|
||||||
moves.remove(j);
|
|
||||||
if (moves.isEmpty()) {
|
|
||||||
mMovesList.remove(i);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
|
|
||||||
ArrayList<RecyclerView.ViewHolder> additions = mAdditionsList.get(i);
|
|
||||||
if (additions.remove(item)) {
|
|
||||||
view.setAlpha(1);
|
|
||||||
dispatchAddFinished(item);
|
|
||||||
if (additions.isEmpty()) {
|
|
||||||
mAdditionsList.remove(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// animations should be ended by the cancel above.
|
|
||||||
//noinspection PointlessBooleanExpression,ConstantConditions
|
|
||||||
if (mRemoveAnimations.remove(item) && DEBUG) {
|
|
||||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
|
||||||
+ "mRemoveAnimations list");
|
|
||||||
}
|
|
||||||
|
|
||||||
//noinspection PointlessBooleanExpression,ConstantConditions
|
|
||||||
if (mAddAnimations.remove(item) && DEBUG) {
|
|
||||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
|
||||||
+ "mAddAnimations list");
|
|
||||||
}
|
|
||||||
|
|
||||||
//noinspection PointlessBooleanExpression,ConstantConditions
|
|
||||||
if (mChangeAnimations.remove(item) && DEBUG) {
|
|
||||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
|
||||||
+ "mChangeAnimations list");
|
|
||||||
}
|
|
||||||
|
|
||||||
//noinspection PointlessBooleanExpression,ConstantConditions
|
|
||||||
if (mMoveAnimations.remove(item) && DEBUG) {
|
|
||||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
|
||||||
+ "mMoveAnimations list");
|
|
||||||
}
|
|
||||||
dispatchFinishedWhenDone();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void resetAnimation(RecyclerView.ViewHolder holder) {
|
|
||||||
if (sDefaultInterpolator == null) {
|
|
||||||
sDefaultInterpolator = new ValueAnimator().getInterpolator();
|
|
||||||
}
|
|
||||||
holder.itemView.animate().setInterpolator(sDefaultInterpolator);
|
|
||||||
endAnimation(holder);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isRunning() {
|
|
||||||
return (!mPendingAdditions.isEmpty()
|
|
||||||
|| !mPendingChanges.isEmpty()
|
|
||||||
|| !mPendingMoves.isEmpty()
|
|
||||||
|| !mPendingRemovals.isEmpty()
|
|
||||||
|| !mMoveAnimations.isEmpty()
|
|
||||||
|| !mRemoveAnimations.isEmpty()
|
|
||||||
|| !mAddAnimations.isEmpty()
|
|
||||||
|| !mChangeAnimations.isEmpty()
|
|
||||||
|| !mMovesList.isEmpty()
|
|
||||||
|| !mAdditionsList.isEmpty()
|
|
||||||
|| !mChangesList.isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check the state of currently pending and running animations. If there are none
|
|
||||||
* pending/running, call {@link #dispatchAnimationsFinished()} to notify any
|
|
||||||
* listeners.
|
|
||||||
*/
|
|
||||||
void dispatchFinishedWhenDone() {
|
|
||||||
if (!isRunning()) {
|
|
||||||
dispatchAnimationsFinished();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void endAnimations() {
|
|
||||||
int count = mPendingMoves.size();
|
|
||||||
for (int i = count - 1; i >= 0; i--) {
|
|
||||||
SafeDefaultItemAnimator.MoveInfo item = mPendingMoves.get(i);
|
|
||||||
View view = item.holder.itemView;
|
|
||||||
view.setTranslationY(0);
|
|
||||||
view.setTranslationX(0);
|
|
||||||
dispatchMoveFinished(item.holder);
|
|
||||||
mPendingMoves.remove(i);
|
|
||||||
}
|
|
||||||
count = mPendingRemovals.size();
|
|
||||||
for (int i = count - 1; i >= 0; i--) {
|
|
||||||
RecyclerView.ViewHolder item = mPendingRemovals.get(i);
|
|
||||||
dispatchRemoveFinished(item);
|
|
||||||
mPendingRemovals.remove(i);
|
|
||||||
}
|
|
||||||
count = mPendingAdditions.size();
|
|
||||||
for (int i = count - 1; i >= 0; i--) {
|
|
||||||
RecyclerView.ViewHolder item = mPendingAdditions.get(i);
|
|
||||||
item.itemView.setAlpha(1);
|
|
||||||
dispatchAddFinished(item);
|
|
||||||
mPendingAdditions.remove(i);
|
|
||||||
}
|
|
||||||
count = mPendingChanges.size();
|
|
||||||
for (int i = count - 1; i >= 0; i--) {
|
|
||||||
endChangeAnimationIfNecessary(mPendingChanges.get(i));
|
|
||||||
}
|
|
||||||
mPendingChanges.clear();
|
|
||||||
if (!isRunning()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int listCount = mMovesList.size();
|
|
||||||
for (int i = listCount - 1; i >= 0; i--) {
|
|
||||||
ArrayList<SafeDefaultItemAnimator.MoveInfo> moves = mMovesList.get(i);
|
|
||||||
count = moves.size();
|
|
||||||
for (int j = count - 1; j >= 0; j--) {
|
|
||||||
SafeDefaultItemAnimator.MoveInfo moveInfo = moves.get(j);
|
|
||||||
RecyclerView.ViewHolder item = moveInfo.holder;
|
|
||||||
View view = item.itemView;
|
|
||||||
view.setTranslationY(0);
|
|
||||||
view.setTranslationX(0);
|
|
||||||
dispatchMoveFinished(moveInfo.holder);
|
|
||||||
moves.remove(j);
|
|
||||||
if (moves.isEmpty()) {
|
|
||||||
mMovesList.remove(moves);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
listCount = mAdditionsList.size();
|
|
||||||
for (int i = listCount - 1; i >= 0; i--) {
|
|
||||||
ArrayList<RecyclerView.ViewHolder> additions = mAdditionsList.get(i);
|
|
||||||
count = additions.size();
|
|
||||||
for (int j = count - 1; j >= 0; j--) {
|
|
||||||
RecyclerView.ViewHolder item = additions.get(j);
|
|
||||||
View view = item.itemView;
|
|
||||||
view.setAlpha(1);
|
|
||||||
dispatchAddFinished(item);
|
|
||||||
additions.remove(j);
|
|
||||||
if (additions.isEmpty()) {
|
|
||||||
mAdditionsList.remove(additions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
listCount = mChangesList.size();
|
|
||||||
for (int i = listCount - 1; i >= 0; i--) {
|
|
||||||
ArrayList<SafeDefaultItemAnimator.ChangeInfo> changes = mChangesList.get(i);
|
|
||||||
count = changes.size();
|
|
||||||
for (int j = count - 1; j >= 0; j--) {
|
|
||||||
endChangeAnimationIfNecessary(changes.get(j));
|
|
||||||
if (changes.isEmpty()) {
|
|
||||||
mChangesList.remove(changes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelAll(mRemoveAnimations);
|
|
||||||
cancelAll(mMoveAnimations);
|
|
||||||
cancelAll(mAddAnimations);
|
|
||||||
cancelAll(mChangeAnimations);
|
|
||||||
|
|
||||||
dispatchAnimationsFinished();
|
|
||||||
}
|
|
||||||
|
|
||||||
void cancelAll(List<RecyclerView.ViewHolder> viewHolders) {
|
|
||||||
for (int i = viewHolders.size() - 1; i >= 0; i--) {
|
|
||||||
viewHolders.get(i).itemView.animate().cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
* <p>
|
|
||||||
* If the payload list is not empty, SafeDefaultItemAnimator returns <code>true</code>.
|
|
||||||
* When this is the case:
|
|
||||||
* <ul>
|
|
||||||
* <li>If you override {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)}, both
|
|
||||||
* ViewHolder arguments will be the same instance.
|
|
||||||
* </li>
|
|
||||||
* <li>
|
|
||||||
* If you are not overriding {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)},
|
|
||||||
* then SafeDefaultItemAnimator will call {@link #animateMove(RecyclerView.ViewHolder, int, int, int, int)} and
|
|
||||||
* run a move animation instead.
|
|
||||||
* </li>
|
|
||||||
* </ul>
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder,
|
|
||||||
@NonNull List<Object> payloads) {
|
|
||||||
return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,609 @@
|
||||||
|
package com.habitrpg.android.habitica.ui.helpers
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.animation.AnimatorListenerAdapter
|
||||||
|
import android.animation.TimeInterpolator
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
|
import com.habitrpg.shared.habitica.LogLevel
|
||||||
|
import com.habitrpg.shared.habitica.Logger
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by phillip on 02.10.17.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class SafeDefaultItemAnimator : SimpleItemAnimator() {
|
||||||
|
|
||||||
|
private val pendingRemovals = ArrayList<RecyclerView.ViewHolder>()
|
||||||
|
private val pendingAdditions = ArrayList<RecyclerView.ViewHolder>()
|
||||||
|
private val pendingMoves = ArrayList<MoveInfo>()
|
||||||
|
private val pendingChanges = ArrayList<ChangeInfo>()
|
||||||
|
|
||||||
|
private val additionsList = ArrayList<ArrayList<RecyclerView.ViewHolder>>()
|
||||||
|
private val movesList = ArrayList<ArrayList<MoveInfo>>()
|
||||||
|
private val changesList = ArrayList<ArrayList<ChangeInfo>>()
|
||||||
|
|
||||||
|
private val addAnimations = ArrayList<RecyclerView.ViewHolder>()
|
||||||
|
private val moveAnimations = ArrayList<RecyclerView.ViewHolder>()
|
||||||
|
private val removeAnimations = ArrayList<RecyclerView.ViewHolder>()
|
||||||
|
private val changeAnimations = ArrayList<RecyclerView.ViewHolder>()
|
||||||
|
|
||||||
|
var skipAnimations: Boolean = false
|
||||||
|
|
||||||
|
private class MoveInfo internal constructor(var holder: RecyclerView.ViewHolder?, internal var fromX: Int, internal var fromY: Int, internal var toX: Int, internal var toY: Int)
|
||||||
|
|
||||||
|
private class ChangeInfo private constructor(internal var oldHolder: RecyclerView.ViewHolder?, internal var newHolder: RecyclerView.ViewHolder?) {
|
||||||
|
internal var fromX: Int = 0
|
||||||
|
internal var fromY: Int = 0
|
||||||
|
internal var toX: Int = 0
|
||||||
|
internal var toY: Int = 0
|
||||||
|
|
||||||
|
internal constructor(oldHolder: RecyclerView.ViewHolder, newHolder: RecyclerView.ViewHolder,
|
||||||
|
fromX: Int, fromY: Int, toX: Int, toY: Int) : this(oldHolder, newHolder) {
|
||||||
|
this.fromX = fromX
|
||||||
|
this.fromY = fromY
|
||||||
|
this.toX = toX
|
||||||
|
this.toY = toY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return ("ChangeInfo{"
|
||||||
|
+ "oldHolder=" + oldHolder
|
||||||
|
+ ", newHolder=" + newHolder
|
||||||
|
+ ", fromX=" + fromX
|
||||||
|
+ ", fromY=" + fromY
|
||||||
|
+ ", toX=" + toX
|
||||||
|
+ ", toY=" + toY
|
||||||
|
+ '}'.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun runPendingAnimations() {
|
||||||
|
val removalsPending = pendingRemovals.isNotEmpty()
|
||||||
|
val movesPending = pendingMoves.isNotEmpty()
|
||||||
|
val changesPending = pendingChanges.isNotEmpty()
|
||||||
|
val additionsPending = pendingAdditions.isNotEmpty()
|
||||||
|
if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
|
||||||
|
// nothing to animate
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// First, remove stuff
|
||||||
|
for (holder in pendingRemovals) {
|
||||||
|
animateRemoveImpl(holder)
|
||||||
|
}
|
||||||
|
if (skipAnimations) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pendingRemovals.clear()
|
||||||
|
// Next, move stuff
|
||||||
|
if (movesPending) {
|
||||||
|
val moves = ArrayList(pendingMoves)
|
||||||
|
movesList.add(moves)
|
||||||
|
pendingMoves.clear()
|
||||||
|
val mover = Runnable {
|
||||||
|
for (moveInfo in moves) {
|
||||||
|
moveInfo.holder?.let { animateMoveImpl(it, moveInfo.fromX, moveInfo.fromY,
|
||||||
|
moveInfo.toX, moveInfo.toY) }
|
||||||
|
}
|
||||||
|
moves.clear()
|
||||||
|
movesList.remove(moves)
|
||||||
|
}
|
||||||
|
if (removalsPending) {
|
||||||
|
val holder = moves[0].holder
|
||||||
|
if (holder != null) {
|
||||||
|
val view = holder.itemView
|
||||||
|
ViewCompat.postOnAnimationDelayed(view, mover, removeDuration)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mover.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Next, change stuff, to run in parallel with move animations
|
||||||
|
if (changesPending) {
|
||||||
|
|
||||||
|
val changes = ArrayList(pendingChanges)
|
||||||
|
changesList.add(changes)
|
||||||
|
pendingChanges.clear()
|
||||||
|
val changer = Runnable {
|
||||||
|
for (change in changes) {
|
||||||
|
animateChangeImpl(change)
|
||||||
|
}
|
||||||
|
changes.clear()
|
||||||
|
changesList.remove(changes)
|
||||||
|
}
|
||||||
|
if (removalsPending) {
|
||||||
|
val holder = changes[0].oldHolder
|
||||||
|
if (holder != null) {
|
||||||
|
ViewCompat.postOnAnimationDelayed(holder.itemView, changer, removeDuration)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
changer.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Next, add stuff
|
||||||
|
if (additionsPending) {
|
||||||
|
val additions = ArrayList(pendingAdditions)
|
||||||
|
additionsList.add(additions)
|
||||||
|
pendingAdditions.clear()
|
||||||
|
val adder = Runnable {
|
||||||
|
for (holder in additions) {
|
||||||
|
animateAddImpl(holder)
|
||||||
|
}
|
||||||
|
additions.clear()
|
||||||
|
additionsList.remove(additions)
|
||||||
|
}
|
||||||
|
if (removalsPending || movesPending || changesPending) {
|
||||||
|
val removeDuration = if (removalsPending) removeDuration else 0
|
||||||
|
val moveDuration = if (movesPending) moveDuration else 0
|
||||||
|
val changeDuration = if (changesPending) changeDuration else 0
|
||||||
|
val totalDelay = removeDuration + moveDuration.coerceAtLeast(changeDuration)
|
||||||
|
if (additions[0] != null) {
|
||||||
|
val view = additions[0].itemView
|
||||||
|
ViewCompat.postOnAnimationDelayed(view, adder, totalDelay)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
adder.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun animateRemove(holder: RecyclerView.ViewHolder): Boolean {
|
||||||
|
resetAnimation(holder)
|
||||||
|
pendingRemovals.add(holder)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateRemoveImpl(holder: RecyclerView.ViewHolder) {
|
||||||
|
val view = holder.itemView
|
||||||
|
val animation = view.animate()
|
||||||
|
removeAnimations.add(holder)
|
||||||
|
animation.setDuration(removeDuration).alpha(0f).setListener(
|
||||||
|
object : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationStart(animator: Animator) {
|
||||||
|
dispatchRemoveStarting(holder)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(animator: Animator) {
|
||||||
|
animation.setListener(null)
|
||||||
|
view.alpha = 1f
|
||||||
|
dispatchRemoveFinished(holder)
|
||||||
|
removeAnimations.remove(holder)
|
||||||
|
dispatchFinishedWhenDone()
|
||||||
|
}
|
||||||
|
}).start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun animateAdd(holder: RecyclerView.ViewHolder): Boolean {
|
||||||
|
resetAnimation(holder)
|
||||||
|
holder.itemView.alpha = 0f
|
||||||
|
pendingAdditions.add(holder)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateAddImpl(holder: RecyclerView.ViewHolder) {
|
||||||
|
val view = holder.itemView
|
||||||
|
val animation = view.animate()
|
||||||
|
addAnimations.add(holder)
|
||||||
|
animation.alpha(1f).setDuration(addDuration)
|
||||||
|
.setListener(object : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationStart(animator: Animator) {
|
||||||
|
dispatchAddStarting(holder)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationCancel(animator: Animator) {
|
||||||
|
view.alpha = 1f
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(animator: Animator) {
|
||||||
|
animation.setListener(null)
|
||||||
|
dispatchAddFinished(holder)
|
||||||
|
addAnimations.remove(holder)
|
||||||
|
dispatchFinishedWhenDone()
|
||||||
|
}
|
||||||
|
}).start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun animateMove(holder: RecyclerView.ViewHolder, fromX: Int, fromY: Int,
|
||||||
|
toX: Int, toY: Int): Boolean {
|
||||||
|
var newFromX = fromX
|
||||||
|
var newFromY = fromY
|
||||||
|
val view = holder.itemView
|
||||||
|
newFromX += holder.itemView.translationX.toInt()
|
||||||
|
newFromY += holder.itemView.translationY.toInt()
|
||||||
|
resetAnimation(holder)
|
||||||
|
val deltaX = toX - newFromX
|
||||||
|
val deltaY = toY - newFromY
|
||||||
|
if (deltaX == 0 && deltaY == 0) {
|
||||||
|
dispatchMoveFinished(holder)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (deltaX != 0) {
|
||||||
|
view.translationX = (-deltaX).toFloat()
|
||||||
|
}
|
||||||
|
if (deltaY != 0) {
|
||||||
|
view.translationY = (-deltaY).toFloat()
|
||||||
|
}
|
||||||
|
Logger.log(LogLevel.INFO, "Moving1", "$toX, $fromX, $deltaX")
|
||||||
|
Logger.log(LogLevel.INFO, "Moving1", "$toX, $fromX, $deltaX")
|
||||||
|
pendingMoves.add(MoveInfo(holder, newFromX, newFromY, toX, toY))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateMoveImpl(holder: RecyclerView.ViewHolder, fromX: Int, fromY: Int, toX: Int, toY: Int) {
|
||||||
|
val view = holder.itemView
|
||||||
|
val deltaX = toX - fromX
|
||||||
|
val deltaY = toY - fromY
|
||||||
|
if (deltaX != 0) {
|
||||||
|
view.animate().translationX(0f)
|
||||||
|
}
|
||||||
|
if (deltaY != 0) {
|
||||||
|
view.animate().translationY(0f)
|
||||||
|
}
|
||||||
|
Logger.log(LogLevel.INFO, "Moving", "$toX, $fromX, $deltaX")
|
||||||
|
Logger.log(LogLevel.INFO, "Moving", "$toX, $fromX, $deltaX")
|
||||||
|
// vpas are canceled (and can't end them. why?)
|
||||||
|
// need listener functionality in VPACompat for this. Ick.
|
||||||
|
val animation = view.animate()
|
||||||
|
moveAnimations.add(holder)
|
||||||
|
animation.setDuration(moveDuration).setListener(object : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationStart(animator: Animator) {
|
||||||
|
dispatchMoveStarting(holder)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationCancel(animator: Animator) {
|
||||||
|
if (deltaX != 0) {
|
||||||
|
view.translationX = 0f
|
||||||
|
}
|
||||||
|
if (deltaY != 0) {
|
||||||
|
view.translationY = 0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(animator: Animator) {
|
||||||
|
animation.setListener(null)
|
||||||
|
dispatchMoveFinished(holder)
|
||||||
|
moveAnimations.remove(holder)
|
||||||
|
dispatchFinishedWhenDone()
|
||||||
|
}
|
||||||
|
}).start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun animateChange(oldHolder: RecyclerView.ViewHolder, newHolder: RecyclerView.ViewHolder?,
|
||||||
|
fromX: Int, fromY: Int, toX: Int, toY: Int): Boolean {
|
||||||
|
if (oldHolder === newHolder) {
|
||||||
|
// Don't know how to run change animations when the same view holder is re-used.
|
||||||
|
// run a move animation to handle position changes.
|
||||||
|
return animateMove(oldHolder, fromX, fromY, toX, toY)
|
||||||
|
}
|
||||||
|
val prevTranslationX = oldHolder.itemView.translationX
|
||||||
|
val prevTranslationY = oldHolder.itemView.translationY
|
||||||
|
val prevAlpha = oldHolder.itemView.alpha
|
||||||
|
resetAnimation(oldHolder)
|
||||||
|
val deltaX = (toX.toFloat() - fromX.toFloat() - prevTranslationX).toInt()
|
||||||
|
val deltaY = (toY.toFloat() - fromY.toFloat() - prevTranslationY).toInt()
|
||||||
|
// recover prev translation state after ending animation
|
||||||
|
oldHolder.itemView.translationX = prevTranslationX
|
||||||
|
oldHolder.itemView.translationY = prevTranslationY
|
||||||
|
oldHolder.itemView.alpha = prevAlpha
|
||||||
|
if (newHolder != null) {
|
||||||
|
// carry over translation values
|
||||||
|
resetAnimation(newHolder)
|
||||||
|
newHolder.itemView.translationX = (-deltaX).toFloat()
|
||||||
|
newHolder.itemView.translationY = (-deltaY).toFloat()
|
||||||
|
newHolder.itemView.alpha = 0f
|
||||||
|
}
|
||||||
|
Logger.log(LogLevel.INFO, "Changing", "$toX, $fromX, $deltaX")
|
||||||
|
Logger.log(LogLevel.INFO, "Changing", "$toX, $fromX, $deltaX")
|
||||||
|
newHolder?.let { pendingChanges.add(ChangeInfo(oldHolder, it, fromX, fromY, toX, toY)) }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateChangeImpl(changeInfo: ChangeInfo) {
|
||||||
|
val holder = changeInfo.oldHolder
|
||||||
|
val view = holder?.itemView
|
||||||
|
val newHolder = changeInfo.newHolder
|
||||||
|
val newView = newHolder?.itemView
|
||||||
|
if (view != null) {
|
||||||
|
val oldViewAnim = view.animate().setDuration(
|
||||||
|
changeDuration)
|
||||||
|
changeInfo.oldHolder?.let { changeAnimations.add(it) }
|
||||||
|
oldViewAnim.translationX((changeInfo.toX - changeInfo.fromX).toFloat())
|
||||||
|
oldViewAnim.translationY((changeInfo.toY - changeInfo.fromY).toFloat())
|
||||||
|
oldViewAnim.alpha(0f).setListener(object : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationStart(animator: Animator) {
|
||||||
|
dispatchChangeStarting(changeInfo.oldHolder, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(animator: Animator) {
|
||||||
|
oldViewAnim.setListener(null)
|
||||||
|
view.alpha = 1f
|
||||||
|
view.translationX = 0f
|
||||||
|
view.translationY = 0f
|
||||||
|
dispatchChangeFinished(changeInfo.oldHolder, true)
|
||||||
|
changeInfo.oldHolder?.let { changeAnimations.add(it) }
|
||||||
|
dispatchFinishedWhenDone()
|
||||||
|
}
|
||||||
|
}).start()
|
||||||
|
}
|
||||||
|
if (newView != null) {
|
||||||
|
val newViewAnimation = newView.animate()
|
||||||
|
changeInfo.newHolder?.let { changeAnimations.add(it) }
|
||||||
|
newViewAnimation.translationX(0f).translationY(0f).setDuration(changeDuration)
|
||||||
|
.alpha(1f).setListener(object : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationStart(animator: Animator) {
|
||||||
|
dispatchChangeStarting(changeInfo.newHolder, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(animator: Animator) {
|
||||||
|
newViewAnimation.setListener(null)
|
||||||
|
newView.alpha = 1f
|
||||||
|
newView.translationX = 0f
|
||||||
|
newView.translationY = 0f
|
||||||
|
dispatchChangeFinished(changeInfo.newHolder, false)
|
||||||
|
changeInfo.newHolder?.let { changeAnimations.add(it) }
|
||||||
|
dispatchFinishedWhenDone()
|
||||||
|
}
|
||||||
|
}).start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun endChangeAnimation(infoList: MutableList<ChangeInfo>, item: RecyclerView.ViewHolder) {
|
||||||
|
for (i in infoList.indices.reversed()) {
|
||||||
|
val changeInfo = infoList[i]
|
||||||
|
if (endChangeAnimationIfNecessary(changeInfo, item)) {
|
||||||
|
if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
|
||||||
|
infoList.remove(changeInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun endChangeAnimationIfNecessary(changeInfo: ChangeInfo) {
|
||||||
|
if (changeInfo.oldHolder != null) {
|
||||||
|
endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder)
|
||||||
|
}
|
||||||
|
if (changeInfo.newHolder != null) {
|
||||||
|
endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun endChangeAnimationIfNecessary(changeInfo: ChangeInfo, item: RecyclerView.ViewHolder?): Boolean {
|
||||||
|
var oldItem = false
|
||||||
|
when {
|
||||||
|
changeInfo.newHolder === item -> changeInfo.newHolder = null
|
||||||
|
changeInfo.oldHolder === item -> {
|
||||||
|
changeInfo.oldHolder = null
|
||||||
|
oldItem = true
|
||||||
|
}
|
||||||
|
else -> return false
|
||||||
|
}
|
||||||
|
item?.itemView?.alpha = 1f
|
||||||
|
item?.itemView?.translationX = 0f
|
||||||
|
item?.itemView?.translationY = 0f
|
||||||
|
dispatchChangeFinished(item, oldItem)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun endAnimation(item: RecyclerView.ViewHolder) {
|
||||||
|
val view = item.itemView
|
||||||
|
// this will trigger end callback which should set properties to their target values.
|
||||||
|
view.animate().cancel()
|
||||||
|
// TODO if some other animations are chained to end, how do we cancel them as well?
|
||||||
|
for (i in pendingMoves.indices.reversed()) {
|
||||||
|
val moveInfo = pendingMoves[i]
|
||||||
|
if (moveInfo.holder === item) {
|
||||||
|
view.translationY = 0f
|
||||||
|
view.translationX = 0f
|
||||||
|
dispatchMoveFinished(item)
|
||||||
|
pendingMoves.removeAt(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endChangeAnimation(pendingChanges, item)
|
||||||
|
if (pendingRemovals.remove(item)) {
|
||||||
|
view.alpha = 1f
|
||||||
|
dispatchRemoveFinished(item)
|
||||||
|
}
|
||||||
|
if (pendingAdditions.remove(item)) {
|
||||||
|
view.alpha = 1f
|
||||||
|
dispatchAddFinished(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i in changesList.indices.reversed()) {
|
||||||
|
val changes = changesList[i]
|
||||||
|
endChangeAnimation(changes, item)
|
||||||
|
if (changes.isEmpty()) {
|
||||||
|
changesList.removeAt(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i in movesList.indices.reversed()) {
|
||||||
|
val moves = movesList[i]
|
||||||
|
for (j in moves.indices.reversed()) {
|
||||||
|
val moveInfo = moves[j]
|
||||||
|
if (moveInfo.holder === item) {
|
||||||
|
view.translationY = 0f
|
||||||
|
view.translationX = 0f
|
||||||
|
dispatchMoveFinished(item)
|
||||||
|
moves.removeAt(j)
|
||||||
|
if (moves.isEmpty()) {
|
||||||
|
movesList.removeAt(i)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i in additionsList.indices.reversed()) {
|
||||||
|
val additions = additionsList[i]
|
||||||
|
if (additions.remove(item)) {
|
||||||
|
view.alpha = 1f
|
||||||
|
dispatchAddFinished(item)
|
||||||
|
if (additions.isEmpty()) {
|
||||||
|
additionsList.removeAt(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// animations should be ended by the cancel above.
|
||||||
|
check(!(removeAnimations.remove(item) && DEBUG)) { "after animation is cancelled, item should not be in " + "removeAnimations list" }
|
||||||
|
|
||||||
|
check(!(addAnimations.remove(item) && DEBUG)) { "after animation is cancelled, item should not be in " + "addAnimations list" }
|
||||||
|
|
||||||
|
check(!(changeAnimations.remove(item) && DEBUG)) { "after animation is cancelled, item should not be in " + "changeAnimations list" }
|
||||||
|
|
||||||
|
check(!(moveAnimations.remove(item) && DEBUG)) { "after animation is cancelled, item should not be in " + "moveAnimations list" }
|
||||||
|
dispatchFinishedWhenDone()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetAnimation(holder: RecyclerView.ViewHolder) {
|
||||||
|
if (sDefaultInterpolator == null) {
|
||||||
|
sDefaultInterpolator = ValueAnimator().interpolator
|
||||||
|
}
|
||||||
|
holder.itemView.animate().interpolator = sDefaultInterpolator
|
||||||
|
endAnimation(holder)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isRunning(): Boolean {
|
||||||
|
return (pendingAdditions.isNotEmpty()
|
||||||
|
|| pendingChanges.isNotEmpty()
|
||||||
|
|| pendingMoves.isNotEmpty()
|
||||||
|
|| pendingRemovals.isNotEmpty()
|
||||||
|
|| moveAnimations.isNotEmpty()
|
||||||
|
|| removeAnimations.isNotEmpty()
|
||||||
|
|| addAnimations.isNotEmpty()
|
||||||
|
|| changeAnimations.isNotEmpty()
|
||||||
|
|| movesList.isNotEmpty()
|
||||||
|
|| additionsList.isNotEmpty()
|
||||||
|
|| changesList.isNotEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the state of currently pending and running animations. If there are none
|
||||||
|
* pending/running, call [.dispatchAnimationsFinished] to notify any
|
||||||
|
* listeners.
|
||||||
|
*/
|
||||||
|
private fun dispatchFinishedWhenDone() {
|
||||||
|
if (!isRunning) {
|
||||||
|
dispatchAnimationsFinished()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun endAnimations() {
|
||||||
|
var count = pendingMoves.size
|
||||||
|
for (i in count - 1 downTo 0) {
|
||||||
|
val item = pendingMoves[i]
|
||||||
|
val view = item.holder?.itemView
|
||||||
|
view?.translationY = 0f
|
||||||
|
view?.translationX = 0f
|
||||||
|
dispatchMoveFinished(item.holder)
|
||||||
|
pendingMoves.removeAt(i)
|
||||||
|
}
|
||||||
|
count = pendingRemovals.size
|
||||||
|
for (i in count - 1 downTo 0) {
|
||||||
|
val item = pendingRemovals[i]
|
||||||
|
dispatchRemoveFinished(item)
|
||||||
|
pendingRemovals.removeAt(i)
|
||||||
|
}
|
||||||
|
count = pendingAdditions.size
|
||||||
|
for (i in count - 1 downTo 0) {
|
||||||
|
val item = pendingAdditions[i]
|
||||||
|
item.itemView.alpha = 1f
|
||||||
|
dispatchAddFinished(item)
|
||||||
|
pendingAdditions.removeAt(i)
|
||||||
|
}
|
||||||
|
count = pendingChanges.size
|
||||||
|
for (i in count - 1 downTo 0) {
|
||||||
|
endChangeAnimationIfNecessary(pendingChanges[i])
|
||||||
|
}
|
||||||
|
pendingChanges.clear()
|
||||||
|
if (!isRunning) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var listCount = movesList.size
|
||||||
|
for (i in listCount - 1 downTo 0) {
|
||||||
|
val moves = movesList[i]
|
||||||
|
count = moves.size
|
||||||
|
for (j in count - 1 downTo 0) {
|
||||||
|
val moveInfo = moves[j]
|
||||||
|
val item = moveInfo.holder
|
||||||
|
val view = item?.itemView
|
||||||
|
view?.translationY = 0f
|
||||||
|
view?.translationX = 0f
|
||||||
|
dispatchMoveFinished(moveInfo.holder)
|
||||||
|
moves.removeAt(j)
|
||||||
|
if (moves.isEmpty()) {
|
||||||
|
movesList.remove(moves)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listCount = additionsList.size
|
||||||
|
for (i in listCount - 1 downTo 0) {
|
||||||
|
val additions = additionsList[i]
|
||||||
|
count = additions.size
|
||||||
|
for (j in count - 1 downTo 0) {
|
||||||
|
val item = additions[j]
|
||||||
|
val view = item.itemView
|
||||||
|
view.alpha = 1f
|
||||||
|
dispatchAddFinished(item)
|
||||||
|
additions.removeAt(j)
|
||||||
|
if (additions.isEmpty()) {
|
||||||
|
additionsList.remove(additions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listCount = changesList.size
|
||||||
|
for (i in listCount - 1 downTo 0) {
|
||||||
|
val changes = changesList[i]
|
||||||
|
count = changes.size
|
||||||
|
for (j in count - 1 downTo 0) {
|
||||||
|
endChangeAnimationIfNecessary(changes[j])
|
||||||
|
if (changes.isEmpty()) {
|
||||||
|
changesList.remove(changes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelAll(removeAnimations)
|
||||||
|
cancelAll(moveAnimations)
|
||||||
|
cancelAll(addAnimations)
|
||||||
|
cancelAll(changeAnimations)
|
||||||
|
|
||||||
|
dispatchAnimationsFinished()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelAll(viewHolders: List<RecyclerView.ViewHolder>) {
|
||||||
|
for (i in viewHolders.indices.reversed()) {
|
||||||
|
viewHolders[i].itemView.animate().cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* If the payload list is not empty, SafeDefaultItemAnimator returns `true`.
|
||||||
|
* When this is the case:
|
||||||
|
*
|
||||||
|
* * If you override [.animateChange], both
|
||||||
|
* ViewHolder arguments will be the same instance.
|
||||||
|
*
|
||||||
|
* *
|
||||||
|
* If you are not overriding [.animateChange],
|
||||||
|
* then SafeDefaultItemAnimator will call [.animateMove] and
|
||||||
|
* run a move animation instead.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
override fun canReuseUpdatedViewHolder(viewHolder: RecyclerView.ViewHolder,
|
||||||
|
payloads: List<Any>): Boolean {
|
||||||
|
return payloads.isNotEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val DEBUG = false
|
||||||
|
|
||||||
|
private var sDefaultInterpolator: TimeInterpolator? = null
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue