title: Implementing "Bounce Back" RecyclerView Animation with ItemTouchHelper.Callback
body: <p>I am attempting to create an animation in my <code>RecyclerView</code> rows with a <code>Helper</code> class that extends <code>ItemTouchHelper.Callback</code>.</p>
  
  <p>The default "swipe" behaviour with <code>ItemTouchHelper</code> 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.</p>
  
  <p>I have been able to create a <code>ValueAnimator</code> that animates the row back into view if it is swiped off screen. However, the problem is that I cannot get the parent <code>ItemTouchHelper</code> class to recognise that the <code>x</code> values it has stored have changed from the width of the row (so that it is completely offscreen) to <code>0</code> (so that it is back on screen).</p>
  
  <p>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 <code>X</code> position of <code>0</code> to an <code>X</code> 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.</p>
  
  <p>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:</p>
  
  <p><a href="https://youtu.be/rq6DyK2wVAc" rel="nofollow noreferrer" title="Video Title"><img src="https://img.youtube.com/vi/rq6DyK2wVAc/0.jpg" alt="Youtube - Issue with ItemTouchHelper.Callback"></a></p>
  
  <p>Here is the code I use to conduct the animation:</p>
  
  <pre><code>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 &gt; 0.8F &amp;&amp; !animatorRunning &amp;&amp; !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);
          }
      }
  
  //...
  </code></pre>
  
  <p>Note that this inner class, <code>RecoverAnimation</code>, is pulled from the <code>ItemTouchHelper</code> source code:</p>
  
  <pre><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) {
  
      }
  }
  </code></pre>
  
  <p>Is there anything I can do to counteract this issue, or is it simply not possible (yet)?</p>