Interpretation of how ImageView's wrap_content and adjustViewBounds work

Android Advanced Road Series: http://blog.csdn.net/column/details/16488.html

ImageView is a component that is often used in the android development process. Due to the fragmentation of the android screen, sometimes we cannot set a specific width and height. For example, the width is match_parent. At this time, we also want the image to be completely filled in the width and display normally. We directly think of setting the height to wrap_content. However, students who have used it know that the actual area of ​​ImageView is larger than the image area, as shown in the figure: You can see that there is a large blank space above and below the image. Some people may want to set fitXY to solve this problem. The result is that the ImageView area is unchanged, but the image is deformed, as shown in the figure: The above situation occurs when the actual size of the image is larger than the screen size (or the size set for the ImageView). So if the picture is smaller, will the situation improve? We also observe the situation before and after setting fitXY, the pictures are as follows:







 
It can be seen that when the picture is relatively small, there will be blanks left and right, and after setting fitXY, the ImageView area remains unchanged, so the picture is deformed.

So why can't the area of ​​ImageView with wrap_content set to fit the size of the image content?
We know that View's onMeasure function calculates the size of a view, so let's take a look at ImageView's onMeasure function. The code is as follows:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    resolveUri();
    int w;
    int h;


    // Desired aspect ratio of the view's contents (not including padding)
    float desiredAspect = 0.0f;


    // We are allowed to change the view's width
    boolean resizeWidth = false;


    // We are allowed to change the view's height
    boolean resizeHeight = false;


    final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);


    if (mDrawable == null) {
        // If no drawable, its intrinsic size is 0.
        mDrawableWidth = -1;
        mDrawableHeight = -1;
        w = h = 0;
    } else {
        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;
        }
    }


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


    int widthSize;
    int heightSize;


    if (resizeWidth || resizeHeight) {


        // 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);


        if (desiredAspect != 0.0f) {
            // See what our actual aspect ratio is
            final float actualAspect = (float)(widthSize - pleft - pright) /
                                    (heightSize - ptop - pbottom);


            if (Math.abs(actualAspect - desiredAspect) > 0.0000001) {


                boolean done = false;


                // 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 && !sCompatAdjustViewBounds) {
                        widthSize = resolveAdjustedSize(newWidth, mMaxWidth, widthMeasureSpec);
                    }


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


                // Try adjusting height to be proportional to width
                if (!done && resizeHeight) {
                    int newHeight = (int)((widthSize - pleft - pright) / desiredAspect) +
                            ptop + pbottom;


                    // Allow the height to outgrow its original estimate if width is fixed.
                    if (!resizeWidth && !sCompatAdjustViewBounds) {
                        heightSize = resolveAdjustedSize(newHeight, mMaxHeight,
                                heightMeasureSpec);
                    }


                    if (newHeight <= heightSize) {
                        heightSize = newHeight;
                    }
                }
            }
        }
    } else {
        ...


        widthSize = resolveSizeAndState(w, widthMeasureSpec, 0);
        heightSize = resolveSizeAndState(h, heightMeasureSpec, 0);
    }


    setMeasuredDimension(widthSize, heightSize);
}

Let's look at it step by step. First, let's see that this function calls the resolveUri function at the beginning. The code of this function is as follows:
private void resolveUri() {
    ...
    if (mResource != 0) {
        try {
            d = mContext.getDrawable(mResource);
        } catch (Exception e) {
            ...
        }
    } else if (mUri != null) {
        d = getDrawableFromUri (mUri);
        ...
    } else {
        return;
    }


    updateDrawable(d);
}


private void updateDrawable(Drawable d) {
    ...
    if (d != null) {
        ...
        mDrawableWidth = d.getIntrinsicWidth();
        mDrawableHeight = d.getIntrinsicHeight ();
        ...
    } else {
        mDrawableWidth = mDrawableHeight = -1;
    }
}

通过上面代码可以看到resolveUri函数会先得到drawable对象(从resource或uri中),然后通过updateDrawable将
mDrawableWidth和mDrawableHeight这两个变量设置为drawable的宽高。

我们回到onMeasure函数继续往下看,第一个if-else,因为我们讨论的是有图片的情况,所以mDrawable一定不为null,那么走进了else语句,
将刚才的mDrawableWidth和mDrawableHeight两个变量的值赋给了w和h这两个局部变量。

同时这里如果mAdjustViewBounds为ture,则改变resizeWidth和resizeHeight。而他们的默认值是false。这里我们先讨论mAdjustViewBounds为false的情况。

再继续,第二个if-else,判断是否resize,由于mAdjustViewBounds为false,所以resizeWidth和resizeHeight都为false,走进else语句块。
在else中则用resolveSizeAndState这个函数来计算宽高,代码如下:
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}

首先从measureSpec中获取mode和size。

这里简单解释一下measureSpec,它是一个int值,前两位(32和31位)存储标志位,即specMode;后面则存储着size即限制大小。而measureSpec是父View传给子View的,也就是说父View会根据自身的情况限制子View的大小。
这里还涉及到specMode的三种模式:
①UNSPECIFIED:父View没有对子View施加任何约束。它可以是任何它想要的大小。 
②EXACTLY:父View已经确定了子View的确切尺寸。子View将被限制在给定的界限内,而忽略其本身的大小。 
③AT_MOST:子View的大小不能超过指定的大小
这部分也值得一说,以后专门开一篇新文章来仔细讲讲。


基于上面的解释,我们回头在来看resolveAdjustedSize的代码就比较好理解了。
在从measureSpec中的到了mode和size后,根据mode不同会有不同的处理。
这里我们有一个隐藏的前提暴露出来了,就是ImageView不能超出它的父view的显示区域。即mode只能为EXACTLY或AT_MOST。因为view的宽度是match_parent,所以mode是EXACTLY,直接是父view的宽度,我们就不再考虑了。
那么重点来看高度,因为是wrap_content,所以mode应该是AT_MOST,则最终的高度是desiredSize、specSize和maxSize的最小值,desiredSize是前面获取的图片的高度,specSize是父view限制的大小。而最终高度则取他们两个的最小值。

这样我们就有一个结论,在我们设定的前提下,ImageView的宽度是父View的限制宽度,而高度是图片高度与父View限制高度的较小值。两者并无关联,所以并不会按照图片的比例计算自己的宽高。所以在这种情况下,wrap_content无法达到让ImageView按图片的比例显示,这样就会出现文章开头的情况。

但是我们知道,当我们为ImageView设置了
android:adjustViewBounds="true"
ImageView就可以按照图片的比例来显示了。

这是怎么实现的?
接下来我们回过头看看之前的mAdjustViewBounds,我们上面讨论的是它为false的情况。当我们设置了adjustViewBounds,它就为ture了,这时就执行if语句中的代码:
resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;
resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;
desiredAspect = (float) w / (float) h;
当ImageView的宽高没有都是设置为固定值或match_parent时,resizeWidth和resizeHeight一定有一个为ture。
而desiredAspect则是宽高比。

继续看onMeasure中接下来的代码,在第二个if-else时,由于resizeWidth和resizeHeight一定有一个为ture,所以走进if语句块。
首先调用了resolveAdjustedSize这个函数来计算宽高。我们先来看看resolveAdjustedSize的代码:
private int resolveAdjustedSize(int desiredSize, int maxSize,
                               int measureSpec) {
    int result = desiredSize;
    final int specMode = MeasureSpec.getMode(measureSpec);
    final 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;
}

与上面resolveSizeAndState方法很类似,当mode为AT_MOST时,
result = Math.min(Math.min(desiredSize, specSize), maxSize);
是取图片size、specSize和maxSize的最小值。

到目前为止没什么不同,下面才是重点,继续看onMeasure下面的代码:
final float actualAspect = (float)(widthSize - pleft - pright) /
                        (heightSize - ptop - pbottom);
if (Math.abs(actualAspect - desiredAspect) > 0.0000001) {
    boolean done = false;


    // 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 && !sCompatAdjustViewBounds) {
            widthSize = resolveAdjustedSize(newWidth, mMaxWidth, widthMeasureSpec);
        }


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


    // Try adjusting height to be proportional to width
    if (!done && resizeHeight) {
        int newHeight = (int)((widthSize - pleft - pright) / desiredAspect) +
                ptop + pbottom;


        // Allow the height to outgrow its original estimate if width is fixed.
        if (!resizeWidth && !sCompatAdjustViewBounds) {
            heightSize = resolveAdjustedSize(newHeight, mMaxHeight,
                    heightMeasureSpec);
        }


        if (newHeight <= heightSize) {
            heightSize = newHeight;
        }
    }
}

当计算后的宽高比与图片宽高比不同时,会根据之前resizeWidth和resizeHeight,用固定的那个值和图片宽高比取计算另外一个值。
这样ImageView的宽高比例就完全符合了图片的实际宽高比,不会出现文章前面的留白的情况了。

本文讨论的是ImageView保持固定的宽高比,那么其他组件也可以么?
有几种方法可实现的组件的固定宽高比,具体请期待下一篇文章。

Android进阶之路系列: http://blog.csdn.net/column/details/16488.html

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324512941&siteId=291194637