前言
ImageView是Android最基础的控件之一,通过ImageView我们能够展示各式各样的图片,对其原理的研究有助于我们更好的使用它。
通过本篇文章,你将了解到:
1、ImageView 如何确定view的尺寸
2、ImageView "adjustViewBounds" 怎么用
3、ImageView "scaleType" 理解与运用
4、ImageView 和Drawable异同
ImageView 尺寸的确定
ImageView继承自View,我们知道View的尺寸最终是在onMeasure方法里确定,看看ImageView对该方法有没有做处理:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//省略
int w;
int h;
if (mDrawable == null) {
mDrawableWidth = -1;
mDrawableHeight = -1;
w = h = 0;
} else {
//mDrawableWidth 为ImageView内容宽、高
w = mDrawableWidth;
h = mDrawableHeight;
if (w <= 0) w = 1;
if (h <= 0) h = 1;
}
if (resizeWidth || resizeHeight) {
//关于adjustViewBounds 处理
//省略
} else {
w += pleft + pright;
h += ptop + pbottom;
//寻找较大值,确保能够容纳
w = Math.max(w, getSuggestedMinimumWidth());
h = Math.max(h, getSuggestedMinimumHeight());
//widthMeasureSpec/heightMeasureSpec 是父控件为ImageView分配的大小
//w、h为内容的大小
//该方法是结合父控件给的大小与内容的大小,最终算出View真正需要的大小
//具体规则如下:
//1、如果父控件给的测量模式是:EXACTLY,那么ImageView将采取spec里的值
//2、如果父控件给的测量模式是:AT_MOST,那么ImageView将会采取内容的值。这里还需要注意的是
//如果内容的大小超过父控件的给大小,那么限制最终的大小不超过父控件给的值
widthSize = resolveSizeAndState(w, widthMeasureSpec, 0);
heightSize = resolveSizeAndState(h, heightMeasureSpec, 0);
}
//最终、将计算后的尺寸保存到mMeasuredWidth/mMeasuredHeight里,ImageView测量完成
setMeasuredDimension(widthSize, heightSize);
}
ImageView重写了onMeasure方法。从上面可以看出,ImageView尺寸取决于内容的大小与父控件的大小,来看看不同组合对ImageView大小的影响。
小例子
首先选取一张图片test2.jpg,并放置在Drawable/nodpi目录下(读取图片原始尺寸,不进行压缩,对此想了解的请移步:Android 屏幕分辨率适配)。
该图片长宽分别为:592*258(单位像素)
//对应AT_MOST 此时以内容大小为准
//对应宽为:EXACTLY 以父控件给的大小为准
// 对应高为:AT_MOST 以内容高为准
两者都设置了背景,便于直观观察ImageView的尺寸变化(当然更精确的比较是打印ImageView大小)。看看效果:
对比上面两图,只是更改了"layout_width"属性,产生的效果却是不同。
总结:当ImageView使用“wrap_content”时,其尺寸取决于内容的大小
ImageView scaleType 属性
上面讲述了ImageView尺寸是如何确定的,但是如果给ImageView设置固定宽高,而图片尺寸与之不一样,该怎么确定图片在ImageView上的展示呢?ImageView提供了“scaleType”属性来定制不同的展示方式。
public enum ScaleType {
MATRIX (0),
FIT_XY (1),
FIT_START (2),
FIT_CENTER (3),
FIT_END (4),
CENTER (5),
CENTER_CROP (6),
CENTER_INSIDE (7);
}
来看看设置不同scaleType的效果。
图片尺寸是:182 * 538(px)
ImageView尺寸是:100 * 100 (dp) ,测试设备密度是2.75 ,换算作像素是:275 * 275 px
关于dp与px请参考:Android 屏幕分辨率适配
为了更直观看出控件尺寸与图片尺寸,给控件尺寸加了红色背景。
可以看出图片的宽小于控件的宽,图片的高大于控件的高。
分别来看看各个模式下展示表现,每个控件上都有标明对应的scaleType。
1、原图展示在屏幕最下方,此时图片没有缩放。
2、matrix:图片没有缩放,按照正常布局(matrix如果设置了变换,则可能会有缩放、平移等操作),从左上角开始展示,超出部分不显示。
3、fitXY:图片非等比例缩放,把图片宽高限制在控件内并且充满控件的四周。
4、fitStart:图片等比例缩放,把图片的宽高限制在控件内,缩放规则:两边都需要缩放到控件内,至少有一边与控件的某边齐平。缩放后,从左上角开始展示。
5、fitCenter:缩放规则同fitStart,只是缩放后,图片居中展示。
6、fitEnd:缩放规则同fitStart,只是缩放后,从右下角开始展示。
7、center:不缩放,图片居中展示。
8、centerCrop:等比例缩放,缩放规则:图片长宽都都需要大于等于控件长宽。居中展示。
9、centerInside:等比例缩放,缩放规则,图片长宽有一边大于控件尺寸,则缩放,两边都需要缩放到控件内。如果图片长宽都小于控件尺寸,则不缩放。不管是否缩放,都居中展示。
大部分文章对scaleType解释止步于此,看过容易忘记,主要是原理没有展开将讲述,接下来我们从源码角度进行深入分析,让记忆更深刻。
ImageView ScaleType 源码实现
ScaleType实现在ImageView configureBounds方法里,该方法在layout之后生效。
private void configureBounds() {
if (mDrawable == null || !mHaveFrame) {
return;
}
//图片尺寸
final int dwidth = mDrawableWidth;
final int dheight = mDrawableHeight;
//控件尺寸
final int vwidth = getWidth() - mPaddingLeft - mPaddingRight;
final int vheight = getHeight() - mPaddingTop - mPaddingBottom;
final boolean fits = (dwidth < 0 || vwidth == dwidth)
&& (dheight < 0 || vheight == dheight);
if (dwidth <= 0 || dheight <= 0 || ImageView.ScaleType.FIT_XY == mScaleType) {
//将drawable尺寸设置为与控件尺寸一致
//最终会将图片绘制到drawable设置的大小区域
mDrawable.setBounds(0, 0, vwidth, vheight);
mDrawMatrix = null;
} else {
//将drawable尺寸设置为与图片尺寸一致
//最终的会将图片绘制到drawable设置的大小区域
mDrawable.setBounds(0, 0, dwidth, dheight);
if (ImageView.ScaleType.MATRIX == mScaleType) {
//使用matrix
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) {
//这个判断不是那么直观,换个方式看
//dwidth * vheight > vwidth * dheight
//dwidth / vwidth > dheight / vheight
//vwidth / dwidth <= vheight / 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 {
//与centerCrop模式相反,这里的判断是:如果控件/图片宽比例 小于 其高的比例
//那么缩放比例采用宽的比例,也就是较小值的比例。
//此种模式下,缩放后图片的宽高都不能超过控件宽高
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);
//先缩放,沿着默认的点(0,0)
mDrawMatrix.setScale(scale, scale);
//再平移,使得图片居中
mDrawMatrix.postTranslate(dx, dy);
} else {
// 剩下的模式包括:
//fitStart
//fitEnd
//fitCenter
mTempSrc.set(0, 0, dwidth, dheight);
mTempDst.set(0, 0, vwidth, vheight);
mDrawMatrix = mMatrix;
//重点是此
mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType));
}
}
}
上看代码有注释,应该比较详细了。接下来看看
mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType));方法:
enum ScaleToFit {
kFill_ScaleToFit,
kStart_ScaleToFit, //对应java层fit_Start
kCenter_ScaleToFit, //对应java层fit_Center
kEnd_ScaleToFit, //对应java层fit_End
};
bool SkMatrix::setRectToRect(const SkRect& src, const SkRect& dst, ScaleToFit align) {
if (src.isEmpty()) {
this->reset();
return false;
}
if (dst.isEmpty()) {
sk_bzero(fMat, 8 * sizeof(SkScalar));
fMat[kMPersp2] = 1;
this->setTypeMask(kScale_Mask | kRectStaysRect_Mask);
} else {
//dst 表示控件尺寸
//src 表示图片尺寸
//先算宽、高比例
SkScalar tx, sx = dst.width() / src.width();
SkScalar ty, sy = dst.height() / src.height();
bool xLarger = false;
//对应fit
if (align != kFill_ScaleToFit) {
//依然是熟悉的配方,取比例比较小值进行缩放,缩放规则同java层的center_inside模式
if (sx > sy) {
xLarger = true;
sx = sy;
} else {
sy = sx;
}
}
tx = dst.fLeft - src.fLeft * sx;
ty = dst.fTop - src.fTop * sy;
if (align == kCenter_ScaleToFit || align == kEnd_ScaleToFit) {
SkScalar diff;
if (xLarger) {
//算出平移量,注意是整个x偏移
diff = dst.width() - src.width() * sy;
} else {
diff = dst.height() - src.height() * sy;
}
if (align == kCenter_ScaleToFit) {
//如果是fit_center模式,则算出居中偏移量 SkScalarHalf=diff/2
diff = SkScalarHalf(diff);
}
if (xLarger) {
//如果是以高的比例缩放,那么需要对x方向进行平移
//照上面的计算,如果是fit_center模式,那么diff已经是平分过了
//如果是fit_end模式,那么将偏移到平齐右下方
tx += diff;
} else {
ty += diff;
}
} else {
//如果是fit_start,那么不对diff作操作
}
//最后进行缩放+平移
this->setScaleTranslate(sx, sy, tx, ty);
}
return true;
}
1、可以看出fit_start、fit_center、fit_end缩放规则与center_inside类似,只是center_inside图片宽、高其一大于控件宽、高才生效。
2、对于图片的缩放最终都会落实到matrix变换。ImageView scaleType也是Matrix经典运用的具体体现。需要注意的是,这里的Matrix scale都是基于左上角(0,0)做变换的。关于Matrix变换请移步Android Matrix 不再疑惑
3、scaleType只是改变图片的展示方式,并没有减少或者增大图片的内存占用。
4、scaleType做的工作实际上就是如何让内容在控件上做不同的展示,这里面的思想运用也比较多,比如做视频播放器时,如何让视频填充高或者宽,并居中播放。
scaleType默认值
private void initImageView() {
mMatrix = new Matrix();
mScaleType = ScaleType.FIT_CENTER;
if (!sCompatDone) {
final int targetSdkVersion = mContext.getApplicationInfo().targetSdkVersion;
sCompatAdjustViewBounds = targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR1;
sCompatUseCorrectStreamDensity = targetSdkVersion > Build.VERSION_CODES.M;
sCompatDrawableVisibilityDispatch = targetSdkVersion < Build.VERSION_CODES.N;
sCompatDone = true;
}
}
可以看出scaleType默认值是FIT_CENTER模式。
ImageView "adjustViewBounds" 怎么用
scaleType是控件尺寸不变,图片适应控件的尺寸。那么控件的尺寸能否随着图片的尺寸变化呢?答案是可以的,就是通过adjustViewBounds,顾名思义。
我们都知道控件的尺寸确定是在onMeasure方法里,前面我们分析ImageView onMeasure方法时,省略了一部分代码:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mDrawable == null) {
// If no drawable, its intrinsic size is 0.
mDrawableWidth = -1;
mDrawableHeight = -1;
w = h = 0;
} else {
//是否设置了"AdjustViewBounds"属性
if (mAdjustViewBounds) {
//是否需要重新计算宽高,如果父类给的测量模式不是EXACTLY,则需要重新计算
//如果是EXACTLY,则控件尺寸都是固定的,没必要重新计算
resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;
resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;
//图片 宽/高比例
desiredAspect = (float) w / (float) h;
}
}
if (resizeWidth || resizeHeight) {
//计算控件宽
widthSize = resolveAdjustedSize(w + pleft + pright, mMaxWidth, widthMeasureSpec);
//计算控件高
heightSize = resolveAdjustedSize(h + ptop + pbottom, mMaxHeight, heightMeasureSpec);
if (desiredAspect != 0.0f) {
// 实际的控件宽/高比例
final 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 && !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 {
//省略
}
setMeasuredDimension(widthSize, heightSize);
}
在xml里使用这属性
无论图片多大,控件就会多大,跟随图片的尺寸变化。此时图片不缩放,不平移。因此adjustViewBounds属性和scaleType属性是两种不同的作用,前者是控制控件的大小,后者是控制图片的显示大小。
ImageView 和 Drawable异同
在演示scaleType属性的时候,ImageView引用了BitmapDrawable,而BitmapDrawable持有Bitmap对象,最终将Bitmap展示在view上。
大体流程如下:
ImageView->onDraw()->Drawable->draw()->canvas.drawXXX();
我们知道View需要展示到屏幕上,最终得在onDraw()方法里调用canvas的系列方法,比如:
@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(bitmap, null, new Rect(0, 0, 100, 258), paint);
}
如果在onDraw里的绘制有通用的部分可以展示,那么可以提出来,如:
private void draw(Canvas canvas) {
canvas.drawBitmap(bitmap, null, new Rect(0, 0, 100, 258), paint);
canvas.drawXX();
}
}
那么在View的onDraw()方法里只需要调用公共部分draw()方法就可以实现不同的效果。实际上Drawable就是这么使用的,我们把一些通用效果封装为不同的Drawable,在View里持有Drawable对象,最终在View的onDraw()里调用Drawable的draw()方法。
Drawable需要的两个要素
1、通过setBounds()设置drawable尺寸
2、重写draw()方法,该方法里使用drawable限制的尺寸进行绘制