使用 ItemTouchHelper.Callback 实现 "Bounce Back" RecyclerView 动画
Implementing "Bounce Back" RecyclerView Animation with ItemTouchHelper.Callback
我正在尝试在我的 RecyclerView
行中创建一个动画,其中 Helper
class 扩展 ItemTouchHelper.Callback
.
ItemTouchHelper
的默认 "swipe" 行为是,如果以足够的速度滑动一行,它就会从视图中消失(即滑动到消失)。就我而言,我想创建一个动画,使该行在发生此类事件时直接弹回。
我已经能够创建一个 ValueAnimator
,如果该行被滑出屏幕,它会以动画方式重新显示该行。但是,问题是我无法让父 ItemTouchHelper
class 认识到它存储的 x
值已经从行的宽度改变(因此它完全在屏幕外)到 0
(以便它回到屏幕上)。
这意味着每当我在完成我的自定义动画后与该行进行交互时,它的行为就好像该行仍然在屏幕外,即使它是完全可见的。这通常意味着按下它意味着行从 0
的 X
位置跳到行宽的 X
位置,因此从屏幕边缘再次动画。在完全重置并再次正常运行之前,它会执行几次。
此视频将有助于说明问题。由于前两次滑动不会在屏幕外滑动,因此默认行为有效。然而,在该行从屏幕上滑出并弹回后,点击该行显示它从右边缘返回:
这是我用来执行动画的代码:
public class CustomItemTouchHelper extends ItemTouchHelper.Callback {
private RecoverAnimation ra;
private boolean animatorRunning;
//...
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
final float per = (dX/viewHolder.itemView.getWidth());
if (per > 0.8F && !animatorRunning && !isCurrentlyActive) {
ra = new RecoverAnimation(c, recyclerView, viewHolder, ItemTouchHelper.ANIMATION_TYPE_SWIPE_CANCEL, actionState, dX, dY, 0, 0);
ra.start();
} else {
getDefaultUIUtil().onDraw(c, recyclerView, viewHolder.itemView, dX, dY, actionState, isCurrentlyActive);
}
}
//...
请注意,此内部 class、RecoverAnimation
是从 ItemTouchHelper
源代码中提取的:
private class RecoverAnimation implements AnimatorListenerCompat {
final float mStartDx;
final float mStartDy;
final float mTargetX;
final float mTargetY;
final Canvas mCanvas;
final RecyclerView mRecyclerView;
final RecyclerView.ViewHolder mViewHolder;
final int mActionState;
private final ValueAnimatorCompat mValueAnimator;
float mX;
float mY;
// if user starts touching a recovering view, we put it into interaction mode again,
// instantly.
boolean mOverridden = false;
private boolean mEnded = false;
private float mFraction;
public RecoverAnimation(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
int actionState, float startDx, float startDy, float targetX, float targetY) {
mRecyclerView = recyclerView;
mCanvas = c;
mActionState = actionState;
mViewHolder = viewHolder;
mStartDx = startDx;
mStartDy = startDy;
mTargetX = targetX;
mTargetY = targetY;
mValueAnimator = AnimatorCompatHelper.emptyValueAnimator();
mValueAnimator.addUpdateListener(
new AnimatorUpdateListenerCompat() {
@Override
public void onAnimationUpdate(ValueAnimatorCompat animation) {
setFraction(animation.getAnimatedFraction());
update();
}
});
mValueAnimator.setTarget(viewHolder.itemView);
mValueAnimator.addListener(this);
setFraction(0f);
}
public void setDuration(long duration) {
mValueAnimator.setDuration(duration);
}
public void start() {
animatorRunning = true;
mViewHolder.setIsRecyclable(false);
mValueAnimator.start();
}
public void cancel() {
mValueAnimator.cancel();
}
public void setFraction(float fraction) {
mFraction = fraction;
}
/**
* We run updates on onDraw method but use the fraction from animator callback.
* This way, we can sync translate x/y values w/ the animators to avoid one-off frames.
*/
public void update() {
if (mStartDx == mTargetX) {
mX = ViewCompat.getTranslationX(mViewHolder.itemView);
} else {
mX = mStartDx + mFraction * (mTargetX - mStartDx);
}
if (mStartDy == mTargetY) {
mY = ViewCompat.getTranslationY(mViewHolder.itemView);
} else {
mY = mStartDy + mFraction * (mTargetY - mStartDy);
}
getDefaultUIUtil().onDraw(mCanvas, mRecyclerView, mViewHolder.itemView, mX, mY, mActionState, false);
}
@Override
public void onAnimationStart(ValueAnimatorCompat animation) {
}
@Override
public void onAnimationEnd(ValueAnimatorCompat animation) {
animatorRunning = false;
mEnded = true;
getDefaultUIUtil().onDraw(mCanvas, mRecyclerView, mViewHolder.itemView, 0, 0, ItemTouchHelper.ACTION_STATE_IDLE, false);
getDefaultUIUtil().clearView(mViewHolder.itemView);
}
@Override
public void onAnimationCancel(ValueAnimatorCompat animation) {
setFraction(1f); //make sure we recover the view's state.
}
@Override
public void onAnimationRepeat(ValueAnimatorCompat animation) {
}
}
我能做些什么来解决这个问题,还是根本不可能(还)?
以下解决方案将在不实施您的自定义 RecoverAnimation 的情况下弹回 recyclerview 项目 class。
public class CustomItemTouchHelper extends ItemTouchHelper.Callback {
private RecoverAnimation ra;
private boolean animatorRunning;
private boolean swipeBack = false;
//...
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
recyclerView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event)
{
if(event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL)
{
swipeBack = true;
}
else
{
swipeBack = false;
}
return false;
}
});
}
@Override
public int convertToAbsoluteDirection(int flags, int layoutDirection) {
return swipeBack ? 0 : super.convertToAbsoluteDirection(flags, layoutDirection);
}
//...
我正在尝试在我的 RecyclerView
行中创建一个动画,其中 Helper
class 扩展 ItemTouchHelper.Callback
.
ItemTouchHelper
的默认 "swipe" 行为是,如果以足够的速度滑动一行,它就会从视图中消失(即滑动到消失)。就我而言,我想创建一个动画,使该行在发生此类事件时直接弹回。
我已经能够创建一个 ValueAnimator
,如果该行被滑出屏幕,它会以动画方式重新显示该行。但是,问题是我无法让父 ItemTouchHelper
class 认识到它存储的 x
值已经从行的宽度改变(因此它完全在屏幕外)到 0
(以便它回到屏幕上)。
这意味着每当我在完成我的自定义动画后与该行进行交互时,它的行为就好像该行仍然在屏幕外,即使它是完全可见的。这通常意味着按下它意味着行从 0
的 X
位置跳到行宽的 X
位置,因此从屏幕边缘再次动画。在完全重置并再次正常运行之前,它会执行几次。
此视频将有助于说明问题。由于前两次滑动不会在屏幕外滑动,因此默认行为有效。然而,在该行从屏幕上滑出并弹回后,点击该行显示它从右边缘返回:
这是我用来执行动画的代码:
public class CustomItemTouchHelper extends ItemTouchHelper.Callback {
private RecoverAnimation ra;
private boolean animatorRunning;
//...
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
final float per = (dX/viewHolder.itemView.getWidth());
if (per > 0.8F && !animatorRunning && !isCurrentlyActive) {
ra = new RecoverAnimation(c, recyclerView, viewHolder, ItemTouchHelper.ANIMATION_TYPE_SWIPE_CANCEL, actionState, dX, dY, 0, 0);
ra.start();
} else {
getDefaultUIUtil().onDraw(c, recyclerView, viewHolder.itemView, dX, dY, actionState, isCurrentlyActive);
}
}
//...
请注意,此内部 class、RecoverAnimation
是从 ItemTouchHelper
源代码中提取的:
private class RecoverAnimation implements AnimatorListenerCompat {
final float mStartDx;
final float mStartDy;
final float mTargetX;
final float mTargetY;
final Canvas mCanvas;
final RecyclerView mRecyclerView;
final RecyclerView.ViewHolder mViewHolder;
final int mActionState;
private final ValueAnimatorCompat mValueAnimator;
float mX;
float mY;
// if user starts touching a recovering view, we put it into interaction mode again,
// instantly.
boolean mOverridden = false;
private boolean mEnded = false;
private float mFraction;
public RecoverAnimation(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
int actionState, float startDx, float startDy, float targetX, float targetY) {
mRecyclerView = recyclerView;
mCanvas = c;
mActionState = actionState;
mViewHolder = viewHolder;
mStartDx = startDx;
mStartDy = startDy;
mTargetX = targetX;
mTargetY = targetY;
mValueAnimator = AnimatorCompatHelper.emptyValueAnimator();
mValueAnimator.addUpdateListener(
new AnimatorUpdateListenerCompat() {
@Override
public void onAnimationUpdate(ValueAnimatorCompat animation) {
setFraction(animation.getAnimatedFraction());
update();
}
});
mValueAnimator.setTarget(viewHolder.itemView);
mValueAnimator.addListener(this);
setFraction(0f);
}
public void setDuration(long duration) {
mValueAnimator.setDuration(duration);
}
public void start() {
animatorRunning = true;
mViewHolder.setIsRecyclable(false);
mValueAnimator.start();
}
public void cancel() {
mValueAnimator.cancel();
}
public void setFraction(float fraction) {
mFraction = fraction;
}
/**
* We run updates on onDraw method but use the fraction from animator callback.
* This way, we can sync translate x/y values w/ the animators to avoid one-off frames.
*/
public void update() {
if (mStartDx == mTargetX) {
mX = ViewCompat.getTranslationX(mViewHolder.itemView);
} else {
mX = mStartDx + mFraction * (mTargetX - mStartDx);
}
if (mStartDy == mTargetY) {
mY = ViewCompat.getTranslationY(mViewHolder.itemView);
} else {
mY = mStartDy + mFraction * (mTargetY - mStartDy);
}
getDefaultUIUtil().onDraw(mCanvas, mRecyclerView, mViewHolder.itemView, mX, mY, mActionState, false);
}
@Override
public void onAnimationStart(ValueAnimatorCompat animation) {
}
@Override
public void onAnimationEnd(ValueAnimatorCompat animation) {
animatorRunning = false;
mEnded = true;
getDefaultUIUtil().onDraw(mCanvas, mRecyclerView, mViewHolder.itemView, 0, 0, ItemTouchHelper.ACTION_STATE_IDLE, false);
getDefaultUIUtil().clearView(mViewHolder.itemView);
}
@Override
public void onAnimationCancel(ValueAnimatorCompat animation) {
setFraction(1f); //make sure we recover the view's state.
}
@Override
public void onAnimationRepeat(ValueAnimatorCompat animation) {
}
}
我能做些什么来解决这个问题,还是根本不可能(还)?
以下解决方案将在不实施您的自定义 RecoverAnimation 的情况下弹回 recyclerview 项目 class。
public class CustomItemTouchHelper extends ItemTouchHelper.Callback {
private RecoverAnimation ra;
private boolean animatorRunning;
private boolean swipeBack = false;
//...
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
recyclerView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event)
{
if(event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL)
{
swipeBack = true;
}
else
{
swipeBack = false;
}
return false;
}
});
}
@Override
public int convertToAbsoluteDirection(int flags, int layoutDirection) {
return swipeBack ? 0 : super.convertToAbsoluteDirection(flags, layoutDirection);
}
//...