Showing posts with label animator. Show all posts
Showing posts with label animator. Show all posts

Tuesday, March 14, 2017

TL:DR:
If you ever plan on using Frame Animations (animation-list) to animate a list item in a recycler view make sure you stop the animation in ALL cases or it will be very easy to leak it.

DETAILS:
For one of my projects I had to add a loading list item in a recycler view so the user would see something pretty while they wait for the list to populate.

So I went ahead and created an animation-list,
<?xml version="1.0" encoding="utf-8">
android="http://schemas.android.com/apk/res/android" android:oneshot="false">
    <android:drawable="@drawable/tile_loading_01" android:duration="@integer/content_loading_anim" >
    <android:drawable="@drawable/tile_loading_02" android:duration="@integer/content_loading_anim" >
    <android:drawable="@drawable/tile_loading_03" android:duration="@integer/content_loading_anim" >
[...]

a new view holder for this list item
class ContentLoadingViewHolder extends RecyclerView.ViewHolder {
    private AnimationDrawable loopAnimation = null;
  
    ContentLoadingViewHolder(View itemView) {
        super(itemView);
        ImageView image = (ImageView) itemView.findViewById(R.id.illustration);
        if (image != null) {
            loopAnimation = (AnimationDrawable) image.getDrawable();
        }
    }

    void startAnimation() {
        Log.d("Starting animation - loopAnimation: " + loopAnimation
            + " isRunning: " + (loopAnimation != null ? loopAnimation.isRunning() : "null"));
        if (loopAnimation != null && !loopAnimation.isRunning()) {
            loopAnimation.start();
        }
    }

    void stopAnimation() {
        Log.d("Stopping animation - loopAnimation: " + loopAnimation
            + " isRunning: " + (loopAnimation != null ? loopAnimation.isRunning() : "null"));
        if (loopAnimation != null && loopAnimation.isRunning()) {
            loopAnimation.stop();        }
        }
    }

 in my adapter I added the call to start the animation in onbindview
@Override
@MainThread
public void onBindViewHolder(final RecyclerView.ViewHolder viewHolder, int unreliablePosition) {
    if (viewHolder instanceof ContentLoadingViewHolder) {
        ((ContentLoadingViewHolder)viewHolder).startAnimation();
    }
}

and stop it in onViewRecycled
@Override
public void onViewRecycled(RecyclerView.ViewHolder holder) {
    clearAnimationIfRunning(holder);
    super.onViewRecycled(holder);
}

At this point you might think. Ok I'm good this should work. And for the most part it does but it will possibly leak your animation if you don't also stop it in all these other cases:
@Override
public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
    clearAllAnimations();
    super.onDetachedFromRecyclerView(recyclerView);
}

@Override
public void onViewRecycled(RecyclerView.ViewHolder holder) {
    clearAnimationIfRunning(holder);
    super.onViewRecycled(holder);
}

@Override
public boolean onFailedToRecycleView(RecyclerView.ViewHolder holder) {
    clearAnimationIfRunning(holder);
    return super.onFailedToRecycleView(holder);
}

void clearAllAnimations() {
    Log.d("Clearing all animations in the suggestion adapter.");
    // clear all animation in recycler view
    int count = recyclerView.getChildCount();
    for (int i = 0; i < count; ++i) {
        try {
            RecyclerView.ViewHolder holder = recyclerView.findViewHolderForAdapterPosition(i);
            clearAnimationIfRunning(holder);
        } catch (Throwable t) {
            Log.e("Unable to clear animations");
        }
    }

    // clear all animation if left pending in recycler view pool
    RecyclerView.RecycledViewPool pool = recyclerView.getRecycledViewPool();
    if (pool != null) {
        while (true) {
            RecyclerView.ViewHolder holder = pool.getRecycledView(LOADING.ordinal());
            if (holder != null) {
                clearAnimationIfRunning(holder);
            } else {
                break;
            }
        }
    }
}

private void clearAnimationIfRunning(RecyclerView.ViewHolder holder) {
    if (holder != null && holder instanceof ContentLoadingViewHolder) {
        ((ContentLoadingViewHolder)holder).stopAnimation();
    }
}

See the recycler might have failed to recycle the view in which case you need to catch it in "onFailedToRecycleView" and the user might tap the recent apps button and swipe the app closed and for that case you will catch it in onDetachedFromRecyclerView.

Lastly, if you're using this in a fragment, you also want to stop animations when the fragment gets detached:
@Override
public void onDetach() {
    if (adapter != null) {
        adapter.clearAllAnimations();
    }
    super.onDetach();
}

Hope this helps keep your app leak free.

Wednesday, May 11, 2016

How to make your ViewPager give a sneak peak at the next page

Simply add a scroll animation like so:

final long DRAG_HINT_ANIMATION_DURATION_IN = 500;
final long DRAG_HINT_ANIMATION_DURATION_OUT = 250;

ObjectAnimator scrollIn = ObjectAnimator.ofInt(viewPager, "scrollX", 200);
scrollIn.setDuration(DRAG_HINT_ANIMATION_DURATION_IN);
scrollIn.setInterpolator(new AccelerateDecelerateInterpolator());
ObjectAnimator scrollOut = ObjectAnimator.ofInt(viewPager, "scrollX", 0);
scrollOut.setDuration(DRAG_HINT_ANIMATION_DURATION_OUT);
scrollOut.setInterpolator(new AccelerateDecelerateInterpolator());

AnimatorSet set = new AnimatorSet();
set.playSequentially(scrollIn, scrollOut);
set.start();

This will scroll your viewpager by 200 pixels in 500ms. and then scroll it back in 250ms.
I'm currently using an easing interpolator (AccelerateDecelerateInterpolator)
It accelerates to the normal speed and then decelerates before reaching the end of the animation.
Of course any of these parameters are yours to modify.
Enjoy !