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が出てくるのは下記です。
// 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がどこで使われているかというと、主にImageView
のonDraw
メソッド内部で実行される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
// 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だったらやめます)
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;