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);
}
}