woshidan's blog

そんなことよりコードにダイブ。

AndroidのImageViewのscaleTypeについて(コード読んだメモ編)

前の記事の検証する前に、いまいち動作がはっきりしないところがあったのでコード読んでいたときのメモです。

力尽きたので雑なのですが。。

どこでscaleTypeの設定を適用しているかを調べるために、 https://github.com/android/platform_frameworks_base/blob/marshmallow-release/core/java/android/widget/ImageView.java をscaleTypeなどで検索するところから始めました。

読んでいるソースコードがMarshmallowのものである理由は、1/20時点でダッシュボードにて確認したら一番シェアが高かったためです。

目次

  • 関連しそうなソースコード
  • configureBounds()内で処理されるscaleTypeについて
  • mDrawMatrixがどこで使われるかについて
  • scaleTypeごとのコードリーディング
    • scaleType: CENTER
    • scaleType: CENTER_CROP
    • scaleType: CENTER_INSIDE
    • scaleType: FIT_XY
    • scaleType: MATRIX
    • FIT_CENTER, FIT_END, FIT_START
    • ScaleTypeがMATRIXの場合以外、サイズが合っていたら何もしない

関連しそうなソースコード

さて、初期化以外でscaleTypeが出てくるのは下記です。

https://github.com/android/platform_frameworks_base/blob/marshmallow-release/core/java/android/widget/ImageView.java#L1093-L1176

// ImageView.java
    private void configureBounds() {
        if (mDrawable == null || !mHaveFrame) {
            return;
        }

        int dwidth = mDrawableWidth;
        int dheight = mDrawableHeight;

        int vwidth = getWidth() - mPaddingLeft - mPaddingRight;
        int vheight = getHeight() - mPaddingTop - mPaddingBottom;

        boolean fits = (dwidth < 0 || vwidth == dwidth) &&
                       (dheight < 0 || vheight == dheight);

        if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) {
            /* If the drawable has no intrinsic size, or we're told to
                scaletofit, then we just fill our entire view.
            */
            mDrawable.setBounds(0, 0, vwidth, vheight);
            mDrawMatrix = null;
        } else {
            // We need to do the scaling ourself, so have the drawable
            // use its native size.
            mDrawable.setBounds(0, 0, dwidth, dheight);

            if (ScaleType.MATRIX == mScaleType) {
                // Use the specified matrix as-is.
                if (mMatrix.isIdentity()) {
                    mDrawMatrix = null;
                } else {
                    mDrawMatrix = mMatrix;
                }
            } else if (fits) {
                // The bitmap fits exactly, no transform needed.
                mDrawMatrix = null;
            } else if (ScaleType.CENTER == mScaleType) {
                // Center bitmap in view, no scaling.
                mDrawMatrix = mMatrix;
                mDrawMatrix.setTranslate(Math.round((vwidth - dwidth) * 0.5f),
                                         Math.round((vheight - dheight) * 0.5f));
            } else if (ScaleType.CENTER_CROP == mScaleType) {
                mDrawMatrix = mMatrix;

                float scale;
                float dx = 0, dy = 0;

                if (dwidth * vheight > vwidth * dheight) {
                    scale = (float) vheight / (float) dheight; 
                    dx = (vwidth - dwidth * scale) * 0.5f;
                } else {
                    scale = (float) vwidth / (float) dwidth;
                    dy = (vheight - dheight * scale) * 0.5f;
                }

                mDrawMatrix.setScale(scale, scale);
                mDrawMatrix.postTranslate(Math.round(dx), Math.round(dy));
            } else if (ScaleType.CENTER_INSIDE == mScaleType) {
                mDrawMatrix = mMatrix;
                float scale;
                float dx;
                float dy;
                
                if (dwidth <= vwidth && dheight <= vheight) {
                    scale = 1.0f;
                } else {
                    scale = Math.min((float) vwidth / (float) dwidth,
                            (float) vheight / (float) dheight);
                }
                
                dx = Math.round((vwidth - dwidth * scale) * 0.5f);
                dy = Math.round((vheight - dheight * scale) * 0.5f);

                mDrawMatrix.setScale(scale, scale);
                mDrawMatrix.postTranslate(dx, dy);
            } else {
                // Generate the required transform.
                mTempSrc.set(0, 0, dwidth, dheight);
                mTempDst.set(0, 0, vwidth, vheight);
                
                mDrawMatrix = mMatrix;
                mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType));
            }
        }
    }

configureBounds()内で処理されるscaleTypeについて

全部のscaleTypeがこのメソッドの中の分岐に登場しているわけではなかったので、登場してるかなどざっくり分けると、

scaleType configureBounds()内での扱い
CENTER, CENTER_CROP, CENTER_INSIDE, MATRIX, FIT_XY (+ 画像サイズが取得できなかった場合) 処理あり
FIT_START, FIT_END, FIT_CENTER 個別に処理なし

となっていて、だいたいMatrix.ScaleToFitと同じ内容の処理はここに書いていない、という感じでした。

mDrawMatrixがどこで使われるかについて

ここから、各scaleTypeの処理について、さっきのconfiguraBounds()メソッドの分岐の中を覗いてみて、mDrawMatrixの値がどう設定されているかを見ていくことで、scaleTypeごとにどんな処理がされているかを見ていこうと思います。

といきなり書きましたが、mDrawMatrixの値がどういう風にImageViewの中のDrawableの見え方に影響するかちょっとわかりにくいですね。

このmDrawMatrixがどこで使われているかというと、主にImageViewonDrawメソッド内部で実行されるCanvas.concat(Matrix matrix)メソッドで、Drawableが持ってるCanvasインスタンスに対して拡大縮小などの処理をしています。(細かいところは拡大縮小じゃなくて画像のメモリ管理の話になるので適当にはしょってます)

ImageView onDraw 全体: https://github.com/android/platform_frameworks_base/blob/marshmallow-release/core/java/android/widget/ImageView.java#L1214-L1247

// 右下方向の余白のために切り抜く
if (mCropToPadding) {
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop,
            scrollX + mRight - mLeft - mPaddingRight,
            scrollY + mBottom - mTop - mPaddingBottom);
}
// 余白の分だけ左上からずらす
canvas.translate(mPaddingLeft, mPaddingTop);

// Matrixの設定があったら、上記の余白の適用前に、
// configureBoundsで設定したMatrixの設定を適用させる
if (mDrawMatrix != null) {
    canvas.concat(mDrawMatrix);
}
// DrawableにMatrixなどが適用されたCanvasの中身を書き込み
mDrawable.draw(canvas);

scaleTypeごとのコードリーディング

scaleType: CENTER

vwidth, vheight: ImageViewのwidth, heightからpadding引いた値(pixel), mDrawableWidth, mDrawableWidth(Drawableの初期化の際に設定されるDrawableの画像の元々の大きさ(pixel))

// Drawableの大きさを元の画像の大きさにセットしておく
mDrawable.setBounds(0, 0, dwidth, dheight);

...

// Center bitmap in view, no scaling.
mDrawMatrix = mMatrix;
mDrawMatrix.setTranslate(Math.round((vwidth - dwidth) * 0.5f),
                         Math.round((vheight - dheight) * 0.5f));

https://developer.android.com/reference/android/graphics/Matrix.html#setTranslate(float, float)

Matrix.setTranslate Set the matrix to translate by (dx, dy).とあるので、

説明文から見ても、Drawbleの画像の大きさを変えずに画像を中央に移動させています。

scaleType: CENTER_CROP

vwidth, vheight: ImageViewのwidth, heightからpadding引いた値(pixel), mDrawableWidth, mDrawableWidth(Drawableの初期化の際に設定されるDrawableの画像の元々の大きさ(pixel))

mDrawable.setBounds(0, 0, dwidth, dheight);

mDrawMatrix = mMatrix;

float scale;
float dx = 0, dy = 0;

if (dwidth * vheight > vwidth * dheight) {
    scale = (float) vheight / (float) dheight; 
    dx = (vwidth - dwidth * scale) * 0.5f;
} else {
    scale = (float) vwidth / (float) dwidth;
    dy = (vheight - dheight * scale) * 0.5f;
}

mDrawMatrix.setScale(scale, scale);
mDrawMatrix.postTranslate(Math.round(dx), Math.round(dy));

dwidth * vheight > vwidth * dheightがよくわからないんですけど、 d -> Orig(元画像), v -> Space(画像が入るスペース)と一旦添え字を変えて、

OrigWidth / OrigHeight > SpaceWidth / SpaceHeightと書き換えると、 アスペクト比を見て、元画像の方が画像が入るスペースより横長の場合は、となるので、

if (元画像の方が画像が入るスペースよりアスペクト比が横長の場合) {
    scale = (float) vheight / (float) dheight; 
    dx = (vwidth - dwidth * scale) * 0.5f;
} else {
  scale = (float) vwidth / (float) dwidth;
    dy = (vheight - dheight * scale) * 0.5f;
}

となって、scaleについては、元画像の方が画像が入るスペースよりアスペクト比が横長の場合(cf. 縦長のスペースに横長の画像を突っ込んだとか)は、高さを合わせるようにscaleを求めて、その大きさに拡大した画像が横方向に中心に来るように移動する距離(dx)を求めています。

一言で言うと、画像側の短い方の辺の長さを画像が入るスペースに揃えるように拡大縮小したら、はみ出した分を切り取る(ようにcanvas上で画像の位置を移動させている)となります。

scaleType: CENTER_INSIDE

vwidth, vheight: ImageViewのwidth, heightからpadding引いた値(pixel), mDrawableWidth, mDrawableWidth(Drawableの初期化の際に設定されるDrawableの画像の元々の大きさ(pixel))

mDrawable.setBounds(0, 0, dwidth, dheight);

mDrawMatrix = mMatrix;
float scale;
float dx;
float dy;

if (dwidth <= vwidth && dheight <= vheight) {
// 画像の縦横が、両方画像を入れるスペースの縦横より小さかったら縮小しない
    scale = 1.0f;
} else {
// 画像の縦横が、画像入れるスペースの縦横と比べて大きい方に合わせて拡大縮小する
// cf. 縦長のスペースに横長の画像を入れるとしたら、画像の横幅が縦長のスペースに合うように拡大縮小する
    scale = Math.min((float) vwidth / (float) dwidth,
            (float) vheight / (float) dheight);
}

// このとき、なるべく画像がスペースの中心に来るように移動させる
dx = Math.round((vwidth - dwidth * scale) * 0.5f);
dy = Math.round((vheight - dheight * scale) * 0.5f);

mDrawMatrix.setScale(scale, scale);
mDrawMatrix.postTranslate(dx, dy);

scaleType: FIT_XY

vwidth, vheight: ImageViewのwidth, heightからpadding引いた値(pixel), mDrawableWidth, mDrawableWidth(Drawableの初期化の際に設定されるDrawableの画像の元々の大きさ(pixel))

余白分あけてスペースいっぱい引伸ばす。アスペクト比なんてなかった。元画像のサイズがうまく取得できなかった場合も余白分あけてスペースいっぱい引伸ばす。

if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) {
    /* If the drawable has no intrinsic size, or we're told to
        scaletofit, then we just fill our entire view.
    */
    mDrawable.setBounds(0, 0, vwidth, vheight);
    // onDrawで再度拡大縮小はしない
    mDrawMatrix = null;
}

scaleType: MATRIX

mDrawable.setBounds(0, 0, dwidth, dheight);

// Use the specified matrix as-is.
if (mMatrix.isIdentity()) {
// 単位行列が設定されている場合は、余白などの設定以外何も差せない
// (scaleも移動もしない)
    mDrawMatrix = null;
} else {
// 設定されているmatrixを適用
    mDrawMatrix = mMatrix;
}

FIT_CENTER, FIT_END, FIT_START

https://github.com/android/platform_frameworks_base/blob/marshmallow-release/core/java/android/widget/ImageView.java#L107-L109

// Avoid allocations...
private RectF mTempSrc = new RectF();
private RectF mTempDst = new RectF();

mDrawable.setBounds(0, 0, dwidth, dheight);

// Generate the required transform.
mTempSrc.set(0, 0, dwidth, dheight);
mTempDst.set(0, 0, vwidth, vheight);

mDrawMatrix = mMatrix;
// scaleTypeToScaleToFitでScaleTypeをMatrixの方のScaleTypeに変換して、
// Matrix.setRectToRectでそのScaleTypeの効果を適用させるようにしている
mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType));

ここにあるコードブロックではそうなんですが、適用されている効果が何なのかわからないのでもう少し追います。(Cだったらやめます)

https://github.com/android/platform_frameworks_base/blob/marshmallow-release/graphics/java/android/graphics/Matrix.java#L848

    private static native void native_setScale(long native_object,
                                        float sx, float sy, float px, float py);

Cだったので止めます。

ドキュメントから引用。

Type Value description
ImageView.ScaleType FIT_CENTER Scale the image using CENTER.
Matrix.ScaleToFit CENTER Compute a scale that will maintain the original src aspect ratio, but will also ensure that src fits entirely inside dst.
ImageView.ScaleType FIT_START Scale the image using START.
Matrix.ScaleToFit START Compute a scale that will maintain the original src aspect ratio, but will also ensure that src fits entirely inside dst.
ImageView.ScaleType FIT_END Scale the image using END.
Matrix.ScaleToFit END Compute a scale that will maintain the original src aspect ratio, but will also ensure that src fits entirely inside dst.

Matrix.ScaleToFit.CENTER, Matrix.ScaleToFit.START, Matrix.ScaleToFit.ENDの3つ、説明文が全く同じ気がしますが、

  • Matrix.ScaleToFit.CENTER 中央寄せ
  • Matrix.ScaleToFit.START 文頭寄せ
  • Matrix.ScaleToFit.END 文末寄せ

の違いがあります。詳しくはこちら

ScaleTypeがMATRIXの場合以外、サイズが合っていたら何もしない

        boolean fits = (dwidth < 0 || vwidth == dwidth) &&
                       (dheight < 0 || vheight == dheight);
       // 中略

            } else if (fits) {
                // The bitmap fits exactly, no transform needed.
                mDrawMatrix = null;