woshidan's blog

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

後からコード上でBitmapを与える場合、ImageViewのadjustViewBounds=trueの指定が動かない

結論

onMeasureで一部人間にはよく分からない動きをしている気がします。

  • ImageViewのandroid:adjustViewBounds属性がtrueで、ImageViewの幅などを直接指定していない場合
    • ImageViewを許される限り拡大した場合の高さと幅を求める(widthSize/heightSize)
    • 上記の高さ or 幅を適用したとして、他辺の長さを求める(newWidth/newHeight= widthSize * 縦横比の場合 / heightSize / 縦横比)*1
    • 下記の最小値 >=newWidth/newHeightの場合、ImageViewのリサイズが行われる
      • DrawableのサイズとPaddingの合計値(in pixel)
      • 親Viewから許容されている値(in pixel)
      • android:minHeightあるいはandroid:minWidthで指定している値(in pixel)
    • 上記の最小値 < newWidth/newHeightの場合、ImageViewのリサイズが行われない
    • 現状のImageViewとDrawableのアスペクト比がほぼ同じ場合もonMeasureでリサイズしない

コード読み

主に関係あるのはImageViewのonMeasureメソッドなのでこれを読んでいきます。

また、下記のケースは親Viewから具体的にImageViewの大きさが制限されていない*2場合のケースとなります。

android:adjustViewBounds="true"の場合、まずDrawableのサイズを取得します。

            w = mDrawableWidth;
            h = mDrawableHeight;
            if (w <= 0) w = 1;
            if (h <= 0) h = 1;

            // We are supposed to adjust view bounds to match the aspect
            // ratio of our drawable. See if that is possible.
            if (mAdjustViewBounds) {
                resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;
                resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;
                
                desiredAspect = (float) w / (float) h;
            }

そのあとにPaddingの値を取得します。

        int pleft = mPaddingLeft;
        int pright = mPaddingRight;
        int ptop = mPaddingTop;
        int pbottom = mPaddingBottom;

そして、現在の条件でImageViewを拡大していい最大サイズの計算をします。

        if (resizeWidth || resizeHeight) {
            /* If we get here, it means we want to resize to match the
                drawables aspect ratio, and we have the freedom to change at
                least one dimension. 
            */

            // Get the max possible width given our constraints
            widthSize = resolveAdjustedSize(w + pleft + pright, mMaxWidth, widthMeasureSpec);

            // Get the max possible height given our constraints
            heightSize = resolveAdjustedSize(h + ptop + pbottom, mMaxHeight, heightMeasureSpec);

次に、縦か横をこのどちらかの大きさにあわせた場合のもう一辺の長さを計算します。

// 横方向にリサイズしていい場合は、まず縦の長さを目一杯撮った場合の横幅(newWidth)の計算をします
                    // Try adjusting width to be proportional to height
                    if (resizeWidth) {
                        int newWidth = (int)(desiredAspect * (heightSize - ptop - pbottom)) +
                                pleft + pright;

                        // Allow the width to outgrow its original estimate if height is fixed.
                        if (!resizeHeight && !mAdjustViewBoundsCompat) {
                            widthSize = resolveAdjustedSize(newWidth, mMaxWidth, widthMeasureSpec);
                        }

新しく計算した長さが、先ほど計算したImageViewを拡大していい最大サイズ以下であればリサイズをします。

if (newWidth <= widthSize) {
    widthSize = newWidth;
    done = true;
} 

横方向にリサイズできなかった場合は、縦方向のリサイズを試みます。

resolveAdjustedSizeの中身ですが、MeasureSpecの値が、MeasureSpec. AT_MOSTの場合、

  • DrawableのサイズとPaddingの合計値(in pixel)
  • 親Viewから許容されている値(in pixel)
  • android:minHeightあるいはandroid:minWidthで指定している値(in pixel)

の最小値となっています。

    private int resolveAdjustedSize(int desiredSize, int maxSize,
                                   int measureSpec) {
        int result = desiredSize;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize =  MeasureSpec.getSize(measureSpec);
        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                /* Parent says we can be as big as we want. Just don't be larger
                   than max size imposed on ourselves.
                */
                result = Math.min(desiredSize, maxSize);
                break;
            case MeasureSpec.AT_MOST:
                // Parent says we can be as big as we want, up to specSize. 
                // Don't be larger than specSize, and don't be larger than 
                // the max size imposed on ourselves.
                result = Math.min(Math.min(desiredSize, specSize), maxSize);
                break;
            case MeasureSpec.EXACTLY:
                // No choice. Do what we are told.
                result = specSize;
                break;
        }
        return result;
    }

現場からは以上です。

*1:少し大雑把なので、正確にはこの辺 https://github.com/android/platform_frameworks_base/blob/marshmallow-release/core/java/android/widget/ImageView.java#L1010

*2:親Viewのandroid:layout_widthがwrap_contentなどで、MeasureSpec.getModeでMeasureSpec.AT_MOSTが返ってくる場合