问题
I am attempting to create an animation in my RecyclerView
rows with a Helper
class that extends ItemTouchHelper.Callback
.
The default "swipe" behaviour with ItemTouchHelper
is that if a row is swiped with sufficient velocity, it disappears from view (i.e. swipe-to-dimiss). In my case, I want to create an animation that causes the row to bounce straight back if such an event takes place.
I have been able to create a ValueAnimator
that animates the row back into view if it is swiped off screen. However, the problem is that I cannot get the parent ItemTouchHelper
class to recognise that the x
values it has stored have changed from the width of the row (so that it is completely offscreen) to 0
(so that it is back on screen).
This means that whenever I go to interact with the row after it has completed my custom animation, it behaves as if the row is still off screen, even though it is fully visible. This usually means that pressing it means the row jumps from an X
position of 0
to an X
position of the row width, and so animates back in again from the screen's edge. It does this a couple of times before it fully resets and behaves normally again.
This video will help show the problem. As the first two swipes are not flings off-screen, the default behaviour works. After the row is swiped off screen however and bounces back, tapping the row shows it pinging back in from the right edge:
Here is the code I use to conduct the animation:
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);
}
}
//...
Note that this inner class, RecoverAnimation
, is pulled from the ItemTouchHelper
source code:
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) {
}
}
Is there anything I can do to counteract this issue, or is it simply not possible (yet)?
回答1:
Following solution will bounce back recyclerview item without implementing your custom RecoverAnimation 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);
}
//...
来源:https://stackoverflow.com/questions/32784798/implementing-bounce-back-recyclerview-animation-with-itemtouchhelper-callback