4
\$\begingroup\$

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?

\$\endgroup\$
2
  • \$\begingroup\$ If you do view.setBackgroundResource(R.drawable.huge_bitmap); in the Activity's onResume method, doesn't Android take care of the scaling itself? \$\endgroup\$ Commented Jan 26, 2015 at 18:13
  • \$\begingroup\$ @SimonAndréForsberg It does. However, it loads a full image into memory (without subsampling). As written in the post, assume this throws an OutOfMemoryError. \$\endgroup\$
    – Tadej
    Commented Jan 27, 2015 at 10:15

1 Answer 1

2
\$\begingroup\$

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);
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.