Written on
To Scale or not to Scale
As this is yet another day I can't remember the various differences and meanings of the android:scaleType attribute values, I decided to have a closer look at the scaling mechanisms provided by ImageView.The ImageView died that day
Android's ImageView class is used to draw arbitrary Drawable objects [0]. For the purpose of better understanding its internals, let us assume this class was not part of the Android framework and we would have to introduce a similar class for our project. How should the implementation look like?The DrawableView
Let's name our class DrawableView as it is used solely to show object instances of type Drawable and extend it from View as this is the case with every other UI widget.
public class DrawableView extends View {
public DrawableView(Context context) {
super(context);
}
public DrawableView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public DrawableView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
}
<resources>
<declare-styleable name="com.example.DrawableView">
<attr name="src" format="reference"/>
</declare-styleable>
</resources>
// ...
private Drawable drawable;
public DrawableView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.com_example_DrawableView, defStyle, 0);
drawable = typedArray.getDrawable(R.styleable.com_example_DrawableView_src);
typedArray.recycle();
}
// ...
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:example="http://schemas.android.com/apk/res/com.example"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<com.example.DrawableView
android:id="@+id/header"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
example:src="@drawable/header"/>
</LinearLayout>
Case 1 - View > Image
If the available view space is larger than the image, we want a way to specify that the image should be centered horizontally and vertically within the available space - this is exactly the case with ImageView.ScaleType.CENTER. The image is centered without scaling. To make our DrawableView work we will have to implement onMeasure first.
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = drawable.getIntrinsicWidth();
int height = drawable.getIntrinsicHeight();
width = Math.max(width, getSuggestedMinimumWidth());
height = Math.max(height, getSuggestedMinimumHeight());
setMeasuredDimension(resolveSize(width, widthMeasureSpec), resolveSize(height, heightMeasureSpec));
}
private Drawable drawable;
private Matrix matrix = new Matrix();
private int drawableWidth, drawableHeight;
private int viewWidth, viewHeight;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
drawableWidth = drawable.getIntrinsicWidth();
drawableHeight = drawable.getIntrinsicHeight();
int width = Math.max(drawableWidth, getSuggestedMinimumWidth());
int height = Math.max(drawableHeight, getSuggestedMinimumHeight());
viewWidth = resolveSize(width, widthMeasureSpec);
viewHeight = resolveSize(height, heightMeasureSpec);
setMeasuredDimension(viewWidth, viewHeight);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int saveCount = canvas.save();
// this will center the small header image
matrix.setTranslate((int) ((viewWidth - drawableWidth) * 0.5f + 0.5f), (int) ((viewHeight - drawableHeight) * 0.5f + 0.5f));
canvas.concat(matrix);
drawable.setBounds(0, 0, drawableWidth, drawableHeight);
drawable.draw(canvas);
canvas.restoreToCount(saveCount);
}

Case 2 - View < Drawable
Let us assume we replace the small header image with a large one, say 800 x 200 px. With a screen height of 480 px the height of the image would fit, but the width of the image is too large. What to do in that case? One approach would be to still keep the image ratio, but scale the image so that at least one axis fits for the view and center the result - which leads to cropping the left/right and/or upper/lower part of the image. That means for our 800 x 200 px image, that the full height is drawn (assuming the screen height has 480 px), but the image width is cropped by 480 pixels, 240 left and 240 right.
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int saveCount = canvas.save();
float scale;
float dx = 0, dy = 0;
if (drawableWidth * viewHeight > viewWidth * drawableHeight) {
scale = (float) viewHeight / (float) drawableHeight;
dx = (viewWidth - drawableWidth * scale) * 0.5f;
} else {
scale = (float) viewWidth / (float) drawableWidth;
dy = (viewHeight - drawableHeight * scale) * 0.5f;
}
matrix.setScale(scale, scale);
matrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));
canvas.concat(matrix);
drawable.setBounds(0, 0, drawableWidth, drawableHeight);
drawable.draw(canvas);
canvas.restoreToCount(saveCount);
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int saveCount = canvas.save();
float scale;
float dx;
float dy;
if (drawableWidth <= viewWidth && drawableHeight <= viewHeight) {
scale = 1.0f;
} else {
scale = Math.min((float) viewWidth / (float) drawableWidth,
(float) viewHeight / (float) drawableHeight);
}
dx = (int) ((viewWidth - drawableWidth * scale) * 0.5f + 0.5f);
dy = (int) ((viewHeight - drawableHeight * scale) * 0.5f + 0.5f);
matrix.setScale(scale, scale);
matrix.postTranslate(dx, dy);
canvas.concat(matrix);
drawable.setBounds(0, 0, drawableWidth, drawableHeight);
drawable.draw(canvas);
canvas.restoreToCount(saveCount);
}

<ImageView
android:id="@+id/header"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:scaleType="centerInside"
android:adjustViewBounds="true"
android:src="@drawable/header"/>

What about the other ScaleType configuration values?
What we did not see so far is the implementation of ImageView.ScaleType.FIT_* attributes and ImageView.ScaleType.MATRIX. The ImageView.ScaleType.MATRIX attribute can be set to use a custom matrix when drawing the image. With ImageView.setImageMatrix arbitrary operations can be executed during drawing the drawable. All FIT_* scale types actually use the Matrix.setRectToRect method, which allows to translate between two given rectangular bounds.
Rect src = new Rect(0, 0, drawableWidth, drawableHeight);
Rect dst = new Rect(0, 0, viewWidth, viewHeight);
matrix.setRectToRect(src, dst, scaleType);