我们平时使用 ImageView 的场景比较多,不管是从资源、本地或是网络中获取图片,然后设置到 ImageView 中,适配图片这个问题绕不过去,比如 ImageView 的宽高比例和图片的宽高比例不一致,怎么办?是从图片居中剪切、从上往下剪切还是全部显示到 ImageView 中,周围留有黑边?如果获取的图片很大,为了优化内存,我们需要选择图片的比例来缩小,这时候要根据 ImageView 的宽高比例来取舍等等,这是为什么呢?如果 ImageView 本身宽度是固定的,图片宽度与 ImageView 的宽适配,想让它的高度随着图片的高度变化而变化,能做到吗
?这一切都需要从 ImageView 的源码说起,先看测量的方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
resolveUri();
int w; int h; float desiredAspect = 0.0f;
boolean resizeWidth = false; boolean resizeHeight = false;
final int widthSpecMode = View.MeasureSpec.getMode(widthMeasureSpec);
final int heightSpecMode = View.MeasureSpec.getMode(heightMeasureSpec);
// 重点零
if (mDrawable == null) {
mDrawableWidth = -1;
mDrawableHeight = -1;
w = h = 0;
} else {
w = mDrawableWidth;
h = mDrawableHeight;
if (w <= 0) w = 1;
if (h <= 0) h = 1;
// 重点一
if (mAdjustViewBounds) {
resizeWidth = widthSpecMode != View.MeasureSpec.EXACTLY;
resizeHeight = heightSpecMode != View.MeasureSpec.EXACTLY;
desiredAspect = (float) w / (float) h;
}
}
int pleft = mPaddingLeft;
int pright = mPaddingRight;
int ptop = mPaddingTop;
int pbottom = mPaddingBottom;
int widthSize;
int heightSize;
// 重点二
if (resizeWidth || resizeHeight) {
...
} else {
// 重点2.2
w += pleft + pright;
h += ptop + pbottom;
w = Math.max(w, getSuggestedMinimumWidth());
h = Math.max(h, getSuggestedMinimumHeight());
widthSize = resolveSizeAndState(w, widthMeasureSpec, 0);
heightSize = resolveSizeAndState(h, heightMeasureSpec, 0);
}
setMeasuredDimension(widthSize, heightSize);
}
这个是简化后的代码,resolveUri() 中是根据 setImageResource(int resId) 或 setImageURI(Uri uri) 方法接收 id 或 uri,根据这两个属性获取到 Drawable,然后调用 updateDrawable(Drawable d) 方法设置图片,这个稍后会介绍。首先获取 ImageView 的宽和高的模式,然后判断 mDrawable 是否为null,如果为null,则 w = h = 0,如果不为null,则把Drawable的宽高赋值给w和h,如果这两个值不是正数,则修正为1,此时假设 mAdjustViewBounds 为 false,先忽略 重点一,则 重点二 也可以忽略,直接走到了 重点2.2; w 和 h 在本身基础上加上对应的 padd 的值,如果 ImageView 设置了最小宽度和高度,还要再做个比较,然后通过 resolveSizeAndState() 方法来计算出它需要的宽和高,最后通过setMeasuredDimension() 方法来设置 ImageView 本身的宽高值给父容器。
此时我们基本可以确定一个简单的 ImageView 的宽和高是怎么确定的,我们再来看看绘制的方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mDrawable == null) {
return; // couldn't resolve the URI
}
if (mDrawableWidth == 0 || mDrawableHeight == 0) {
return; // nothing to draw (empty bounds)
}
if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
mDrawable.draw(canvas);
} else {
int saveCount = canvas.getSaveCount();
canvas.save();
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);
if (mDrawMatrix != null) {
canvas.concat(mDrawMatrix);
}
mDrawable.draw(canvas);
canvas.restoreToCount(saveCount);
}
}
简单点,基本上就是 mDrawable.draw(canvas) 这行代码,原来 ImageView 显示都是通过 Drawable 来绘制图形的;canvas.clipRect() 是用来裁剪的,意思是只在裁剪的区域内绘制,超出的部分不绘制。这是是把 Drawable 给绘制出来了,但 Drawable 的大小显示区域是哪里控制的呢? 难道 ImageView 的的绘制就这么完了? 当然不是,我们知道在 View 的 layout() 方法中会调用 setFrame() 方法,我们看一下 ImageView 中的
@Override
protected boolean setFrame(int l, int t, int r, int b) {
boolean changed = super.setFrame(l, t, r, b);
mHaveFrame = true;
configureBounds();
return changed;
}
它里面调用了 configureBounds() 方法,如果再看看 setImageBitmap(Bitmap bm)、setImageIcon(Icon icon) 等方法,发现最终都是调用 setImageDrawable() 方法,进而调用 updateDrawable(Drawable d) 方法,在这个方法中,会调用 configureBounds() 方法,那么这个方法是干嘛用的呢? 我们平时通过设置 ScaleType 的类型,来控制 ImageView 中图片显示的样式,或居中、或拉伸铺满View,就是在这个方法中控制的。
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 || ImageView.ScaleType.FIT_XY == mScaleType) {
mDrawable.setBounds(0, 0, vwidth, vheight);
mDrawMatrix = null;
} else {
mDrawable.setBounds(0, 0, dwidth, dheight);
if (ImageView.ScaleType.MATRIX == mScaleType) {
if (mMatrix.isIdentity()) {
mDrawMatrix = null;
} else {
mDrawMatrix = mMatrix;
}
} else if (fits) {
mDrawMatrix = null;
} else if (ImageView.ScaleType.CENTER == mScaleType) {
mDrawMatrix = mMatrix;
mDrawMatrix.setTranslate(Math.round((vwidth - dwidth) * 0.5f), Math.round((vheight - dheight) * 0.5f));
} else if (ImageView.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 (ImageView.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 {
mTempSrc.set(0, 0, dwidth, dheight);
mTempDst.set(0, 0, vwidth, vheight);
mDrawMatrix = mMatrix;
mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType));
}
}
}
dwidth 和 dheight 是 Drawable 的宽和高,vwidth 和 vheight 是 ImageView 的可绘制区域的宽和高,fits 这个属性基本可以忽略不计;我们先看第一个if判断,如果drawable 没有大小或者 mScaleType 属性设置的是 FIT_XY 类型,则 把 vwidth 和 vheight 作为 setBounds() 设定它的边界的参数值,同时把 Matrix 矩阵对象置空;除了这种if情况外,其他的设置都是把 dwidth 和 dheight 作为参数设置到 setBounds() 方法中,如果 mScaleType 设置的是 MATRIX,则从左上角起始的矩阵区域开始绘制(自定义矩阵的除外);mScaleType 为 CENTER 时,(vwidth - dwidth) * 0.5f 计算的是view和drawable的宽的差值的一半,setTranslate() 则是设置了矩阵位移的值,只是位移;CENTER_CROP 中,会判断drawable的宽高比例与View的宽高比的大小,然后计算缩放值即位移值,它也是居中显示,但是会缩放图片,选取居中的展示;CENTER_INSIDE 则是有点不同,若图片高宽均小于控件高宽,则不进行缩放只进行偏移,若大于控件宽高,也是缩放,但是以较小值作为缩放的倍数; 网上有篇文章介绍的具体显示效果挺不错的,地址为 https://www.jianshu.com/p/32e335d5b842。
说了半天,还没说到如果宽度固定,如何让图片的高度去自适应的问题。configureBounds() 方法其实就是限定 Drawable 的绘制区域,Drawable 是在 ImageView 中的 onDraw()方法中通过 mDrawable.draw(canvas) 绘制的,决定图片样式大小的还是与 getWidth() 及 mDrawableWidth 有关系,mDrawableWidth 是固定的,那么我们改变 getWidth() 的值就行了。重新看 onMeasure() 方法,我们忽略的代码 重点一 和 重点二
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
boolean resizeWidth = false; boolean resizeHeight = false;
final int widthSpecMode = View.MeasureSpec.getMode(widthMeasureSpec);
final int heightSpecMode = View.MeasureSpec.getMode(heightMeasureSpec);
...
w = mDrawableWidth;
h = mDrawableHeight;
// 重点一
if (mAdjustViewBounds) {
resizeWidth = widthSpecMode != View.MeasureSpec.EXACTLY;
resizeHeight = heightSpecMode != View.MeasureSpec.EXACTLY;
desiredAspect = (float) w / (float) h;
}
int pleft = mPaddingLeft;int pright = mPaddingRight; int ptop = mPaddingTop;int pbottom = mPaddingBottom;
int widthSize; int heightSize;
// 重点二
if (resizeWidth || resizeHeight) {
widthSize = resolveAdjustedSize(w + pleft + pright, mMaxWidth, widthMeasureSpec);
heightSize = resolveAdjustedSize(h + ptop + pbottom, mMaxHeight, heightMeasureSpec);
// 重点2.1
if (desiredAspect != 0.0f) {
float actualAspect = (float)(widthSize - pleft - pright) / (heightSize - ptop - pbottom);
if (Math.abs(actualAspect - desiredAspect) > 0.0000001) {
boolean done = false;
if (resizeWidth) {
int newWidth = (int)(desiredAspect * (heightSize - ptop - pbottom)) + pleft + pright;
if (!resizeHeight && !mAdjustViewBoundsCompat) {
widthSize = resolveAdjustedSize(newWidth, mMaxWidth, widthMeasureSpec);
}
if (newWidth <= widthSize) {
widthSize = newWidth;
done = true;
}
}
if (!done && resizeHeight) {
int newHeight = (int)((widthSize - pleft - pright) / desiredAspect) + ptop + pbottom;
if (!resizeWidth && !mAdjustViewBoundsCompat) {
heightSize = resolveAdjustedSize(newHeight, mMaxHeight,heightMeasureSpec);
}
if (newHeight <= heightSize) {
heightSize = newHeight;
}
}
}
}
} else {
...
}
setMeasuredDimension(widthSize, heightSize);
}
mAdjustViewBounds 这个属性可以通过xml或是set方法来设置,只要它设置为true,就可以自适应了。 重点一 中,如果宽和高的模式不是固定的,那么就需要重新计算,同时计算出 drawable的宽和高的比例值 desiredAspect; 重点二 中,根据drawable的宽和高,获取了新的值 widthSize 和 heightSize,如果 desiredAspect 值不为0,actualAspect这个值是把 padd 值去掉后的宽高比,如果两者误差超过 0.0000001,则重新计算。如果宽度需重新计算,则以高度为准,然后根据比例算出一个新的宽 newWidth,此时如果高度不需要重新计算,则根据 resolveAdjustedSize() 方法算出 widthSize,然后做个比较,如果新的宽比较小,则用新的值,高度不变,用 drawable 获取到的高; 像我们上面举的例子,宽固定,高去自适应,测会根据drawable的宽高比去计算出需要的高度,然后比较一下大小,和计算宽度一个道理。 这里需要注意一点,设置 mAdjustViewBounds 后ScaleType 会变为 FIT_CENTER 模式。