Loading Bitmaps Efficiently

Posted on

Problem

Suppose there is a huge bitmap, huge_bitmap, in the drawable-nodpi folder. I want to create a custom view with huge_bitmap as its background:

public class CustomView extends View {

    private Bitmap mBackground;

    public CustomView(Context context) {
        super(context);
    }

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mBackground = decodeAndResizeFromResource(getResources(), 
            R.drawable.huge_bitmap, getMeasuredWidth(), getMeasuredHeight());
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(mBackground, 0, 0, mPaint);
        super.onDraw(canvas);
    }

}

where decodeAndResizeFromResource method subsamples the image and loads a smaller, scaled version into memory.

This works fine. However, onMeasure will often be invoked more than once. Lint warns about this:

Avoid object allocations during draw/layout operations (preallocate and reuse instead)

Allocating objects in methods such as onMeasure, onLayout, onSizeChanged and onDraw should therefore be avoided.

How do I correctly fix the example above?

I obviously don’t want to preallocate the bitmap (assume it will throw an OutOfMemoryError). Since I need the view’s measured width and height in order to subsample/scale accordingly, I need to load the bitmap after measuring.

Here are two possible workarounds:

@Override
protected void onDraw(Canvas canvas) {
    if (mBackground == null) {
        mBackground = decodeAndResizeFromResource(getResources(), 
            R.drawable.huge_bitmap, getMeasuredWidth(), getMeasuredHeight());
    }
    canvas.drawBitmap(mBackground, 0, 0, mPaint);
    super.onDraw(canvas);
}

or maybe

private int mWidth, mHeight;

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    final int w = getMeasuredWidth();
    final int h = getMeasuredHeight();
    if (mWidth != w || mHeight != h) {
        mBackground = decodeAndResizeFromResource(getResources(), 
            R.drawable.huge_bitmap, w, h);
        mWidth = w;
        mHeight = h;
    }
}

but both feel clumsy. What would you recommend?

Solution

To achieve that the first thing for me would be to move your logic away from the view in a MVP way.

With that in place you could use a observer/observable pattern to wrap that logic in a different class that would be triggered when that event happen.

The problem with this is when your onMeasure is triggered before the addObserver is ran, this of course would end up with the decodeAndResizeFromResource() not running on that first call. A way to fix that, if it is an issue, could be using CustomPresenter constructor to make the values manually or use some default values and make that first call.

Something like this:

import java.util.Observable;
import java.util.Observer;

public class CustomPresenter extends View implements Observable {

    private Bitmap mBackground;
    private List observers = new ArrayList();

    public CustomPresenter (Context context) {
        super(context);
    }

    public CustomPresenter (Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomPresenter (Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        hasChanged();
        notifyObservers();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(mBackground, 0, 0, mPaint);
        super.onDraw(canvas);
    }

    public Map getHugeBitmap(){
        return this.drawable.huge_bitmap;
    }

    public void registerObserver(Observer observer) {
        observers.add(observer);
    }

    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    public void notifyObservers() {
        for (Observer ob : observers) {
            ob.update(this);
        }
    }
}

Then the “View”

    public class CustomResizerView implements Observer{
       public void update(Object obj){
           CustomPresenter view = (CustomPresenter) obj;            
           mBackground = decodeAndResizeFromResource(view.getResources(),
                   view.getHugeBitmap, view.getMeasuredWidth(), view.getMeasuredHeight());
       }
}

Then the test

public class Test {
    public static void main(String args[]) {
        CustomPresenter view = new CustomPresenter();
        CustomResizerView resizer = new CustomResizerView();
        view.registerObserver(resizer);
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *