Causing OutOfMemoryError in Frame by Frame Animation in Android
Asked Answered
E

11

40

I am having lots of images as frames in my resources/drawable folder (let say approx 200). And using this images i want run a animation. The longest animation is of 80Frames. I am successfully able to run the animation on click of the buttons for some, but for some of the animation it is giving me OutOfMemoryError saying that VM can't provide such memory. It is out of VM Budget. I count the size of all of the images its about 10MB. The size of each image is 320x480 in pixels.

I try googling and found that i need to explicitly call the Garbage Collector using System.gc() method. I have done that but still i am getting some time error of memory. Can anyone please kindly help me out in this.

Some Code:-

ImageView img = (ImageView)findViewById(R.id.xxx);
img.setBackgroundResource(R.anim.angry_tail_animation);
AnimationDrawable mailAnimation = (AnimationDrawable) img.getBackground();
MediaPlayer player = MediaPlayer.create(this.getApplicationContext(), R.raw.angry);
    if(mailAnimation.isRunning()) {
    mailAnimation.stop();
    mailAnimation.start();
        if (player.isPlaying()) {
        player.stop();
        player.start();
    }
    else {
        player.start();
    }
}
else {
    mailAnimation.start();
        if (player.isPlaying()) {
        player.stop();
        player.start();
    }
    else {
        player.start();
    }
}

This is the code i have written in on click of a Button.....

Resource file inside res/drawable/anim

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="true" >

<item android:drawable="@drawable/cat_angry0000" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0001" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0002" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0003" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0004" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0005" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0006" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0007" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0008" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0009" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0010" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0011" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0012" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0013" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0014" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0015" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0016" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0017" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0018" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0019" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0020" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0021" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0022" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0023" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0024" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0025" android:duration="50"/>

</animation-list>

** The above is the resource file used in setBackgroundResource, same way I am having 10 more file for other different animation. **

Error Log

01-16 22:23:41.594: E/AndroidRuntime(399): FATAL EXCEPTION: main
01-16 22:23:41.594: E/AndroidRuntime(399): java.lang.IllegalStateException: Could not execute method of the activity
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.view.View$1.onClick(View.java:2144)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.view.View.performClick(View.java:2485)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.view.View$PerformClick.run(View.java:9080)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.os.Handler.handleCallback(Handler.java:587)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.os.Handler.dispatchMessage(Handler.java:92)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.os.Looper.loop(Looper.java:123)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.app.ActivityThread.main(ActivityThread.java:3683)
01-16 22:23:41.594: E/AndroidRuntime(399):  at java.lang.reflect.Method.invokeNative(Native Method)
01-16 22:23:41.594: E/AndroidRuntime(399):  at java.lang.reflect.Method.invoke(Method.java:507)
01-16 22:23:41.594: E/AndroidRuntime(399):  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:839)
01-16 22:23:41.594: E/AndroidRuntime(399):  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:597)
01-16 22:23:41.594: E/AndroidRuntime(399):  at dalvik.system.NativeStart.main(Native Method)
01-16 22:23:41.594: E/AndroidRuntime(399): Caused by: java.lang.reflect.InvocationTargetException
01-16 22:23:41.594: E/AndroidRuntime(399):  at java.lang.reflect.Method.invokeNative(Native Method)
01-16 22:23:41.594: E/AndroidRuntime(399):  at java.lang.reflect.Method.invoke(Method.java:507)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.view.View$1.onClick(View.java:2139)
01-16 22:23:41.594: E/AndroidRuntime(399):  ... 11 more
01-16 22:23:41.594: E/AndroidRuntime(399): Caused by: java.lang.OutOfMemoryError: bitmap size exceeds VM budget
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:460)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.graphics.BitmapFactory.decodeResourceStream(BitmapFactory.java:336)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.graphics.drawable.Drawable.createFromResourceStream(Drawable.java:697)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.content.res.Resources.loadDrawable(Resources.java:1709)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.content.res.Resources.getDrawable(Resources.java:581)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.graphics.drawable.AnimationDrawable.inflate(AnimationDrawable.java:267)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.graphics.drawable.Drawable.createFromXmlInner(Drawable.java:787)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.graphics.drawable.Drawable.createFromXml(Drawable.java:728)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.content.res.Resources.loadDrawable(Resources.java:1694)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.content.res.Resources.getDrawable(Resources.java:581)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.view.View.setBackgroundResource(View.java:7533)
01-16 22:23:41.594: E/AndroidRuntime(399):  at talking.cat.CatActivity.middleButtonClicked(CatActivity.java:83)

Same way i have different buttons for different animation... Thanks

Elisavetgrad answered 1/1, 2012 at 8:0 Comment(12)
Yeah buddy it is a big problem of android.I always face it but didnot find particular solution though.. ;)Drape
@AndroidKiller Thanks for your reply but i need to find something for this.......Elisavetgrad
Can you post some code here.... that may help us understand better ...Sharisharia
what are you testing on? A real device, emulator?Marozas
@Marozas I am testing it on Emulator right now...Elisavetgrad
@KingRV Thanks for your response..See now i have put some snippet of the code....Elisavetgrad
Please any one having any idea let me know, I really need a solution for this. ThanksElisavetgrad
is it possible you are not giving your emulator enough RAM?Airliah
have you read this? developer.android.com/resources/articles/… i can't be sure if this is the problem (since you have only provided code for the button's onClickListener) but it's worth reading... leaking an entire Activity's context is never a good thing. could this be the issue?Airliah
more code would be helpful if you want someone to try and find the problem :).Airliah
@AlexLockwood, Thanks for your reply. Given 512 MB memory to emulator and also modify the default memory in eclipse.ini file from 256mb to 512 mb. I have not read the article, but now going through it. Thanks once againElisavetgrad
@AlexLockwood, I posted all the code over here. Rest of the things are same as above. What is going to change is the resource file and audio files nothing else. Then also i try to put everything.Elisavetgrad
P
25

I assume that your animation frame images are compressed (PNG or JPG). The compressed size is not useful for calculating how much memory is needed to display them. For that, you need to think about the uncompressed size. This will be the number of pixels (320x480) multiplied by the number of bytes per pixel, which is typically 4 (32 bits). For your images, then, each one will be 614,400 bytes. For the 26-frame animation example you provided, that will require a total of 15,974,400 bytes to hold the raw bitmap data for all the frames, not counting the object overhead.

Looking at the source code for AnimationDrawable, it appears to load all of the frames into memory at once, which it would basically have to do for good performance.

Whether you can allocate this much memory or not is very system dependent. I would at least recommend trying this on a real device instead of the emulator. You can also try tweaking the emulator's available RAM size, but this is just guessing.

There are ways to use BitmapFactory.inPreferredConfig to load bitmaps in a more memory-efficient format like RGB 565 (rather than ARGB 8888). This would save some space, but it still might not be enough.

If you can't allocate that much memory at once, you have to consider other options. Most high performance graphics applications (e.g. games) draw their graphics from combinations of smaller graphics (sprites) or 2D or 3D primitives (rectangles, triangles). Drawing a full-screen bitmap for every frame is effectively the same as rendering video; not necessarily the most efficient.

Does the entire content of your animation change with each frame? Another optimization could be to animate only the portion that actually changes, and chop up your bitmaps to account for that.

To summarize, you need to find a way to draw your animation using less memory. There are many options, but it depends a lot on how your animation needs to look.

Pepper answered 17/1, 2012 at 4:57 Comment(4)
@ LYRICSBOY ,Thanks for your response. You might heard about the Talking Tom Cat game in android and others also. I want exactly the same thing in my app. I am having frames of size 320x480 pixels as i told earlier, so I search about sprites on google and found that sprites are useful for small size images not for large pngs. And for Bitmap Factory i don't know that how i can load all this frames in one Bitmap object or how to run my animation using Bitmap classes instead of AnimationDrawable.Elisavetgrad
It looks like you can use AnimationDrawable in code (rather than XML) and use addFrame(Drawable, int) to pass your own drawables, also created in code using BitmapDrawable to wrap Bitmaps created using BitmapFactory. Or you could try creating your own subclass of DrawableContainer (the superclass of AnimationDrawable) that is more suited to your needs. OR you could just create a game-style drawing loop that fires as often as possible but no faster than your desired frame rate and loads the bitmaps from the resources in some kind of smart cache.Pepper
@ LYRICSBOY, Can you please guide me how can I use BitmapFactory , BitmapDrawable & Bitmap for my application as i never used this things before ever. I used addFrame in my code before this AnimationDrawable but in that I am getting the same error. But i have not used thread to run the animation. Does that make any difference?Elisavetgrad
The code associated with this article has some examples of using BitmapFactory. A quick search should find you several more.Pepper
B
55

I had the same problem. Android loads all the drawables at once, so animation with many frames causes this error.

I ended up creating my own simple sequence animation:

public class AnimationsContainer {
    public int FPS = 30;  // animation FPS

    // single instance procedures
    private static AnimationsContainer mInstance;

    private AnimationsContainer() {
    };

    public static AnimationsContainer getInstance() {
        if (mInstance == null)
            mInstance = new AnimationsContainer();
        return mInstance;
    }

    // animation progress dialog frames
    private int[] mProgressAnimFrames = { R.drawable.logo_30001, R.drawable.logo_30002, R.drawable.logo_30003 };

    // animation splash screen frames
    private int[] mSplashAnimFrames = { R.drawable.logo_ding200480001, R.drawable.logo_ding200480002 };


    /**
     * @param imageView 
     * @return progress dialog animation
     */
    public FramesSequenceAnimation createProgressDialogAnim(ImageView imageView) {
        return new FramesSequenceAnimation(imageView, mProgressAnimFrames);
    }

    /**
     * @param imageView
     * @return splash screen animation
     */
    public FramesSequenceAnimation createSplashAnim(ImageView imageView) {
        return new FramesSequenceAnimation(imageView, mSplashAnimFrames);
    }

    /**
     * AnimationPlayer. Plays animation frames sequence in loop
     */
public class FramesSequenceAnimation {
    private int[] mFrames; // animation frames
    private int mIndex; // current frame
    private boolean mShouldRun; // true if the animation should continue running. Used to stop the animation
    private boolean mIsRunning; // true if the animation currently running. prevents starting the animation twice
    private SoftReference<ImageView> mSoftReferenceImageView; // Used to prevent holding ImageView when it should be dead.
    private Handler mHandler;
    private int mDelayMillis;
    private OnAnimationStoppedListener mOnAnimationStoppedListener;

    private Bitmap mBitmap = null;
    private BitmapFactory.Options mBitmapOptions;

    public FramesSequenceAnimation(ImageView imageView, int[] frames, int fps) {
        mHandler = new Handler();
        mFrames = frames;
        mIndex = -1;
        mSoftReferenceImageView = new SoftReference<ImageView>(imageView);
        mShouldRun = false;
        mIsRunning = false;
        mDelayMillis = 1000 / fps;

        imageView.setImageResource(mFrames[0]);

        // use in place bitmap to save GC work (when animation images are the same size & type)
        if (Build.VERSION.SDK_INT >= 11) {
            Bitmap bmp = ((BitmapDrawable) imageView.getDrawable()).getBitmap();
            int width = bmp.getWidth();
            int height = bmp.getHeight();
            Bitmap.Config config = bmp.getConfig();
            mBitmap = Bitmap.createBitmap(width, height, config);
            mBitmapOptions = new BitmapFactory.Options();
            // setup bitmap reuse options. 
            mBitmapOptions.inBitmap = mBitmap;
            mBitmapOptions.inMutable = true;
            mBitmapOptions.inSampleSize = 1;
        }
    }

    private int getNext() {
        mIndex++;
        if (mIndex >= mFrames.length)
            mIndex = 0;
        return mFrames[mIndex];
    }

    /**
     * Starts the animation
     */
    public synchronized void start() {
        mShouldRun = true;
        if (mIsRunning)
            return;

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                ImageView imageView = mSoftReferenceImageView.get();
                if (!mShouldRun || imageView == null) {
                    mIsRunning = false;
                    if (mOnAnimationStoppedListener != null) {
                        mOnAnimationStoppedListener.AnimationStopped();
                    }
                    return;
                }

                mIsRunning = true;
                mHandler.postDelayed(this, mDelayMillis);

                if (imageView.isShown()) {
                    int imageRes = getNext();
                    if (mBitmap != null) { // so Build.VERSION.SDK_INT >= 11
                        Bitmap bitmap = null;
                        try {
                            bitmap = BitmapFactory.decodeResource(imageView.getResources(), imageRes, mBitmapOptions);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        if (bitmap != null) {
                            imageView.setImageBitmap(bitmap);
                        } else {
                            imageView.setImageResource(imageRes);
                            mBitmap.recycle();
                            mBitmap = null;
                        }
                    } else {
                        imageView.setImageResource(imageRes);
                    }
                }

            }
        };

        mHandler.post(runnable);
    }

        /**
         * Stops the animation
         */
        public synchronized void stop() {
            mShouldRun = false;
        }
    }
}

Usage:

FramesSequenceAnimation anim = AnimationsContainer.getInstance().createSplashAnim(mSplashImageView);
anim.start();
  • don't forget to stop it...
Beira answered 12/6, 2012 at 9:29 Comment(8)
Thanks for your answer. I will surely try this. Hopefully it solve my problem. Once again thanksElisavetgrad
Works really great. For OnAnimationStoppedListener I added an interface with only one method void AnimationStopped(). Also I changed it a little bit so that I can have a oneshot animation.Erlineerlinna
I have improve this class and uploaded to github. Please click here who have a problem with oom with frame animation.Rockbound
For some reason, the animation is not smooth and it is taking more time than it should. My array is building dynamically (on main thread) so I've switched to ArrayList (~200 elements) and my interval is 34ms (30fps). Any suggestions?Sicklebill
If the animation runs slow, its probably because the frames are loaded on real-time... My suggestion would be to set the images to the same size and type and make sure that "if (Build.VERSION.SDK_INT >= 11) {"... code is running - it should use the same memory allocation for new images and give you better performanceBeira
@HoryunLee ive downloaded the github code, but the animation keeps on continuing in a loop, stop isnt getting called / or onAnimationEnd listener isnt getting calledExecution
@Pinhassi Thnks for your answers . it works perfectly . but I need to stop the animation after running all frames . like I have 81 frames to animate and I want to run this animation for one time not repeatedly . You mentioned about the stop method but when should I call it it .plz give me just a little bit of details for stopping the animation after running all frames . and hey thnks in anvanceEclampsia
in FramesSequenceAnimation.getNext() you can tell when one loop was finished. just add stop() at that point. (or even better, add "onLoopEnd() call back).Beira
P
25

I assume that your animation frame images are compressed (PNG or JPG). The compressed size is not useful for calculating how much memory is needed to display them. For that, you need to think about the uncompressed size. This will be the number of pixels (320x480) multiplied by the number of bytes per pixel, which is typically 4 (32 bits). For your images, then, each one will be 614,400 bytes. For the 26-frame animation example you provided, that will require a total of 15,974,400 bytes to hold the raw bitmap data for all the frames, not counting the object overhead.

Looking at the source code for AnimationDrawable, it appears to load all of the frames into memory at once, which it would basically have to do for good performance.

Whether you can allocate this much memory or not is very system dependent. I would at least recommend trying this on a real device instead of the emulator. You can also try tweaking the emulator's available RAM size, but this is just guessing.

There are ways to use BitmapFactory.inPreferredConfig to load bitmaps in a more memory-efficient format like RGB 565 (rather than ARGB 8888). This would save some space, but it still might not be enough.

If you can't allocate that much memory at once, you have to consider other options. Most high performance graphics applications (e.g. games) draw their graphics from combinations of smaller graphics (sprites) or 2D or 3D primitives (rectangles, triangles). Drawing a full-screen bitmap for every frame is effectively the same as rendering video; not necessarily the most efficient.

Does the entire content of your animation change with each frame? Another optimization could be to animate only the portion that actually changes, and chop up your bitmaps to account for that.

To summarize, you need to find a way to draw your animation using less memory. There are many options, but it depends a lot on how your animation needs to look.

Pepper answered 17/1, 2012 at 4:57 Comment(4)
@ LYRICSBOY ,Thanks for your response. You might heard about the Talking Tom Cat game in android and others also. I want exactly the same thing in my app. I am having frames of size 320x480 pixels as i told earlier, so I search about sprites on google and found that sprites are useful for small size images not for large pngs. And for Bitmap Factory i don't know that how i can load all this frames in one Bitmap object or how to run my animation using Bitmap classes instead of AnimationDrawable.Elisavetgrad
It looks like you can use AnimationDrawable in code (rather than XML) and use addFrame(Drawable, int) to pass your own drawables, also created in code using BitmapDrawable to wrap Bitmaps created using BitmapFactory. Or you could try creating your own subclass of DrawableContainer (the superclass of AnimationDrawable) that is more suited to your needs. OR you could just create a game-style drawing loop that fires as often as possible but no faster than your desired frame rate and loads the bitmaps from the resources in some kind of smart cache.Pepper
@ LYRICSBOY, Can you please guide me how can I use BitmapFactory , BitmapDrawable & Bitmap for my application as i never used this things before ever. I used addFrame in my code before this AnimationDrawable but in that I am getting the same error. But i have not used thread to run the animation. Does that make any difference?Elisavetgrad
The code associated with this article has some examples of using BitmapFactory. A quick search should find you several more.Pepper
A
11

I spent a lot of time on this and have two different solutions, both good..

First, the problem: 1) Android loads all of the images into RAM, in uncompressed Bitmap format. 2) Android uses resource scaling, so on a phone with an xxxhdpi display (such as LG G3), each frame takes up a TON of space, so you quickly run out of RAM.

Solution #1

1) Bypasses Android's resource scaling. 2) Stores the bytearrays of all files in memory (these are small, especially for JPEGs). 3) Generates Bitmaps frame-by-frame, so it is almost impossible to run out of RAM.

Disadvantages: It spams your logs as Android is allocating memory for new Bitmaps and recycling old ones. It also performs lousy on older devices (Galaxy S1), but performs nicely on current budget phones (read: $10 Alcatel C1 I picked up at BestBuy). Second solution below performs better on older devices, but could still run out of RAM in some circumstances.

public class MyAnimationDrawable {
public static class MyFrame {
    byte[] bytes;
    int duration;
    Drawable drawable;
    boolean isReady = false;
}


public interface OnDrawableLoadedListener {
    public void onDrawableLoaded(List<MyFrame> myFrames);
}

public static void loadRaw(final int resourceId, final Context context, final OnDrawableLoadedListener onDrawableLoadedListener) {
    loadFromXml(resourceId, context, onDrawableLoadedListener);
}

private static void loadFromXml(final int resourceId, final Context context, final OnDrawableLoadedListener onDrawableLoadedListener) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            final ArrayList<MyFrame> myFrames = new ArrayList<>();

            XmlResourceParser parser = context.getResources().getXml(resourceId);

            try {
                int eventType = parser.getEventType();
                while (eventType != XmlPullParser.END_DOCUMENT) {
                    if (eventType == XmlPullParser.START_DOCUMENT) {

                    } else if (eventType == XmlPullParser.START_TAG) {

                        if (parser.getName().equals("item")) {
                            byte[] bytes = null;
                            int duration = 1000;

                            for (int i=0; i<parser.getAttributeCount(); i++) {
                                if (parser.getAttributeName(i).equals("drawable")) {
                                    int resId = Integer.parseInt(parser.getAttributeValue(i).substring(1));
                                    bytes = IOUtils.toByteArray(context.getResources().openRawResource(resId));
                                }
                                else if (parser.getAttributeName(i).equals("duration")) {
                                    duration = parser.getAttributeIntValue(i, 1000);
                                }
                            }

                            MyFrame myFrame = new MyFrame();
                            myFrame.bytes = bytes;
                            myFrame.duration = duration;
                            myFrames.add(myFrame);
                        }

                    } else if (eventType == XmlPullParser.END_TAG) {

                    } else if (eventType == XmlPullParser.TEXT) {

                    }

                    eventType = parser.next();
                }
            }
            catch (IOException | XmlPullParserException e) {
                e.printStackTrace();
            }

            // Run on UI Thread
            new Handler(context.getMainLooper()).post(new Runnable() {
                @Override
                public void run() {
                    if (onDrawableLoadedListener != null) {
                        onDrawableLoadedListener.onDrawableLoaded(myFrames);
                    }
                }
            });
        }
    }).run();
}

public static void animateRawManually(int resourceId, final ImageView imageView, final Runnable onStart, final Runnable onComplete) {
    loadRaw(resourceId, imageView.getContext(), new OnDrawableLoadedListener() {
        @Override
        public void onDrawableLoaded(List<MyFrame> myFrames) {
            if (onStart != null) {
                onStart.run();
            }

            animateRawManually(myFrames, imageView, onComplete);
        }
    });
}

public static void animateRawManually(List<MyFrame> myFrames, ImageView imageView, Runnable onComplete) {
    animateRawManually(myFrames, imageView, onComplete, 0);
}

private static void animateRawManually(final List<MyFrame> myFrames, final ImageView imageView, final Runnable onComplete, final int frameNumber) {
    final MyFrame thisFrame = myFrames.get(frameNumber);

    if (frameNumber == 0) {
        thisFrame.drawable = new BitmapDrawable(imageView.getContext().getResources(), BitmapFactory.decodeByteArray(thisFrame.bytes, 0, thisFrame.bytes.length));
    }
    else {
        MyFrame previousFrame = myFrames.get(frameNumber - 1);
        ((BitmapDrawable) previousFrame.drawable).getBitmap().recycle();
        previousFrame.drawable = null;
        previousFrame.isReady = false;
    }

    imageView.setImageDrawable(thisFrame.drawable);
    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            // Make sure ImageView hasn't been changed to a different Image in this time
            if (imageView.getDrawable() == thisFrame.drawable) {
                if (frameNumber + 1 < myFrames.size()) {
                    MyFrame nextFrame = myFrames.get(frameNumber+1);

                    if (nextFrame.isReady) {
                        // Animate next frame
                        animateRawManually(myFrames, imageView, onComplete, frameNumber + 1);
                    }
                    else {
                        nextFrame.isReady = true;
                    }
                }
                else {
                    if (onComplete != null) {
                        onComplete.run();
                    }
                }
            }
        }
    }, thisFrame.duration);

    // Load next frame
    if (frameNumber + 1 < myFrames.size()) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                MyFrame nextFrame = myFrames.get(frameNumber+1);
                nextFrame.drawable = new BitmapDrawable(imageView.getContext().getResources(), BitmapFactory.decodeByteArray(nextFrame.bytes, 0, nextFrame.bytes.length));
                if (nextFrame.isReady) {
                    // Animate next frame
                    animateRawManually(myFrames, imageView, onComplete, frameNumber + 1);
                }
                else {
                    nextFrame.isReady = true;
                }

            }
        }).run();
    }
}
}

** Solution #2 **

It loads the XML resource, parses it and loads the raw resources - thereby bypassing Android's resource scaling (which is responsible for most OutOfMemoryExceptions), and creates an AnimationDrawable.

Advantages: Performs better on older devices (eg. Galaxy S1)

Disadvantages: Can still run out of RAM as it's holding all of the uncompressed Bitmaps in memory (but they are smaller because they are not scaled the way Android normally scales images)

public static void animateManuallyFromRawResource(int animationDrawableResourceId, ImageView imageView, Runnable onStart, Runnable onComplete) {
    AnimationDrawable animationDrawable = new AnimationDrawable();

    XmlResourceParser parser = imageView.getContext().getResources().getXml(animationDrawableResourceId);

    try {
        int eventType = parser.getEventType();
        while (eventType != XmlPullParser.END_DOCUMENT) {
            if (eventType == XmlPullParser.START_DOCUMENT) {

            } else if (eventType == XmlPullParser.START_TAG) {

                if (parser.getName().equals("item")) {
                    Drawable drawable = null;
                    int duration = 1000;

                    for (int i=0; i<parser.getAttributeCount(); i++) {
                        if (parser.getAttributeName(i).equals("drawable")) {
                            int resId = Integer.parseInt(parser.getAttributeValue(i).substring(1));
                            byte[] bytes = IoUtils.readBytes(imageView.getContext().getResources().openRawResource(resId));
                            drawable = new BitmapDrawable(imageView.getContext().getResources(), BitmapFactory.decodeByteArray(bytes, 0, bytes.length));
                        }
                        else if (parser.getAttributeName(i).equals("duration")) {
                            duration = parser.getAttributeIntValue(i, 66);
                        }
                    }

                    animationDrawable.addFrame(drawable, duration);
                }

            } else if (eventType == XmlPullParser.END_TAG) {

            } else if (eventType == XmlPullParser.TEXT) {

            }

            eventType = parser.next();
        }
    }
    catch (IOException | XmlPullParserException e) {
        e.printStackTrace();
    }

    if (onStart != null) {
        onStart.run();
    }
    animateDrawableManually(animationDrawable, imageView, onComplete, 0);
}

private static void animateDrawableManually(final AnimationDrawable animationDrawable, final ImageView imageView, final Runnable onComplete, final int frameNumber) {
    final Drawable frame = animationDrawable.getFrame(frameNumber);
    imageView.setImageDrawable(frame);
    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            // Make sure ImageView hasn't been changed to a different Image in this time
            if (imageView.getDrawable() == frame) {
                if (frameNumber + 1 < animationDrawable.getNumberOfFrames()) {
                    // Animate next frame
                    animateDrawableManually(animationDrawable, imageView, onComplete, frameNumber + 1);
                }
                else {
                    // Animation complete
                    if (onComplete != null) {
                        onComplete.run();
                    }
                }
            }
        }
    }, animationDrawable.getDuration(frameNumber));
}

If you are still having memory issues, use smaller images... or store the resource name + duration, and generate the byte-array + Drawable on each frame. That would almost certainly cause too much chopping between frames, but uses almost zero RAM.

Aitken answered 21/4, 2015 at 22:5 Comment(7)
Solution 1 is the Best i found in hours. Just changed: bytes = IoUtils.readBytes(context.getResources().openRawResource(resId)); To: bytes = IOUtils.toByteArray(context.getResources().openRawResource(resId));Genitor
Thanks for the catch @MeerschweinBob , I had been referencing my own class. I've updated the answer to use the standard IOUtils. I'm glad it (otherwise) helped you.Aitken
why in the 1st solution you're executing the next frame twice in animateRawManually. once in Handler.postDelayed and once in Thread ?Cambium
@StevenL can you also explain the isReady boolean ? what is it for ?Cambium
Can you put a sample on how to use your solutions? Those who aren't as well versed might not have an idea on how to start with your code.Waki
Thank you so much! The first solutions works like a charm! :DDepone
@Waki You just need to call the static methods like ex: loadRaw or animateRawManuallyBerzelius
L
3

I've created an animation class that displays frames based on passed in drawables resources and frames durations.

 protected class SceneAnimation{
    private ImageView mImageView;
    private int[] mFrameRess;
    private int[] mDurations;
    private int mDuration;

    private int mLastFrameNo;
    private long mBreakDelay;

 public SceneAnimation(ImageView pImageView, int[] pFrameRess, int[] pDurations){
        mImageView = pImageView;
        mFrameRess = pFrameRess;
        mDurations = pDurations;
        mLastFrameNo = pFrameRess.length - 1;

        mImageView.setImageResource(mFrameRess[0]);
        play(1);
    }

    public SceneAnimation(ImageView pImageView, int[] pFrameRess, int pDuration){
        mImageView = pImageView;
        mFrameRess = pFrameRess;
        mDuration = pDuration;
        mLastFrameNo = pFrameRess.length - 1;

        mImageView.setImageResource(mFrameRess[0]);
        playConstant(1);
    }

    public SceneAnimation(ImageView pImageView, int[] pFrameRess, int pDuration, long pBreakDelay){            
        mImageView = pImageView;
        mFrameRess = pFrameRess;
        mDuration = pDuration;
        mLastFrameNo = pFrameRess.length - 1;
        mBreakDelay = pBreakDelay;

        mImageView.setImageResource(mFrameRess[0]);
        playConstant(1);
    }


    private void play(final int pFrameNo){
        mImageView.postDelayed(new Runnable(){
            public void run() {
                mImageView.setImageResource(mFrameRess[pFrameNo]);
                if(pFrameNo == mLastFrameNo)
                    play(0);
                else
                    play(pFrameNo + 1);
            }
        }, mDurations[pFrameNo]);
    }


    private void playConstant(final int pFrameNo){
        mImageView.postDelayed(new Runnable(){
            public void run() {                    
                mImageView.setImageResource(mFrameRess[pFrameNo]);

                if(pFrameNo == mLastFrameNo)
                    playConstant(0);
                else
                    playConstant(pFrameNo + 1);
            }
        }, pFrameNo==mLastFrameNo && mBreakDelay>0 ? mBreakDelay : mDuration);
    }        
};

It is used like this:

 private ImageView mTapScreenTextAnimImgView;    
private final int[] mTapScreenTextAnimRes = {R.drawable.tap0001_b, R.drawable.tap0002_b, R.drawable.tap0003_b, 
        R.drawable.tap0004_b, R.drawable.tap0005_b, R.drawable.tap0006_b, R.drawable.tap0005_b, R.drawable.tap0004_b,
        R.drawable.tap0003_b, R.drawable.tap0002_b, R.drawable.tap0001_b};
private final int mTapScreenTextAnimDuration = 100;
private final int mTapScreenTextAnimBreak = 500;

and in onCreate:

 mTapScreenTextAnimImgView = (ImageView) findViewById(R.id.scene1AnimBottom);
    new SceneAnimation(mTapScreenTextAnimImgView, mTapScreenTextAnimRes, mTapScreenTextAnimDuration, mTapScreenTextAnimBreak);
Langlois answered 20/9, 2012 at 19:14 Comment(1)
Thank you for your response, i will try this and hope that it works for me.Elisavetgrad
P
2

I had this problem and solved it by doing the two following things:

  1. Cut the resolution of the animation images in half...1/4 the size in uncompressed bytes.
  2. Put the images in the drawable-nodpi folder so they don't get scaled up by Android for you.

My animation was still failing to load on some phones after doing step 1. Step 2 got it working on those phones.

Hope this saves somebody else some time.

EDIT: I was still experiencing crashes after going to the Activity that plays the AnimationDrawable but I have it working now. Here are the additional things I did:

  1. Do not use an animation-list in xml. Instead create the AnimationDrawable each time you need to use it. Otherwise, the next time you load the animation drawable from the resource it will still be trying to use the bitmaps you end up recycling.
  2. Recycle the bitmaps in the AnimationDrawable when you are done using it. This is the magic that frees up the memory.
  3. Use the Android Device Monitor to monitor the allocated bytes in your heap.

Here is code I am using for creating the AnimationDrawable:

    protected AnimationDrawable CreateLoadingAnimationDrawable()
    {
        AnimationDrawable animation = new AnimationDrawable ();
        animation.OneShot = false;
        for (int i = 0; i < kNumberOfFrames; ++i) {
            int index = (i * 2) + 1;
            string stringIndex = index.ToString ("00");
            string bitmapStringId = kBaseAnimationName + stringIndex;
            int resID = this.Resources.GetIdentifier (bitmapStringId, "drawable", this.PackageName);
            Bitmap bitmap = BitmapFactory.DecodeResource (this.Resources, resID);
            BitmapDrawable frame = new BitmapDrawable (bitmap);
            //Drawable frame = Resources.GetDrawable (resID);
            animation.AddFrame (frame, 111);
        }
        return animation;
    }

And code for freeing up the bitmaps when you are done using them. You could do this in OnPause or OnDestroy. _loadingAnimation is my AnimationDrawable created above. I would love to know what SetCallback() does for you in this case. I just copied that from somewhere else on SO.

        if (_loadingAnimation != null) {
            _loadingAnimation.Stop ();
            _loadingImageView.SetBackgroundResource (Resource.Drawable.loading_anim_full7001);
            for (int i = 0; i < _loadingAnimation.NumberOfFrames; ++i) {
                BitmapDrawable frame = _loadingAnimation.GetFrame (i) as BitmapDrawable;
                if (frame != null) {
                    Android.Graphics.Bitmap bitmap = frame.Bitmap;
                    bitmap.Recycle ();
                    frame.SetCallback(null);
                }
            }
            _loadingAnimation.SetCallback(null);
            _loadingAnimation = null;
        }

Ted

Prase answered 22/9, 2015 at 17:45 Comment(4)
This worked really nicely for me and is the simplest to implement. Thanks!Clougher
I'm glad this helped somebody!Prase
@Prase .would you please answer me here too. https://mcmap.net/q/408018/-how-to-recycle-images-as-frame-of-animationdrawable-and-solve-outofmemory-error-in-android/3671748Schema
Am I missing something, or what's up with the weird capitalization of the code? string instead of String, etc?Broadfaced
T
1

Similar to other answers, using rxjava:

public final class RxSequenceAnimation {
    private static final int[] PNG_RESOURCES = new int[]{
            R.drawable.sequence_frame_00,
            R.drawable.sequence_frame_01,
            R.drawable.sequence_frame_02
    };
    private static final String TAG = "rx-seq-anim";
    private final Resources mResource;
    private final ImageView mImageView;
    private final byte[][] RAW_PNG_DATA = new byte[PNG_RESOURCES.length][];
    private final byte[] buff = new byte[1024];
    private Subscription sub;

    public RxSequenceAnimation(Resources resources, ImageView imageView) {
        mResource = resources;
        mImageView = imageView;
    }

    public void start() {
        sub = Observable
                .interval(16, TimeUnit.MILLISECONDS)
                .map(new Func1<Long, Bitmap>() {
                    @Override
                    public Bitmap call(Long l) {
                        int i = (int) (l % PNG_RESOURCES.length);
                        if (RAW_PNG_DATA[i] == null) {
                            // read raw png data (compressed) if not read already into RAM
                            try {
                                RAW_PNG_DATA[i] = read(PNG_RESOURCES[i]);
                            } catch (IOException e) {
                                Log.e(TAG, "IOException " + String.valueOf(e));
                            }
                            Log.d(TAG, "decoded " + i + " size " + RAW_PNG_DATA[i].length);
                        }
                        // decode directly from RAM - only one full blown bitmap is in RAM at a time
                        return BitmapFactory.decodeByteArray(RAW_PNG_DATA[i], 0, RAW_PNG_DATA[i].length);
                    }
                })
                .subscribeOn(Schedulers.newThread())
                .onBackpressureDrop()
                .observeOn(AndroidSchedulers.mainThread())
                .doOnNext(new Action1<Bitmap>() {
                    @Override
                    public void call(Bitmap b) {
                        mImageView.setImageBitmap(b);
                    }
                })
                .subscribe(LogErrorSubscriber.newInstance(TAG));
    }

    public void stop() {
        if (sub != null) {
            sub.unsubscribe();
        }
    }

    private byte[] read(int resId) throws IOException {
        return streamToByteArray(inputStream(resId));
    }

    private InputStream inputStream(int id) {
        return mResource.openRawResource(id);
    }

    private byte[] streamToByteArray(InputStream is) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int i;
        while ((i = is.read(buff, 0, buff.length)) > 0) {
            baos.write(buff, 0, i);
        }
        byte[] bytes = baos.toByteArray();
        is.close();
        return bytes;
    }
}
Turley answered 12/8, 2016 at 9:37 Comment(0)
K
1

I ported a solution to Xamarin Android and did some improvements.

It works well with orientation changes and specially with images around 300 width and height (the larger the image the longer it takes to load the image, the bigger the flickering).

using Android.Content;
using Android.Graphics;
using Android.OS;
using Android.Widget;
using System;

namespace ...Droid.Util
{
    public class FramesSequenceAnimation
    {
        private int[] animationFrames;
        private int currentFrame;
        private bool shouldRun;   // true if the animation should continue running. Used to stop the animation
        private bool isRunning;   // true if the animation currently running. prevents starting the animation twice
        private ImageView imageview;
        private Handler handler;
        private int delayMillis;
        private bool oneShot = false;
        private FramesSequenceAnimationListener onAnimationStoppedListener;
        private Bitmap bitmap = null;
        private BitmapFactory.Options bitmapOptions;
        private Action action;

        private static object Lock = new object();

        public interface FramesSequenceAnimationListener
        {
            void AnimationStopped();
        }

        public void SetFramesSequenceAnimationListener(FramesSequenceAnimationListener onAnimationStoppedListener)
        {
            this.onAnimationStoppedListener = onAnimationStoppedListener;
        }

        public int GetCurrentFrame()
        {
            return currentFrame;
        }

        public void SetCurrentFrame(int currentFrame)
        {
            this.currentFrame = currentFrame;
        }

        public FramesSequenceAnimation(FramesSequenceAnimationListener onAnimationStoppedListener, ImageView imageview, int[] animationFrames, int fps)
        {
            this.onAnimationStoppedListener = onAnimationStoppedListener;
            this.imageview = imageview;
            this.animationFrames = animationFrames;

            delayMillis = 1000 / fps;

            currentFrame = -1;
            shouldRun = false;
            isRunning = false;
            handler = new Handler();
            imageview.SetImageResource(this.animationFrames[0]);

            //// use in place bitmap to save GC work (when animation images are the same size & type)
            //if (Build.VERSION.SdkInt >= BuildVersionCodes.Honeycomb)
            //{
            //    Bitmap bmp = ((BitmapDrawable)imageview.Drawable).Bitmap;
            //    int width = bmp.Width;
            //    int height = bmp.Height;
            //    Bitmap.Config config = bmp.GetConfig();
            //    bitmap = Bitmap.CreateBitmap(width, height, config);
            //    bitmapOptions = new BitmapFactory.Options(); // setup bitmap reuse options
            //    bitmapOptions.InBitmap = bitmap; // reuse this bitmap when loading content
            //    bitmapOptions.InMutable = true;
            //    bitmapOptions.InSampleSize = 1;
            //}

            bitmapOptions = newOptions();
            bitmap = decode(bitmapOptions, getNext());
            bitmapOptions.InBitmap = bitmap;
        }

        private BitmapFactory.Options newOptions()
        {
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.InSampleSize = 1;
            options.InMutable = true;
            options.InJustDecodeBounds = true;
            options.InPurgeable = true;
            options.InInputShareable = true;
            options.InPreferredConfig = Bitmap.Config.Rgb565;
            return options;
        }

        private Bitmap decode(BitmapFactory.Options options, int imageRes)
        {
            return BitmapFactory.DecodeResource(imageview.Resources, imageRes, bitmapOptions);
        }

        public void SetOneShot(bool oneShot)
        {
            this.oneShot = oneShot;
        }

        private int getNext()
        {
            currentFrame++;
            if (currentFrame >= animationFrames.Length)
            {
                if (oneShot)
                {
                    shouldRun = false;
                    currentFrame = animationFrames.Length - 1;
                }
                else
                {
                    currentFrame = 0;
                }
            }
            return animationFrames[currentFrame];
        }

        public void stop()
        {
            lock (Lock)
            {
                shouldRun = false;
            }
        }

        public void start()
        {
            lock (Lock)
            {
                shouldRun = true;

                if (isRunning)
                {
                    return;
                }

                Action tempAction = new Action(delegate
                {
                    if (!shouldRun || imageview == null)
                    {
                        isRunning = false;
                        if (onAnimationStoppedListener != null)
                        {
                            onAnimationStoppedListener.AnimationStopped();
                            onAnimationStoppedListener = null;
                            handler.RemoveCallbacks(action);
                        }
                        return;
                    }

                    isRunning = true;

                    handler.PostDelayed(action, delayMillis);

                    if (imageview.IsShown)
                    {
                        int imageRes = getNext();
                        if (bitmap != null)
                        {
                            if (Build.VERSION.SdkInt >= BuildVersionCodes.Honeycomb)
                            {
                                if (bitmap != null && !bitmap.IsRecycled)
                                {
                                    bitmap.Recycle();
                                    bitmap = null;
                                }
                            }

                            try
                            {
                                bitmap = BitmapFactory.DecodeResource(imageview.Resources, imageRes, bitmapOptions);
                            }
                            catch (Exception e)
                            {
                                bitmap.Recycle();
                                bitmap = null;
                                Console.WriteLine("Exception: " + e.StackTrace);
                            }

                            if (bitmap != null)
                            {
                                imageview.SetImageBitmap(bitmap);
                            }
                            else
                            {
                                imageview.SetImageResource(imageRes);
                                bitmap.Recycle();
                                bitmap = null;
                            }
                        }
                        else
                        {
                            imageview.SetImageResource(imageRes);
                        }
                    }
                });

                action = tempAction;

                handler.Post(action);
            }
        }
    }
}

This is my splash screen class: (this class reads the images from the drawable folder that are named "splash_0001, splash_0002 ...". So no need to name your image resources on an array. Increase the number of frames per second (FPS) to speed up the animation).

using Android.App;
using Android.Content;
using Android.OS;
using Android.Widget;
using ...Droid.Base;
using ...Droid.Util;
using System;
using System.Collections.Generic;
using static ...Util.FramesSequenceAnimation;

namespace ...Droid.Activities
{
    [Activity(MainLauncher = true)]
    public class SplashActivity : BaseActivity, FramesSequenceAnimationListener
    {
        private FramesSequenceAnimation framesSequenceAnimation;

        private const string
            IMAGE_NAME_PREFIX = "splash_",
            KEY_CURRENT_FRAME = "key_current_frame";

        private int FPS = 50;

        private int numberOfImages;

        protected override OrientationEnum GetOrientation()
        {
            return OrientationEnum.ORIENTATION_CHECK_DEVICE_SIZE;
        }

        protected override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);
            SetContentView(Resource.Layout.activity_splash);

            RelativeLayout background = FindViewById<RelativeLayout>(Resource.Id.splash_background);
            background.Click += Click;

            ImageView imageView = FindViewById<ImageView>(Resource.Id.splash_imageview);
            imageView.Click += Click;

            numberOfImages = GetSplashImagesCount();

            framesSequenceAnimation = new FramesSequenceAnimation(this, imageView, GetImageResourcesIDs(), FPS);
            framesSequenceAnimation.SetOneShot(true);

            if (savedInstanceState != null)
            {
                int currentFrame = savedInstanceState.GetInt(KEY_CURRENT_FRAME) + 1;
                if (currentFrame < numberOfImages)
                {
                    framesSequenceAnimation.SetCurrentFrame(currentFrame);
                }
            }

            framesSequenceAnimation.start();
        }

        private int[] GetImageResourcesIDs()
        {
            List<int> list = new List<int>();

            for (int i = 1; i <= numberOfImages; i++)
            {
                var image_name = IMAGE_NAME_PREFIX + i.ToString().PadLeft(4, '0');
                int resID = Resources.GetIdentifier(image_name, "drawable", PackageName);
                list.Add(resID);
            }

            return list.ToArray();
        }

        private int GetSplashImagesCount()
        {
            // Count number of images in drawable folder
            int count = 0;
            var fields = typeof(Resource.Drawable).GetFields();
            foreach (var field in fields)
            {
                if (field.Name.StartsWith(IMAGE_NAME_PREFIX))
                {
                    count++;
                }
            }

            return count;
        }

        private void Click(object sender, EventArgs e)
        {
            framesSequenceAnimation.SetFramesSequenceAnimationListener(null);
            GoToLoginScreen();
        }

        private void GoToLoginScreen()
        {
            Finish();
            StartActivity(new Intent(this, typeof(LoginActivity)));
            OverridePendingTransition(0, Resource.Animation.abc_fade_out);
        }

        void FramesSequenceAnimationListener.AnimationStopped()
        {
            GoToLoginScreen();
        }

        protected override void OnSaveInstanceState(Bundle outState)
        {
            base.OnSaveInstanceState(outState);

            outState.PutInt(KEY_CURRENT_FRAME, framesSequenceAnimation.GetCurrentFrame());
        }
    }
}
Kedah answered 23/11, 2017 at 17:2 Comment(1)
you helped me saving time today and it works nicely well +1Aylward
S
0

It's big problem with the sdk but it can be solved by using threads for concurrently loading the bitmap images instead of loading the entire image at the same time.

Squeegee answered 17/1, 2012 at 4:31 Comment(2)
I add the resource file code in the question now. Thanks for your quick reply. Please kindly let me know where i am making mistake.Elisavetgrad
@ JAFFER, Thanks for your answer, but using thread how can i load my images? Can you please give me some hint or tip that how should i go ahead. I found in many sites to use Bitmap or BitmapFactory.options but i don't know anything about how to use this classes in my case.Elisavetgrad
S
0

I solved my outOfMemoryError problem by cutting down the framerate brutally and scaling down the images in gimp. Depending on what you are doing you can probably get away with a lot less fps than you'd expect.

Skipp answered 1/12, 2015 at 23:59 Comment(0)
I
0

I have solve this problem by put all the images in array and use delay after show each of them. The array of images source in res/string <!-- Array table for the pictures to show on the spinner--> <array name="spinner_list"> <item>@drawable/arrows_loop__00000_org</item> <item>@drawable/arrows_loop__00005_org</item> <item >@drawable/arrows_loop__00010_org</item> <item>@drawable/arrows_loop__00015_org</item> <item >@drawable/arrows_loop__00020_org</item> <item >@drawable/arrows_loop__00025_org</item> . . . </array> I declare about the spinner imageView private static ImageView imagespinner;

Then in my class I call it here:

      final TypedArray imgs = getResources().obtainTypedArray(R.array.spinner_list);
    runimage(imgs, imgs.length());

and then on runimage I do the loop with delay like this:

        /* handle the spinner frame by frame */

public void runimage(final TypedArray array, int index) {

  int size = array.length();

    if(index<size) {// show in sequence the images

    final int localindex= index;

        handler.postDelayed(new Runnable() {
            public void run() {
                imagespinner.setImageResource(array.getResourceId(localindex, -1));// find the picture to show
                runimage(array,(localindex+1));// because use final arg need to do the increase inside
            }
        }, 55);

    }
    else // after show all images go ahead
    {
        textview2.setVisibility(View.VISIBLE);
        handler.postDelayed(myRunnablewait, 2000); // make some time to see text before go to ather fragment
    }

}

so I run all the images with 55milsec delay on the imagespinner. After finish do the nex job.

Indomitable answered 16/12, 2015 at 15:40 Comment(0)
G
0

Thanks to Steven's Answer, I've adapted Solution#1 to Kotlin and Coroutine, and it works very well too. The Kotlin version leverages coroutines for efficient asynchronous processing, which can be more idiomatic and concise in Kotlin compared to the original Java approach.

I've also modified the animation logic to recycle Bitmaps after the animation completes, rather than recycling each one during the animation. This adjustment could reduce the occurrence of the Bitmap has been recycled error.

To use this code, call the doFrameAnimation function with the following parameters: ImageView (where the animation will be displayed), resourceId (the resource ID of the AnimationDrawable XML file), and resources.

Here's the Kotlin implementation:

data class DrawableFrame(
    val bytes: ByteArray? = null,
    val duration: Long = DEFAULT_DURATION
)

const val DEFAULT_DURATION = 40L
private const val DRAWABLE_ITEM = "item"
private const val ITEM_DRAWABLE_TAG = "drawable"
private const val ITEM_DURATION_TAG = "duration"

suspend fun ImageView.doFrameAnimation(
    resourceId: Int,
    resources: Resources
) = withContext(Dispatchers.Default) {
    loadDrawableFrameFromXml(resourceId, resources)?.let { frames ->
        val drawableUsed = mutableListOf<BitmapDrawable?>()

        for (frame in frames) {
            val startTime = System.currentTimeMillis()
            val curDrawable = frame.bytes?.let {
                BitmapDrawable(context.resources,
                    BitmapFactory.decodeByteArray(it, 0, it.size))
            }

            withContext(Dispatchers.Main) {
                setImageDrawable(curDrawable)
            }
            drawableUsed.add(curDrawable)

            val delayTime = frame.duration - (System.currentTimeMillis() - startTime)
            delay(delayTime)
        }

        withContext(Dispatchers.Main) {
            setImageDrawable(null)
        }

        drawableUsed.forEach {
            it?.bitmap?.recycle()
        }
    }
}

private suspend fun loadDrawableFrameFromXml(
    resourceId: Int,
    resources: Resources
): List<DrawableFrame>? = withContext(Dispatchers.IO) {
    resources.getXml(resourceId).run {
        val result = mutableListOf<DrawableFrame>()
        try {
            var eventType = eventType
            while (eventType != XmlPullParser.END_DOCUMENT) {
                if (eventType == XmlPullParser.START_TAG && name == DRAWABLE_ITEM) {
                    processDrawableItem(resources, result)
                }
                eventType = next()
            }
            result
        } catch (e: IOException) {
            e.printStackTrace()
            null
        } catch (e: XmlPullParserException) {
            e.printStackTrace()
            null
        }
    }
}

private fun XmlResourceParser.processDrawableItem(
    resources: Resources,
    result: MutableList<DrawableFrame>
) {
    var bytes: ByteArray? = null
    var duration: Long = DEFAULT_DURATION

    for (i in 0 until attributeCount) {
        if (getAttributeName(i) == ITEM_DRAWABLE_TAG) {
            getAttributeValue(i).substring(1).toIntOrNull()?.let {
                bytes = resources.openRawResource(it).toByteArray()
            }
        } else if (getAttributeName(i) == ITEM_DURATION_TAG) {
            duration = getAttributeIntValue(i, DEFAULT_DURATION.toInt()).toLong()
        }
    }

    result.add(
        DrawableFrame(bytes, duration)
    )
}

private fun InputStream.toByteArray(): ByteArray? {
    return try {
        val outputStream = ByteArrayOutputStream()
        val buf = ByteArray(1024)
        var len: Int
        while (read(buf).also { len = it } != -1) {
            outputStream.write(buf, 0, len)
        }
        close()
        outputStream.toByteArray()
    } catch (e: IOException) {
        null
    }
}
Gyratory answered 7/11, 2023 at 2:53 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.