Causing OutOfMemoryError in Frame by Frame Animation in Android - android
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
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...
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.
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.
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);
I had this problem and solved it by doing the two following things:
Cut the resolution of the animation images in half...1/4 the size in uncompressed bytes.
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:
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.
Recycle the bitmaps in the AnimationDrawable when you are done using it. This is the magic that frees up the memory.
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
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;
}
}
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());
}
}
}
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.
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.
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.
Related
Save bitmaps from gif / video
I would like to save individual images (bitmap) of a GIF file and a video file. I want 5 pictures of each file. Depending on the length (seconds) of the files, the pictures should take pictures at regular intervals, so that there are 5 pictures. I hope someone can help me, thanks!
I found a good solution (for gifs) with Glide: Glide.with(Activity.this) .asGif() .load(picturePath) .into(new SimpleTarget<GifDrawable>() { #Override public void onResourceReady(#NonNull GifDrawable resource, #Nullable Transition<? super GifDrawable> transition) { try { Object GifState = resource.getConstantState(); Field frameLoader = GifState.getClass().getDeclaredField("frameLoader"); frameLoader.setAccessible(true); Object gifFrameLoader = frameLoader.get(GifState); Field gifDecoder = gifFrameLoader.getClass().getDeclaredField("gifDecoder"); gifDecoder.setAccessible(true); StandardGifDecoder standardGifDecoder = (StandardGifDecoder) gifDecoder.get(gifFrameLoader); for (int i = 0; i < standardGifDecoder.getFrameCount(); i++) { standardGifDecoder.advance(); if (i == 4) { bitmap1 = standardGifDecoder.getNextFrame(); } else if (i == standardGifDecoder.getFrameCount() / 3) { bitmap2 = standardGifDecoder.getNextFrame(); } else if (i == standardGifDecoder.getFrameCount() / 2) { bitmap3 = standardGifDecoder.getNextFrame(); } else if (i == standardGifDecoder.getFrameCount() - 4) { bitmap4 = standardGifDecoder.getNextFrame(); } } } catch (Exception ex) { ex.printStackTrace(); } } });
you can use Android-Ndk-Gif library to handle your needs, and here's a full example of how to get each bitmap from GIF, you can change gifDecoder.frameNum() to 4 so you can get the first 5 frames from your GIF GifDecoder gifDecoder = new GifDecoder(); boolean isSucceeded = gifDecoder.load(Path); // PATH OF YOUR IMAGE ArrayList<BitmapGIFUtil> arrayListBitmaps = new ArrayList<>(); for (int i = 0; i < gifDecoder.frameNum(); ++i) { Bitmap bitmap = gifDecoder.frame(i); int GIFDelay = gifDecoder.delay(i); arrayListBitmaps.add(new BitmapGIFUtil(bitmap, GIFDelay)); } BitmapGIFUtil.java public class BitmapGIFUtil { private Bitmap bitmap; private int Delay; public BitmapGIFUtil() { } public BitmapGIFUtil(Bitmap bitmap, int delay) { this.bitmap = bitmap; Delay = delay; } public Bitmap getBitmap() { return bitmap; } public void setBitmap(Bitmap bitmap) { this.bitmap = bitmap; } public int getDelay() { return Delay; } public void setDelay(int delay) { Delay = delay; } } After filling your arrayListBitmaps you can save each bitmap into your gallery
And here is a solution (for videos) with MediaMetadataRetriever: MediaMetadataRetriever retriever = new MediaMetadataRetriever(); retriever.setDataSource(videoFile.getAbsolutePath()); MediaPlayer mp = MediaPlayer.create(getBaseContext(), vidUri); mp.release(); String strLength = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); long time=Long.valueOf(strLength)*1000; bitmap1 = retriever.getFrameAtTime(time/10*2,MediaMetadataRetriever.OPTION_CLOSEST_SYNC); bitmap2 = retriever.getFrameAtTime(time/10*4,MediaMetadataRetriever.OPTION_CLOSEST_SYNC); bitmap3 = retriever.getFrameAtTime(time/10*6,MediaMetadataRetriever.OPTION_CLOSEST_SYNC); bitmap4 = retriever.getFrameAtTime(time/10*8,MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
Multiple Bitmap update on same android SurfaceView
Recently I tried to write an app which captures frame buffer from say USB camera (that need to be displayed as preview), and image processed output that need to be overlapped on the preview. Can anybody give me some pointers how to start? Moreover I need to draw some rectangle on the preview. I was trying to use multiple SurfaceView to draw but not succeeded. Can anybody help me out?
What you need it the bitmap and image view reference counting. Android keeps image data in native array, which is not recycling automatically when the vm GC running. Vm part is garbage collected one way and the native part is another way and much later. You app may run out of memory pretty quickly. Here is set of classes that can help. I think I've got them from android image tutorial and modified a bit for my own convenience. package com.example.android.streaming.ui.cache; import android.content.Context; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.util.AttributeSet; import android.widget.ImageView; /** * Sub-class of ImageView which automatically notifies the drawable when it is * being displayed. */ public class RecyclingImageView extends ImageView { public RecyclingImageView(Context context) { super(context); } public RecyclingImageView(Context context, AttributeSet attrs) { super(context, attrs); } /** * #see android.widget.ImageView#onDetachedFromWindow() */ #Override protected void onDetachedFromWindow() { // This has been detached from Window, so clear the drawable setImageDrawable(null); super.onDetachedFromWindow(); } /** * #see android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable) */ #Override public void setImageDrawable(Drawable drawable) { // Keep hold of previous Drawable final Drawable previousDrawable = getDrawable(); // Call super to set new Drawable super.setImageDrawable(drawable); // Notify new Drawable that it is being displayed notifyDrawable(drawable, true); // Notify old Drawable so it is no longer being displayed notifyDrawable(previousDrawable, false); } #Override public void setImageResource(int resId) { // Keep hold of previous Drawable final Drawable previousDrawable = getDrawable(); super.setImageResource(resId); // Notify new Drawable that it is being displayed final Drawable newDrawable = getDrawable(); notifyDrawable(newDrawable, true); // Notify old Drawable so it is no longer being displayed notifyDrawable(previousDrawable, false); } /** * Notifies the drawable that it's displayed state has changed. * * #param drawable * #param isDisplayed */ private static void notifyDrawable(Drawable drawable, final boolean isDisplayed) { if (drawable != null) { if (drawable instanceof RecyclingBitmapDrawable) { // The drawable is a CountingBitmapDrawable, so notify it ((RecyclingBitmapDrawable) drawable).setIsDisplayed(isDisplayed); } else if (drawable instanceof LayerDrawable) { // The drawable is a LayerDrawable, so recurse on each layer LayerDrawable layerDrawable = (LayerDrawable) drawable; for (int i = 0, z = layerDrawable.getNumberOfLayers(); i < z; i++) { notifyDrawable(layerDrawable.getDrawable(i), isDisplayed); } } } } } And here is another one, a bitmap itself. package com.example.android.streaming.ui.cache; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.util.Log; import com.example.android.streaming.StreamingApp; import com.vg.hangwith.BuildConfig; /** * A BitmapDrawable that keeps track of whether it is being displayed or cached. * When the drawable is no longer being displayed or cached, * {#link Bitmap#recycle() recycle()} will be called on this drawable's bitmap. */ public class RecyclingBitmapDrawable extends BitmapDrawable { private int cacheRefCount = 0; private int displayRefCount = 0; private boolean hasBeenDisplayed; public RecyclingBitmapDrawable(Resources res, Bitmap bitmap) { super(res, bitmap); } /** * Notify the drawable that the displayed state has changed. Internally a * count is kept so that the drawable knows when it is no longer being * displayed. * * #param isDisplayed * - Whether the drawable is being displayed or not */ public void setIsDisplayed(boolean isDisplayed) { synchronized (this) { if (isDisplayed) { displayRefCount++; hasBeenDisplayed = true; } else { displayRefCount--; } } // Check to see if recycle() can be called checkState(); } /** * Notify the drawable that the cache state has changed. Internally a count * is kept so that the drawable knows when it is no longer being cached. * * #param isCached * - Whether the drawable is being cached or not */ public void setIsCached(boolean isCached) { synchronized (this) { if (isCached) { cacheRefCount++; } else { cacheRefCount--; } } // Check to see if recycle() can be called checkState(); } private synchronized void checkState() { // If the drawable cache and display ref counts = 0, and this drawable // has been displayed, then recycle if (cacheRefCount <= 0 && displayRefCount <= 0 && hasBeenDisplayed && hasValidBitmap()) { if (BuildConfig.DEBUG) Log.d(StreamingApp.TAG, "No longer being used or cached so recycling. " + toString()); getBitmap().recycle(); } } private synchronized boolean hasValidBitmap() { Bitmap bitmap = getBitmap(); return bitmap != null && !bitmap.isRecycled(); } } Now, iun your activity, whatever it does, if it needs to present recyclable image, you add this in xml res: <com.example.android.streaming.ui.cache.RecyclingImageView android:id="#+id/ad_image" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_centerInParent="true" android:background="#drawable/bkgd_whitegradient" android:contentDescription="#string/dummy_desc" android:padding="20dip"/> This is just an example, id, background, can be whatever you need. final RecyclingImageView adImage = (RecyclingImageView) findViewById(R.id.ad_image); adImage.setImageDrawable(new RecyclingBitmapDrawable(getResources(), getBitmap(this))); adImage.setVisibility(View.VISIBLE); Note the getBitmap(), this is an example. It is you who should implement it in a way you need. It returns Bitmap instance. In your case, this Bitmap will be created out of array of bytes you've received from your camera. Let's try to do it here too. Next, I have a class for managing avatars in my app (long list of users is a good example). It has number of useful static methods, so you may not need to create it. package com.example.android.streaming.ui.cache; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.URL; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import org.json.JSONObject; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.AsyncTask; import android.util.Log; import android.util.LruCache; import com.example.android.streaming.StreamingApp; import com.example.android.streaming.datamodel.Broadcast; import com.example.android.streaming.datamodel.Channel; import com.facebook.model.GraphUser; import com.parse.ParseFile; import com.parse.ParseUser; import com.vg.hangwith.BuildConfig; import com.vg.hangwith.R; public class AvatarCache { private Map<String, LoadImageTask> tasks = new HashMap<String, AvatarCache.LoadImageTask>(); private LruCache<String, RecyclingBitmapDrawable> memoryCache; public final static int AVATAR_BOUNDS = 100; private String cacheDir; private Context context; public synchronized void addTask(String tag, LoadImageTask task) { tasks.put(tag, task); if (BuildConfig.DEBUG) Log.d(StreamingApp.TAG, "Added avatar load task for tag " + tag); } public synchronized void removeTask(String tag) { tasks.remove(tag); if (BuildConfig.DEBUG) Log.d(StreamingApp.TAG, "Removed avatar load task for tag " + tag); } public synchronized void cancelTasks(int keepLastItems) { int count = 0; Iterator<Map.Entry<String, LoadImageTask>> iter = tasks.entrySet().iterator(); while (iter.hasNext() && tasks.size() > keepLastItems) { Map.Entry<String, LoadImageTask> entry = iter.next(); entry.getValue().cancel(true); iter.remove(); count++; } if (BuildConfig.DEBUG) Log.d(StreamingApp.TAG, "Canceled " + count + " avatar load tasks"); } public void cancelTasks() { cancelTasks(0); } public final static Bitmap downscaleAvatar(Bitmap bitmap) { if (bitmap.getWidth() > AVATAR_BOUNDS && bitmap.getHeight() > AVATAR_BOUNDS) { int height = (int) Math.floor(bitmap.getHeight() / ((1.0f * bitmap.getWidth()) / AVATAR_BOUNDS)); Bitmap scaled = Bitmap.createScaledBitmap(bitmap, AVATAR_BOUNDS, height, false); bitmap.recycle(); bitmap = null; return scaled; } else { return bitmap; } } public final static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { // Raw height and width of image final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { // Calculate ratios of height and width to requested height and width final int heightRatio = Math.round((float) height / (float) reqHeight); final int widthRatio = Math.round((float) width / (float) reqWidth); // Choose the smallest ratio as inSampleSize value, this will guarantee // a final image with both dimensions larger than or equal to the // requested height and width. inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; } return inSampleSize; } public class LoadImageTask extends AsyncTask<Void, Void, RecyclingBitmapDrawable> { protected RecyclingImageView image; protected String url, tag; protected boolean avatar; public LoadImageTask(String url, String tag, boolean avatar, RecyclingImageView image) { super(); this.url = url; this.tag = tag; this.image = image; this.avatar = avatar; image.setTag(R.string.tag_key, tag); addTask(tag, this); } #Override protected RecyclingBitmapDrawable doInBackground(Void... dummy) { if (isCancelled() || !isSameImage()) return null; RecyclingBitmapDrawable drawable = getAvatarFromMemCache(tag); if (drawable == null) { drawable = getAvatarFromDiskCache(tag); if (drawable == null) { try { if (BuildConfig.DEBUG) Log.d(StreamingApp.TAG, "Loading avatar " + url); /* First decode bounds to check the image size. */ BitmapFactory.Options options = new BitmapFactory.Options(); /* Calculate if the avatar should be down scaled. */ if (avatar) { options.inJustDecodeBounds = true; BitmapFactory.decodeStream(new URL(url).openConnection().getInputStream(), null, options); options.inSampleSize = calculateInSampleSize(options, AVATAR_BOUNDS, AVATAR_BOUNDS); } options.inJustDecodeBounds = false; /* Download down scaled avatar. */ Bitmap bitmap = BitmapFactory.decodeStream(new URL(url).openConnection().getInputStream(), null, options); if (bitmap != null) { drawable = new RecyclingBitmapDrawable(context.getResources(), bitmap); if (drawable != null) { addAvatarToDiskCache(tag, url, drawable); addAvatarToMemoryCache(tag, drawable); } } } catch (Exception e) { Log.w(StreamingApp.TAG, "Failed to load and save avatar image. " + e.getMessage()); } } else { addAvatarToMemoryCache(tag, drawable); } } return drawable; } private synchronized boolean isSameImage() { // In case that the same image is reused for different avatar (during scroll), this // function will return false. Object imageTag = image.getTag(R.string.tag_key); return imageTag != null && imageTag.equals(tag); } private void finishedWithResult(RecyclingBitmapDrawable result) { if (result != null && isSameImage()) image.setImageDrawable(result); removeTask(tag); } #Override protected void onPostExecute(RecyclingBitmapDrawable result) { finishedWithResult(result); super.onPostExecute(result); } #Override protected void onCancelled(RecyclingBitmapDrawable result) { finishedWithResult(result); super.onCancelled(); } #Override protected void onCancelled() { finishedWithResult(null); super.onCancelled(); } } public AvatarCache(Context context) { super(); // Get max available VM memory, exceeding this amount will throw an // OutOfMemory exception. Stored in kilobytes as LruCache takes an // int in its constructor. final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // Use 1/10th of the available memory for this memory cache. With small avatars like // we have this is enough to keep ~100 avatars in cache. final int cacheSize = maxMemory / 10; if (BuildConfig.DEBUG) Log.d(StreamingApp.TAG, "Init avatar cache, size: " + cacheSize + ", max mem size: " + maxMemory); memoryCache = new LruCache<String, RecyclingBitmapDrawable>(cacheSize) { #Override protected int sizeOf(String key, RecyclingBitmapDrawable drawable) { // The cache size will be measured in kilobytes rather than // number of items. Bitmap bitmap = drawable.getBitmap(); int bitmapSize = bitmap != null ? bitmap.getByteCount() / 1024 : 0; return bitmapSize == 0 ? 1 : bitmapSize; } #Override protected void entryRemoved(boolean evicted, String key, RecyclingBitmapDrawable oldValue, RecyclingBitmapDrawable newValue) { // The removed entry is a recycling drawable, so notify it. // that it has been removed from the memory cache oldValue.setIsCached(false); } }; this.cacheDir = context.getCacheDir().getAbsolutePath(); this.context = context; } public void flush() { int oldSize = memoryCache.size(); memoryCache.evictAll(); if (BuildConfig.DEBUG) Log.d(StreamingApp.TAG, "Flush avatar cache, flushed " + (oldSize - memoryCache.size()) + " new size " + memoryCache.size()); cancelTasks(); } public void addAvatarToMemoryCache(String key, RecyclingBitmapDrawable drawable) { if (getAvatarFromMemCache(key) == null) { drawable.setIsCached(true); memoryCache.put(key, drawable); if (BuildConfig.DEBUG) Log.d(StreamingApp.TAG, "Add to avatar cache, size: " + memoryCache.size()); } } public RecyclingBitmapDrawable getAvatarFromMemCache(String key) { return memoryCache.get(key); } public void addAvatarToDiskCache(String name, String url, RecyclingBitmapDrawable drawable) throws IOException { if (drawable == null) return; File dir = new File(cacheDir); if (!dir.exists()) dir.mkdirs(); File file = new File(dir, name); Bitmap bitmap = drawable.getBitmap(); if (!file.exists() && bitmap != null) { OutputStream out = new BufferedOutputStream(new FileOutputStream(file)); drawable.getBitmap().compress(Bitmap.CompressFormat.PNG, 85, out); out.flush(); out.close(); } } /* * Update avatar from the network if older than this. */ public static final int AVATAR_MAX_AGE_DAYS = 7; public RecyclingBitmapDrawable getAvatarFromDiskCache(String name) { File file = new File(cacheDir, name); /* Check if cached bitmap is old. */ if ((System.currentTimeMillis() - file.lastModified()) > AVATAR_MAX_AGE_DAYS * 24 * 60 * 60 * 1000) return null; try { Bitmap bitmap = BitmapFactory.decodeFile(file.getCanonicalPath()); if (bitmap != null) { // Log.w(App.TAG, "Loaded " + (bitmap.getByteCount() / 1024.0f) + "K bitmap " + name + " w: " // + bitmap.getWidth() + " h: " + bitmap.getHeight()); return new RecyclingBitmapDrawable(context.getResources(), bitmap); } } catch (Exception e) { Log.w(StreamingApp.TAG, "Failed to decode avatar image " + name + ". " + e.getMessage()); } return null; } public static boolean isValidURL(String url) { try { new URL(url); return true; } catch (Exception e) { } return false; } public void loadUrlAvatar(String url, String name, RecyclingImageView image, int placeholder, boolean checkDiskCache) { RecyclingBitmapDrawable drawable = getAvatarFromMemCache(name); if (drawable == null && checkDiskCache) { drawable = getAvatarFromDiskCache(name); if (drawable != null) addAvatarToMemoryCache(name, drawable); } if (drawable == null) { image.setImageResource(placeholder); if (url != null && isValidURL(url)) new LoadImageTask(url, name, true, image).execute(); } else { image.setImageDrawable(drawable); } } public static String getUserAvatarURL(ParseUser user) { if (user == null) return null; if (user.get("avatar") == null || user.get("avatar") == JSONObject.NULL) return user.getString("avatar_url"); if (user.get("avatar") instanceof JSONObject) Log.w(StreamingApp.TAG, "JSONObject found instead of ParseFile: " + ((JSONObject) user.get("avatar")).toString()); return ((ParseFile) user.get("avatar")).getUrl(); } public static String getUserAvatarURL(GraphUser user) { return "http://graph.facebook.com/" + user.getId() + "/picture"; } public static String getBroadcastAvatarURL(Broadcast broadcast) { if (broadcast.getThumbnail() == null) return null; return broadcast.getThumbnail().getUrl(); } public void loadUserAvatar(ParseUser user, RecyclingImageView image, int placeholder, boolean checkDiskCache) { if (user != null) loadUrlAvatar(getUserAvatarURL(user), user.getUsername(), image, placeholder, checkDiskCache); } public void loadUserAvatar(GraphUser user, RecyclingImageView image, int placeholder, boolean checkDiskCache) { if (user != null) loadUrlAvatar(getUserAvatarURL(user), user.getId(), image, placeholder, checkDiskCache); } public void loadBroadcastAvatar(Broadcast broadcast, RecyclingImageView image, int placeholder, boolean checkDiskCache) { if (broadcast != null) loadUrlAvatar(getBroadcastAvatarURL(broadcast), broadcast.getObjectId(), image, placeholder, checkDiskCache); } public void clearUserAvatar(ParseUser user) { File file = new File(cacheDir, user.getUsername()); if (file.exists()) file.delete(); memoryCache.remove(user.getUsername()); if (BuildConfig.DEBUG) Log.d(StreamingApp.TAG, "Remove avatar from cache, size: " + memoryCache.size()); } public static String getChannelImageURL(Channel channel, boolean small, boolean ageRestricted) { if (ageRestricted) { if (small && channel.getSmallRestrictedState() != null) return channel.getSmallRestrictedState().getUrl(); else if (!small && channel.getLargeRestrictedState() != null) return channel.getLargeRestrictedState().getUrl(); } else { if (small && channel.getSmallEmptyState() != null) return channel.getSmallEmptyState().getUrl(); else if (!small && channel.getLargeEmptyState() != null) return channel.getLargeEmptyState().getUrl(); } return null; } public static final String channelImageCacheName(Channel channel, boolean small, boolean ageRestricted) { return channel.getObjectId() + "-" + (ageRestricted ? "age" : "empty") + "-" + (small ? "small" : "large"); } public boolean loadChannelImage(Channel channel, RecyclingImageView image, boolean checkDiskCache, boolean small, boolean ageRestricted) { boolean result = false; if (channel == null) return false; String name = channelImageCacheName(channel, small, ageRestricted); RecyclingBitmapDrawable drawable = getAvatarFromMemCache(name); if (drawable == null && checkDiskCache) { drawable = getAvatarFromDiskCache(name); if (drawable != null) addAvatarToMemoryCache(name, drawable); } if (drawable == null) { String url = getChannelImageURL(channel, small, ageRestricted); result = url != null && isValidURL(url); if (result) new LoadImageTask(url, name, false, image).execute(); } else { image.setImageDrawable(drawable); result = true; } return result; } public void loadUrlImage(String url, RecyclingImageView image, String name, boolean checkDiskCache) { RecyclingBitmapDrawable drawable = getAvatarFromMemCache(name); if (drawable == null && checkDiskCache) { drawable = getAvatarFromDiskCache(name); if (drawable != null) addAvatarToMemoryCache(name, drawable); } if (drawable == null) { if (url != null && isValidURL(url)) new LoadImageTask(url, name, false, image).execute(); } else { image.setImageDrawable(drawable); } } } Note, it uses Parse framework at some places. Just ignore it. In this example, AvatarCache is loading image by url in doInBackground() function. As you can see it gets an input stream of out url. You can modify it to feed it some different input stream that you use for loading your image. Then you also need to modify loadUrlImage(). In other words, just remove the url thing. And this is how you can use it with Uri. Modify it for using input stream or array of bytes. Just use appropriate BitmapFactory.decodeSomething() method. public Bitmap getBitmap(Uri uri) { BitmapFactory.Options options = new BitmapFactory.Options(); AssetFileDescriptor fd = null; Bitmap b = null; try { fd = getContentResolver().openAssetFileDescriptor(uri, "r"); if (fd != null) { options.inJustDecodeBounds = true; BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, options); options.inSampleSize = AvatarCache.calculateInSampleSize(options, AvatarCache.AVATAR_BOUNDS, AvatarCache.AVATAR_BOUNDS); options.inJustDecodeBounds = false; b = BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, options); try { fd.close(); } catch (IOException e) { } } } catch (Exception e) { e.printStackTrace(); } return b; }
As you can see, AvatarCache is only used statically in this example. In case you need to manage a lot of images, like a photo lib preview.create AvatarCache in your app instance. Also add memory mgnt methods. #Override public void onCreate() { super.onCreate(); avatarCache = new AvatarCache(this); } public void onTrimMemory(int level) { if (level == TRIM_MEMORY_COMPLETE || level == TRIM_MEMORY_RUNNING_CRITICAL || level == TRIM_MEMORY_RUNNING_LOW) { if (avatarCache != null) avatarCache.flush(); } super.onTrimMemory(level); } #Override public void onLowMemory() { Log.w(StreamingApp.TAG, "Low memory event received. Clear avatars cache."); if (avatarCache != null) avatarCache.flush(); super.onLowMemory(); } And then you can use it a way like this: avatarCache.loadUserAvatar(...); It will automatically load the image and place it to the cache. When the app is short of memory, cache will be flushed. Hope this helps. It is quite a lot of stuff here but when you go through this once, you will never have issues with images in your android app. Happy coding! PS. Ask questions if you need. Just be specific with what you need asking and give also some context of your particular use case.
Android outofmemory bitmap
I have used the following animation class because i have multiple images to make an animation. After the 7th animation i get OutofMemoryError. androidgraphics.Bitmap.createBitmap 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 = {}; // animation splash screen frames /** * #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, int[] n) { return new FramesSequenceAnimation(imageView, n); } /** * 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) { mHandler = new Handler(); mFrames = frames; mIndex = -1; mSoftReferenceImageView = new SoftReference<ImageView>(imageView); mShouldRun = false; mIsRunning = false; mDelayMillis = 50; 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 = mIndex - 1; mShouldRun = false; } 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.onAnimationStopped(); } 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; } } } Can anyone explain me why and tell me how to fix it? I already added <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> to my manifest file.
It might help to be sure that you are not keeping references to unneeded images. This is happening because you probably have the "standard" memory heap of 32MB or something similar (maybe 64MB or maybe 16MB). If you consider that most images are 5MB or more, it's not surprising you are out of memory. You can increase the heap size using android:largeHeap="true" like this: How to increase heap size of an android application?
Drawing dynamic elements onto a normal xml layout
I've been trying to figure this out for a while now... I need to place marks over top of a seekBar to show the user places that they bookmarked in the past. The data is stored in xml. The problem is making the little ovals appear over the seekBar... It just doesn't work... Here's my code: public class seekMark extends View { private int seekLength; // in pixels private int seekLeftPad; // in pixels private int seekBottomPad; // in pixels private int trackLength; // in ms private float pxOverMs; // in px/ms ShapeDrawable lmark; private seekMark instance; public seekMark(Context context){ super(context); instance = this; seekLength = progressBar.getWidth(); seekLeftPad = progressBar.getPaddingLeft(); seekBottomPad = progressBar.getBottom(); trackLength = player.getDuration(); pxOverMs = pxPerMs(); lmark = new ShapeDrawable(new OvalShape()); } private float pxPerMs(){ return ((float) seekLength)/((float) trackLength); } private int[] markPxList() throws XmlPullParserException, IOException { int bmStartTime = 0; String bmNames[] = bmNameList(xmlPath); int[] bmPos = new int[bmNames.length]; for(int i=0; i < bmNames.length; i++){ bmStartTime = getBookmark(xmlPath, bmNames[i]); bmPos[i] = (int) (bmStartTime * pxOverMs); } return (bmPos); } public void markPlace() throws XmlPullParserException, IOException { int y = seekBottomPad; int x = 0; int bmPos[] = markPxList(); for(int i = 0; i < bmPos.length; i++){ x = bmPos[i] + seekLeftPad; lmark = new ShapeDrawable(); lmark.getPaint().setColor(0xff74AC23); lmark.setBounds(x, y, x + 1, y + 1); instance.invalidate(); } } protected void onDraw(Canvas canvas) { lmark.draw(canvas); } } It's called from onCreate using this code. I call it using in another thread to avoid the problem where the dimensions of progressBar aren't yet set in onCreate. Display display = ((WindowManager) getSystemService(WINDOW_SERVICE)).getDefaultDisplay(); if (display.getRotation() == 1){ // if landscape final Runnable runner = new Runnable() { public void run() { seekMark seekMarks = new seekMark(context); try { seekMarks.markPlace(); } catch (XmlPullParserException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } // runs in another thread to avoid the problem with calling // seekMark directly from onCreate } }; handler.postDelayed(runner, 1000); } The program crashes whenever I try to call seekMark.markPlace()... I'm trying to draw this over top of my layout main.xml.
im not sure if this is what you are trying to do. Customize Seekbar this seems to be similar while the approach is different.
Android: Bitmap not shown in ImageView
I'm developing an Activity which loads a list of URLs of images and displays them in a Gallery view. For better performance I decided to asynchronously load the images and cache them on the SD card. I found this: http://blog.jteam.nl/2009/09/17/exploring-the-world-of-android-part-2/ and adapted the code. As long as I didn't cache the files on the SD card everything worked fine, but now I see a strange behavior. I guess it's easier to describe the problem with code (see comments): public class AsyncImageLoader { private final static String TAG = "AsyncImageLoader"; private HashMap<String, SoftReference<Bitmap>> bitmapMap; public AsyncImageLoader() { this.bitmapMap = new HashMap<String, SoftReference<Bitmap>>(); } // This method is called to load images asynchronously. // If the Bitmap is cached in bitmapMap and the reference is // still valid, the Bitmap is returned immediately. // If the Bitmap is not in bitmapMap, another thread is started // to load the image. Once it has been loaded, a callback method // is called. public Bitmap loadBitmap(final String imageUrl, final IImageLoadListener imageCallback, final int minWidth, final int minHeight) { if (this.bitmapMap.containsKey(imageUrl)) { SoftReference<Bitmap> softReference = this.bitmapMap.get(imageUrl); Bitmap bitmap = softReference.get(); if (bitmap != null) { Log.d(TAG, "Using a previously loaded Bitmap container"); return bitmap; } } Log.d(TAG, "Need to load the Bitmap container"); final Handler handler = new Handler() { #Override public void handleMessage(Message message) { imageCallback.imageLoaded((Bitmap) message.obj, imageUrl); } }; new Thread() { #Override public void run() { Bitmap bitmap = loadImageFromUrl(imageUrl, minWidth, minHeight); bitmapMap.put(imageUrl, new SoftReference<Bitmap>(bitmap)); Message message = handler.obtainMessage(0, bitmap); handler.sendMessage(message); } }.start(); return null; } // Here is part of the magic behind the scenes: // I get an InputStream (see below) and open it just // to get the Bitmap's dimensions. Then I calculate the // required width (best size for minWidth and minHeight) // and then I create a downsampled Bitmap. private synchronized static Bitmap loadImageFromUrl(String urlString, int minWidth, int minHeight) { Bitmap bitmap = null; try { InputStream is = getInputStream(urlString); BitmapFactory.Options options1 = new BitmapFactory.Options(); options1.inJustDecodeBounds = true; BitmapFactory.decodeStream(is, null, options1); is.close(); int tempWidth = options1.outWidth; int tempHeight = options1.outHeight; int scale = 1; while (true) { if (tempWidth / 2 < minWidth || tempHeight / 2 < minHeight) { break; } tempWidth /= 2; tempHeight /= 2; scale *= 2; } BitmapFactory.Options options2 = new BitmapFactory.Options(); options2.inSampleSize = scale; bitmap = BitmapFactory.decodeStream(getInputStream(urlString), null, options2); // More magic: Once the Bitmap has been downloaded, // I create a file on the SD card so that I don't // need to download it again. File cacheFile = getCacheFile(urlString); if (cacheFile == null) { cacheImage(urlString, bitmap); } } catch (IOException e) { Log.e(TAG, e.getMessage(), e); } return bitmap; } // I check whether a cache file exists for the URL. If it exists // I create the InputStream from this file and I create the InputStream // from the URL otherwise. private synchronized static InputStream getInputStream(String urlString) throws IOException { File cacheFile = getCacheFile(urlString); if (cacheFile != null) { // HERE seems to be something wrong. The file // is valid and can be read, but if I return a // FileInputStream (or InputStream using // cacheFile.toURL().openStream()) the image is not // shown. Log.d(TAG, "Using " + cacheFile.getAbsolutePath()); return new FileInputStream(cacheFile); } else { // In this case, the image is shown everytime! Log.d(TAG, "Downloading " + urlString); URL url = new URL(urlString); return url.openStream(); } } // This is just a helper method that returns the cache directory // and if it doesn't exist it will be created first. private synchronized static File getImageCacheDir() { File imageCacheDir = new File(Environment.getExternalStorageDirectory() .getAbsolutePath() + "/" + Commons.APP_DIRECTORY + "/cache/"); if (!imageCacheDir.exists()) { imageCacheDir.mkdirs(); } return imageCacheDir; } // This method returns a File object for the cache file if it exists // and returns null otherwise. private static File getCacheFile(String urlString) { File file = new File(getImageCacheDir(), CryptoUtils.md5(urlString) + ".jpg"); if (!file.exists()) { return null; } return file; } // Even more magic: // At first I create a file with a .tmp extension just in case two // threads try to read and write at the same time. The image is // downloaded into the temporary file. If this succeeds the .tmp // extension will be removed. private synchronized static void cacheImage(String urlString, Bitmap bitmap) { String filename = CryptoUtils.md5(urlString) + ".jpg"; File tmpFile = new File(getImageCacheDir(), filename + ".tmp"); if (tmpFile.exists()) { Log.d(TAG, "Another process seems to create the cache file."); return; } File file = new File(getImageCacheDir(), filename); FileOutputStream os = null; try { os = new FileOutputStream(tmpFile); boolean retval = bitmap .compress(Bitmap.CompressFormat.JPEG, 90, os); if (retval) { tmpFile.renameTo(file); Log.d(TAG, "Created cache image. Renaming temporary file."); } else { tmpFile.delete(); Log.d(TAG, "Could not create cache image. Deleting temporary file."); } } catch (FileNotFoundException e) { Log.e(TAG, e.getMessage(), e); } finally { if (os != null) { try { os.close(); } catch (IOException e) { Log.e(TAG, e.getMessage(), e); } } } } public interface IImageLoadListener { public void imageLoaded(Bitmap imageBitmap, String imageUrl); } } Here is another big chunk of code: public class DisplayPhotoActivity extends Activity { private final static int PROGRESS_DIALOG = 1; private final static int PHOTO_PICKER = 2; private final static int GALLERY_REQUEST_CODE = 1; private final static int CAMERA_REQUEST_CODE = 2; private AsyncImageLoader asyncImageLoader; private Gallery gallery; private List<Photo> images; private ImageAdapter imageAdapter; private int loadingCounter; private Uri outputFileUri; private Display display; #Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.display_photo); this.asyncImageLoader = new AsyncImageLoader(); this.loadingCounter = 0; this.images = new ArrayList<Photo>(); this.imageAdapter = new ImageAdapter(this, this.images); final ImageView imageView = (ImageView) findViewById(R.id.large_image); this.gallery = (Gallery) findViewById(R.id.gallery); this.gallery.setAdapter(this.imageAdapter); // The Gallery is on top of the Activity and there is a bigger // ImageView below it. I want the image to be shown in the bigger // ImageView when it has been clicked in the Gallery. this.gallery.setOnItemClickListener(new OnItemClickListener() { public void onItemClick(AdapterView<?> parent, View v, int position, long id) { // Here is the part where I load the Bitmap. // This part works! Bitmap bitmap = asyncImageLoader.loadBitmap(images .get(position).getUrl(), new AsyncImageLoader.IImageLoadListener() { public void imageLoaded(Bitmap imageBitmap, String imageUrl) { if (imageBitmap != null) { imageView.setImageBitmap(imageBitmap); gallery.invalidate(); } loadingCounter--; if (loadingCounter == 0) { getParent() .setProgressBarIndeterminateVisibility( false); } } }, 300, 300); if (bitmap != null) { Log.d("DisplayPhotoActivity", "Image was cached. (I)"); imageView.setImageBitmap(bitmap); gallery.invalidate(); loadingCounter--; if (loadingCounter == 0) { getParent() .setProgressBarIndeterminateVisibility(false); } } } }); this.display = (Display) getIntent().getParcelableExtra("display"); populatePhotos(true); } // There is a known bug and this is the bug fix. // See: http://code.google.com/p/android/issues/detail?id=8488 #Override public void onPause() { super.onPause(); System.gc(); } #Override public void onDestroy() { super.onDestroy(); this.asyncImageLoader = null; System.gc(); } #Override protected Dialog onCreateDialog(int id) { switch (id) { case PROGRESS_DIALOG: ProgressDialog progressDialog = new ProgressDialog(this); progressDialog.setMessage("Loading..."); return progressDialog; case PHOTO_PICKER: final CharSequence[] items = { getText(R.string.photo_picker_choose_from_gallery), getText(R.string.photo_picker_take_a_photo) }; AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(getText(R.string.photo_picker_title)); builder.setItems(items, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int item) { switch (item) { case 0: onChooseFromGalleryClick(); break; case 1: onTakeAPhoto(); } } }); return builder.create(); default: return null; } } private void onChooseFromGalleryClick() { Intent intent = new Intent(); intent.setType("image/*"); intent.setAction(Intent.ACTION_GET_CONTENT); startActivityForResult(Intent.createChooser(intent, getText(R.string.photo_picker_choose_from_gallery)), GALLERY_REQUEST_CODE); } private void onTakeAPhoto() { Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); File appDirectory = new File(Environment.getExternalStorageDirectory(), Commons.APP_DIRECTORY); if (!appDirectory.exists()) { appDirectory.mkdirs(); } File photo = new File(appDirectory, "/pic.jpg"); this.outputFileUri = Uri.fromFile(photo); cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, this.outputFileUri); startActivityForResult(Intent.createChooser(cameraIntent, getText(R.string.photo_picker_take_a_photo)), CAMERA_REQUEST_CODE); } #Override public boolean onCreateOptionsMenu(Menu menu) { MenuItem updateMenuItem = menu.add(this.getResources().getString( R.string.update_menuitem_text)); updateMenuItem .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { populatePhotos(true); return false; } }); updateMenuItem.setIcon(R.drawable.ic_menu_refresh); MenuItem uploadPhotoMenuItem = menu.add(this.getResources().getString( R.string.photo_picker_title)); uploadPhotoMenuItem .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { showDialog(PHOTO_PICKER); return true; } }); uploadPhotoMenuItem.setIcon(getResources().getDrawable( android.R.drawable.ic_menu_add)); return true; } #Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK) { Uri uri = null; switch (requestCode) { case GALLERY_REQUEST_CODE: uri = data.getData(); break; case CAMERA_REQUEST_CODE: uri = this.outputFileUri; break; } if (uri == null) { return; } Display display = this.getIntent().getParcelableExtra("display"); this.startService(PhotoUploadService.getIntent(this, display, uri)); } } private void populatePhotos(boolean forceSync) { new ImageListLoader().execute(this.display.getId(), forceSync ? 1 : 0); } private void updateInitialImage(Bitmap bitmap) { ImageView imageView = (ImageView) findViewById(R.id.large_image); if (imageView.getDrawable() == null) { Log.d("DisplayPhotoActivity", "No initial image set."); imageView.setImageBitmap(bitmap); } } public class ImageListLoader extends AsyncTask<Integer, Void, Void> { #Override protected void onPreExecute() { showDialog(PROGRESS_DIALOG); } #Override protected Void doInBackground(Integer... params) { if (params.length < 1) { return null; } int displayId = params[0]; boolean forceSync = params.length == 2 && params[1] == 1; List<Photo> photos = LocalDatabaseHelper.getInstance() .getPhotosByDisplayId(displayId, forceSync); images.clear(); for (Photo photo : photos) { images.add(photo); } Log.d("FOO", "Size: " + images.size()); runOnUiThread(new Runnable() { public void run() { imageAdapter.notifyDataSetChanged(); } }); return null; } #Override protected void onPostExecute(Void result) { dismissDialog(PROGRESS_DIALOG); } } public class ImageAdapter extends BaseAdapter { private Context context; private List<Photo> images; private int galleryItemBackground; public ImageAdapter(Context context, List<Photo> images) { this.context = context; this.images = images; TypedArray attr = this.context .obtainStyledAttributes(R.styleable.DisplayPhotoActivity); this.galleryItemBackground = attr .getResourceId( R.styleable.DisplayPhotoActivity_android_galleryItemBackground, 0); attr.recycle(); } public int getCount() { return this.images.size(); } public Object getItem(int position) { return position; } public long getItemId(int position) { return position; } public View getView(int position, View convertView, ViewGroup parent) { getParent().setProgressBarIndeterminateVisibility(true); loadingCounter++; final ImageView imageView = new ImageView(this.context); String imageUrl = this.images.get(position).getUrl(); Log.d("DisplayPhotoActivity", "getView(): " + imageUrl); // Here is almost the same code as in the OnClickListener // above, but this does not work as expected. If the image // has never been cached (neither as a SoftReference nor as // a file on the SD card) the image is downloaded and shown. // No problem... But if the image is cached, it won't show up! Bitmap bitmap = asyncImageLoader.loadBitmap( imageUrl, new AsyncImageLoader.IImageLoadListener() { public void imageLoaded(Bitmap imageBitmap, String imageUrl) { Log.d("DisplayPhotoActivity", " - imageLoaded:"); if (imageBitmap != null) { Log.d("DisplayPhotoActivity", " - imageBitmap != null"); updateInitialImage(imageBitmap); Log.d("DisplayPhotoActivity", " - updateInitialImage(imageBitmap)"); imageView.setImageBitmap(imageBitmap); Log.d("DisplayPhotoActivity", " - setImageBitmap(imageBitmap)"); int width = Math.round((150 * imageBitmap .getWidth()) / imageBitmap.getHeight()); imageView .setLayoutParams(new Gallery.LayoutParams( width, 150)); //imageView.invalidate(); //gallery.invalidate(); } loadingCounter--; if (loadingCounter == 0) { getParent() .setProgressBarIndeterminateVisibility( false); } } }, 300, 300); if (bitmap != null) { // TODO: Something doesn't work here... Log.d("DisplayPhotoActivity", "Image was cached"); Log.d("DisplayPhotoActivity", bitmap.getWidth() + "x" + bitmap.getHeight()); updateInitialImage(bitmap); imageView.setImageBitmap(bitmap); int width = Math.round((150 * bitmap.getWidth()) / bitmap.getHeight()); imageView.setLayoutParams(new Gallery.LayoutParams(width, 150)); imageView.invalidate(); gallery.invalidate(); loadingCounter--; if (loadingCounter == 0) { getParent().setProgressBarIndeterminateVisibility(false); } } imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); imageView.setBackgroundResource(galleryItemBackground); imageView.setImageDrawable(getResources().getDrawable( R.drawable.loading)); imageView.setLayoutParams(new Gallery.LayoutParams(200, 150)); return imageView; } public float getScale(boolean focused, int offset) { return Math.max(0, 1.0f / (float) Math.pow(2, Math.abs(offset))); } } } Here is the DDMS output: 07-25 02:34:58.496: DEBUG/DisplayPhotoActivity(23589): getView(): http://[...]/files/814efa535ed97cf44ea3dc3a1c15c3fb/1_1311540992715.jpg 07-25 02:34:58.496: DEBUG/AsyncImageLoader(23589): Need to load the Bitmap container 07-25 02:34:58.503: DEBUG/DisplayPhotoActivity(23589): getView(): http://[...]/files/814efa535ed97cf44ea3dc3a1c15c3fb/1_1311540992715.jpg 07-25 02:34:58.503: DEBUG/AsyncImageLoader(23589): Need to load the Bitmap container 07-25 02:35:01.031: DEBUG/AsyncImageLoader(23589): Created cache image. Renaming temporary file. 07-25 02:35:01.031: DEBUG/DisplayPhotoActivity(23589): - imageLoaded: 07-25 02:35:01.031: DEBUG/DisplayPhotoActivity(23589): - imageBitmap != null 07-25 02:35:01.031: DEBUG/DisplayPhotoActivity(23589): No initial image set. 07-25 02:35:01.031: DEBUG/DisplayPhotoActivity(23589): - updateInitialImage(imageBitmap) 07-25 02:35:01.031: DEBUG/DisplayPhotoActivity(23589): - setImageBitmap(imageBitmap) 07-25 02:35:01.597: DEBUG/AsyncImageLoader(23589): Using /mnt/sdcard/PDCollector/cache/4057d5bd1cd9ba7204371ec422ea884a.jpg 07-25 02:35:01.605: DEBUG/AsyncImageLoader(23589): Using /mnt/sdcard/PDCollector/cache/4057d5bd1cd9ba7204371ec422ea884a.jpg 07-25 02:35:01.800: DEBUG/AsyncImageLoader(23589): Using /mnt/sdcard/PDCollector/cache/4057d5bd1cd9ba7204371ec422ea884a.jpg 07-25 02:35:01.800: DEBUG/AsyncImageLoader(23589): Using /mnt/sdcard/PDCollector/cache/4057d5bd1cd9ba7204371ec422ea884a.jpg 07-25 02:35:01.804: DEBUG/DisplayPhotoActivity(23589): - imageLoaded: 07-25 02:35:01.804: DEBUG/DisplayPhotoActivity(23589): - imageBitmap != null 07-25 02:35:01.804: DEBUG/DisplayPhotoActivity(23589): No initial image set. 07-25 02:35:01.804: DEBUG/DisplayPhotoActivity(23589): - updateInitialImage(imageBitmap) 07-25 02:35:01.804: DEBUG/DisplayPhotoActivity(23589): - setImageBitmap(imageBitmap) 07-25 02:35:02.027: DEBUG/AsyncImageLoader(23589): Using /mnt/sdcard/PDCollector/cache/4057d5bd1cd9ba7204371ec422ea884a.jpg 07-25 02:35:02.027: DEBUG/AsyncImageLoader(23589): Using /mnt/sdcard/PDCollector/cache/4057d5bd1cd9ba7204371ec422ea884a.jpg 07-25 02:35:02.082: DEBUG/DisplayPhotoActivity(23589): - imageLoaded: 07-25 02:35:02.082: DEBUG/DisplayPhotoActivity(23589): - imageBitmap != null 07-25 02:35:02.082: DEBUG/DisplayPhotoActivity(23589): - updateInitialImage(imageBitmap) 07-25 02:35:02.082: DEBUG/DisplayPhotoActivity(23589): - setImageBitmap(imageBitmap) 07-25 02:35:02.082: DEBUG/DisplayPhotoActivity(23589): getView(): http://[...]/files/814efa535ed97cf44ea3dc3a1c15c3fb/1_1311540992715.jpg 07-25 02:35:02.082: DEBUG/AsyncImageLoader(23589): Using a previously loaded Bitmap container 07-25 02:35:02.082: DEBUG/DisplayPhotoActivity(23589): Image was cached 07-25 02:35:02.082: DEBUG/DisplayPhotoActivity(23589): 640x480 07-25 02:35:02.257: DEBUG/DisplayPhotoActivity(23589): - imageLoaded: 07-25 02:35:02.257: DEBUG/DisplayPhotoActivity(23589): - imageBitmap != null 07-25 02:35:02.257: DEBUG/DisplayPhotoActivity(23589): - updateInitialImage(imageBitmap) 07-25 02:35:02.257: DEBUG/DisplayPhotoActivity(23589): - setImageBitmap(imageBitmap) I hope someone can explain this strange behavior. Thanks in advance.