【ImageView】ScaleTypeを変更したときの内部処理

[ScaleTypeと表示画像の対応を整理する](https://qiita.com/tkhs0604/items/76bd3c979ae3c0c1570c)ために作成した[サンプルアプリ](https://github.com/tkhs0604/ImageViewScaleTypeChecker)で、`ScaleType`を`FIT_START`→`FIT_XY`→`MATRIX`と切り替えたときに`FIT_START`と`MATRIX`のときの画像表示が同じになる現象が発生したので、備忘録を兼ねてその際に調査した内容をまとめました。

# ScaleTypeを変更したときの内部処理
ImageView.javaのソースコードは[こちら](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget/ImageView.java)にあります。

まず、`ScaleType`を切り替えるために`setScaleType()`を呼び出します。
引数に指定された`ScaleType`が`mScaleType`に代入されます。

```java:ImageView.java#LL.827-838
public void setScaleType(ScaleType scaleType) {
  if (scaleType == null) {
    throw new NullPointerException();
  }

  if (mScaleType != scaleType) {
    mScaleType = scaleType;

    requestLayout();
    invalidate();
  }
}
```
　　
この`mScaleType`は`configureBounds()`内で分岐条件として使われます。
以下、今回の調査に必要な条件のみを抜粋します。

① `mScaleType == MATRIX`のとき、以下のような処理が行われます。

```java:ImageView#LL.1253-1257
if (mMatrix.isIdentity()) {
  mDrawMatrix = null;
} else {
  mDrawMatrix = mMatrix;
}
```

`mMatrix`は`Matrix`の参照を持つ変数です。`mDrawMatrix`は`mMatrix`の参照を代入するための変数です。この２つの変数によって`Matrix`による画像変換を行うか否かを制御しています。
`mMatrix`が単位行列(変換前後で画像の状態を変化させない`Matrix`)の場合、`mDrawMatrix`にnullを代入しています。

② `mScaleType == FIT_CENTER、FIT_END、FIT_START`のとき、以下のような処理が行われます。

```java:ImageView#LL.1302-1306
mTempSrc.set(0, 0, dwidth, dheight);
mTempDst.set(0, 0, vwidth, vheight);

mDrawMatrix = mMatrix;
mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType));
```

ネイティブコードを呼び出していたので詳細は確認できなかったのですが、最後の`setRectToRect()`によって`mScaleType`に対応した`Matrix`を計算しているようです。
この処理によって**`mDrawMatrix`の参照先の内容が更新される**ことが[Evaluate Expression](https://developer.android.com/studio/debug/#breakPoints)でトレースすることによって分かりました。

③ `mScaleType == FIT_XY`のとき、以下のような処理が行われます。
`vwidth`、`vheight`はそれぞれImageViewの幅と高さです。

```java:ImageView.java#LL.1244-1245
mDrawable.setBounds(0, 0, vwidth, vheight);
mDrawMatrix = null;
```
　　
そして、`mDrawMatrix`は`onDraw()`内で分岐条件として使われています。
`mDrawMatrix`がnullでないときのみ`Matrix`による画像変換が行われます。

```java:ImageView.java#LL.1350-1383
@Override
protected void onDraw(Canvas canvas) {
  ...
  if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
    mDrawable.draw(canvas);
  } else {
    ...
    if (mDrawMatrix != null) {
      canvas.concat(mDrawMatrix);
    }
    mDrawable.draw(canvas);
    canvas.restoreToCount(saveCount);
  }
}
```
　　
以上を踏まえ、改めて状況を整理します。

`ScaleType`を`FIT_START`→`FIT_XY`→`MATRIX`と切り替えたとき、

- `FIT_START`
  - ②の処理が行われるので、`mMatrix`の参照先の`Matrix`の内容は`FIT_START`に対応した値になる
  - `mDrawMatrix`はnullでないので、`Matrix`による画像変換が行われる
- `FIT_XY`
  - ③の処理が行われるので、`mMatrix`の参照先の`Matrix`の内容は**`FIT_START`に対応した値のままになる**  
  - `mDrawMatrix`はnullなので、`Matrix`による画像変換は行われない
- `MATRIX`
  - ①の処理が行われるので、`mMatrix`の参照先の`Matrix`の内容は**`FIT_START`に対応した値のままになる**
  - `mDrawMatrix`はnullでないので、**`Matrix`による画像変換が行われる**

となるため、`FIT_START`と`MATRIX`のときの画像表示が同じになっていました。
  
最後に、この現象を回避するために`ScaleType`を変更する前に`setImageMatrix()`を呼び出します。

```java:ImageView.java#LL.871-884
public void setImageMatrix(Matrix matrix) {
  // collapse null and identity to just null
  if (matrix != null && matrix.isIdentity()) {
    matrix = null;
  }

  // don't invalidate unless we're actually changing our matrix
  if (matrix == null && !mMatrix.isIdentity() || matrix != null && !mMatrix.equals(matrix)) {
    mMatrix.set(matrix);
    configureBounds();
    invalidate();
  }
}
```

`setImageMatrix()`の引数に新しい単位行列を代入することで`mMatrix`が初期化されます。