解读ImageView的wrap_content和adjustViewBounds的工作原理

原创 2018年04月17日 10:59:27

ImageView是android开发过程中经常会使用的一种组件,由于android屏幕碎片化的问题,有时候我们无法设定一个具体的宽高。比如说width是match_parent的,这时候我们还想让图片在宽度完全填充并能正常显示,我们直接会想到将height设置为wrap_content。但是用过的同学都知道ImageView的实际区域要大于图片区域,如图:


可以看到在图片的上下留了很大的空白空间,有人可能想设置fitXY来解决这个问题。结果是ImageView区域未变,但是图片变形了,如图:


上面这种情况出现在图片实际尺寸要大于屏幕尺寸(或为ImageView设定的尺寸)。那如果图片比较小,情况会改善么?
我们同样观察设置fitXY前后的情况,图片如下:
 
可以看到当图片比较小的时候,会左右留出空白,而设置fitXY后则ImageView区域依然未改变,所以图片变形了。

1、wrap_content

那么设置了wrap_content的ImageView的区域为什么无法贴合图片内容的大小呢?
我们知道View的onMeasure函数是计算一个view的大小的,那么让我们来看看ImageView的onMeasure函数,代码如下:
@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);
}

我们一步步来看,首先看这个函数一开始调用了resolveUri这个函数,这个函数代码如下:
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按图片的比例显示,这样就会出现文章开头的情况。


2、adjustViewBounds

上面我们发现为ImageView设置了wrap_content无法达到效果。但是ImageView还有一个adjustViewBounds参数,当设置了这个参数,如下:
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的宽高比例就完全符合了图片的实际宽高比,不会出现文章前面的留白的情况了。


3、其他组件

本文讨论的是ImageView保持固定的宽高比,那么其他组件也可以么?
有几种方法可实现的组件的固定宽高比,具体请阅读《Android中如何使控件保持固定宽高比》
版权声明:本文为博主原创文章,转载请注明出处:http://blog.csdn.net/chzphoenix。 https://blog.csdn.net/chzphoenix/article/details/79971509

ImageView中android:adjustViewBounds属性

public voidsetAdjustViewBounds(boolean adjustViewBounds) Since: API Level 1 Set this to true...
  • xiahao86
  • xiahao86
  • 2013-11-20 13:52:18
  • 24474

ImageView.adjustViewBounds属性

取值为true时: Adjust the ImageView's bounds to preserve the aspect ration of its drawable. 调整ImageView...
  • Buaaroid
  • Buaaroid
  • 2015-10-23 15:14:25
  • 662

android ImageView android:adjustViewBounds属性的作用

android:adjustViewBounds  是否保持宽高比。需要与maxWidth、MaxHeight一起使用,否则单独使用没有效果。  android:cropToPadding  ...
  • xuewater
  • xuewater
  • 2014-01-17 16:19:27
  • 2023

Imageview.setAdjustViewBounds用法

public void setAdjustViewBounds (boolean adjustViewBounds) 当你需要在 ImageView调整边框时保持可绘制对象的比例时,将该值设为真。 ...
  • DQ1005
  • DQ1005
  • 2015-08-25 15:25:17
  • 3068

【Android】解决在RelativeLayout中使用ImageView, adjustViewBounds 无效

今天在布局时使用ImageView, 想要的效果是高度一定, 宽度随着高度的变化自动变化,保证ImageView和图片的宽高比一致,于是自然想到了adjustViewBounds属性。结果使用出来,没...
  • u013015161
  • u013015161
  • 2016-06-28 21:35:49
  • 2926

Android ImageView设置长度高度为wrap_content时高度根据图片比例自适应

import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; i...
  • go12355
  • go12355
  • 2014-10-20 16:04:31
  • 5155

Android ImageView的scaleType属性与adjustViewBounds属性

android:scaleType="center"  以原图的几何中心点和ImagView的几何中心点为基准,按图片的原来size居中显示,不缩放,当图片长/宽超过View的长/宽,则截取图片的居...
  • stephenzcl
  • stephenzcl
  • 2014-11-02 21:07:10
  • 7074

ImageView使用wrap_content时图片尺寸有缩放

这两天在做一个项目时,发现UI team给的图片无论我怎么设置layout,图片总是模糊的,有缩放,即使给imageView设置width和height为图片的原始尺寸大小也不行。 后来想想只可能是...
  • w_xue
  • w_xue
  • 2013-12-17 13:30:29
  • 2106

android中ImageView的adjustViewBounds属性的作用

在ImageView中有一个adjustViewBounds的属性,它是做什么用的,官方给出的解释的是是否保持原图的长宽比,单独设置不起作用,需要配合maxWidth或maxHeight一起使用。我们...
  • chenguang79
  • chenguang79
  • 2016-07-18 13:12:37
  • 1157

Android进阶UI之ImageView设置长度高度为wrap_content时高度根据图片比例自适应

1 示例
  • chenliguan
  • chenliguan
  • 2017-04-20 20:48:13
  • 417
收藏助手
不良信息举报
您举报文章:解读ImageView的wrap_content和adjustViewBounds的工作原理
举报原因:
原因补充:

(最多只允许输入30个字)