androidout-of-memoryandroid-animationanimationdrawable

Create "growing ripples" loading animation - OutOfMemoryException


I want to display this animation in an Android app:

enter image description here

So I created a <animation-list> to do so. It uses 46 frames, each one consisting of a 200x200px png:

    <?xml version="1.0" encoding="utf-8"?>
    <animation-list xmlns:android="http://schemas.android.com/apk/res/android">
            <item android:drawable="@drawable/anim_loading_ripple_frame_0" android:duration="45" />
            <item android:drawable="@drawable/anim_loading_ripple_frame_1" android:duration="45" />
            <item android:drawable="@drawable/anim_loading_ripple_frame_2" android:duration="45" />
            <item android:drawable="@drawable/anim_loading_ripple_frame_3" android:duration="45" />
            <item android:drawable="@drawable/anim_loading_ripple_frame_4" android:duration="45" />
            <item android:drawable="@drawable/anim_loading_ripple_frame_5" android:duration="45" />
               ...
            <item android:drawable="@drawable/anim_loading_ripple_frame_45" android:duration="45" />
    </animation-list>

Then I set this as the background of an ImageView and start the animation using

AnimationDrawable loadingAnimation = 
    (AnimationDrawable) loadingAnimationView.getBackground();
loadingAnimation.start();

Everything works perfect, except for the fact that the app sometimes crashes due to an OutOfMemoryException! It seems that the memory requirements by this kind of animation are quite high, as I've read others experiencing the same issues.

Then I thought I could probably just make each frame into an XML shape drawable, so I tried experimenting, but I can't figure out how to make the circles change size - they always occupy the whole width of the view.

I tried doing something like this

<item>
    <shape android:shape="oval">
        <padding android:top="30dp" android:right="30dp" android:bottom="30dp" android:left="30dp" />
        <size
            android:width="100dp"
            android:height="100dp" />
        <stroke
            android:color="#0F0"
            android:width="4dp"/>
    </shape>
</item>

And also using a layer-list to define another shape in the same XML file but with a different size, thinking that would cause the circle to become smaller, but they both ended up occupying 100% of the view.

Also, I'm not sure if the AnimationDrawable will actually use less memory if I define the frames in XML as opposed to a series of png images? Perhaps I'll still run into the same memory usage issue?

What alternatives do I have? If time wasn't an issue, I'd try to work out a custom View, but it seems like a lot of work just for this animation.


Solution

  • The ScaleAnimation solution provided by Ves will work, but I figured I might as well dive into it and make a proper (and I guess quite more efficient) custom View. Especially when dealing with multiple "ripples".

    enter image description here

    This is the entire View class, tweaks to the ripples can be made in init() to change colors, number of ripples, etc.

    RippleView.java

    public class RippleView extends View implements ValueAnimator.AnimatorUpdateListener {
        public static final String TAG = "RippleView";
    
    
        private class Ripple {
            AnimatorSet mAnimatorSet;
            ValueAnimator mRadiusAnimator;
            ValueAnimator mAlphaAnimator;
            Paint mPaint;
    
            Ripple(float startRadiusFraction, float stopRadiusFraction, float startAlpha, float stopAlpha, int color, long delay, long duration, float strokeWidth, ValueAnimator.AnimatorUpdateListener updateListener){
                mRadiusAnimator = ValueAnimator.ofFloat(startRadiusFraction, stopRadiusFraction);
                mRadiusAnimator.setDuration(duration);
                mRadiusAnimator.setRepeatCount(ValueAnimator.INFINITE);
                mRadiusAnimator.addUpdateListener(updateListener);
                mRadiusAnimator.setInterpolator(new DecelerateInterpolator());
    
                mAlphaAnimator = ValueAnimator.ofFloat(startAlpha, stopAlpha);
                mAlphaAnimator.setDuration(duration);
                mAlphaAnimator.setRepeatCount(ValueAnimator.INFINITE);
                mAlphaAnimator.addUpdateListener(updateListener);
                mAlphaAnimator.setInterpolator(new DecelerateInterpolator());
    
                mAnimatorSet = new AnimatorSet();
                mAnimatorSet.playTogether(mRadiusAnimator, mAlphaAnimator);
                mAnimatorSet.setStartDelay(delay);
    
                mPaint = new Paint();
                mPaint.setStyle(Paint.Style.STROKE);
                mPaint.setColor(color);
                mPaint.setAlpha((int)(255*startAlpha));
                mPaint.setAntiAlias(true);
                mPaint.setStrokeWidth(strokeWidth);
            }
    
            void draw(Canvas canvas, int centerX, int centerY, float radiusMultiplicator){
                mPaint.setAlpha( (int)(255*(float)mAlphaAnimator.getAnimatedValue()) );
                canvas.drawCircle(centerX, centerY, (float)mRadiusAnimator.getAnimatedValue()*radiusMultiplicator, mPaint);
            }
    
            void startAnimation(){
                mAnimatorSet.start();
            }
    
            void stopAnimation(){
                mAnimatorSet.cancel();
            }
        }
    
        private List<Ripple> mRipples = new ArrayList<>();
    
    
    
        public RippleView(Context context, AttributeSet attrs) {
            super(context, attrs);
            init(context, attrs);
        }
    
        public RippleView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init(context, attrs);
        }
    
        @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
        public RippleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            super(context, attrs, defStyleAttr, defStyleRes);
            init(context, attrs);
        }
    
        private void init(Context context, AttributeSet attrs) {
            if( isInEditMode() )
                return;
    
            /*
            Tweak your ripples here!
            */
            mRipples = new ArrayList<>();
            mRipples.add(new Ripple(0.0f, 1.0f, 1.0f, 0.0f, Color.RED, 0, 2000, 4, this));
            mRipples.add(new Ripple(0.0f, 1.0f, 1.0f, 0.0f, Color.WHITE, 500, 2000, 4, this));
        }
    
    
        public void startAnimation() {
            setVisibility(View.VISIBLE);
    
            for (Ripple ripple : mRipples) {
                ripple.startAnimation();
            }
        }
    
        public void stopAnimation() {
            for (Ripple ripple : mRipples) {
                ripple.stopAnimation();
            }
    
            setVisibility(View.GONE);
        }
    
    
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            invalidate();
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            int centerX = getWidth()/2;
            int centerY = getHeight()/2;
            int radiusMultiplicator = getWidth()/2;
    
            for (Ripple ripple : mRipples) {
                ripple.draw(canvas, centerX, centerY, radiusMultiplicator);
            }
        }
    }
    

    Just call .startAnimation() on the RippleView to start the animation.

    RippleView r = findViewById(R.id.rippleView);
    r.startAnimation();