我们在布局文件中使用ImageView的时候,通常会有两种方法显示图片,设置background
属性或者设置src
属性。这两者有什么区别和联系呢?下面分析。
ImageView的源码版本:9.0
我们看个例子
运行结果
先说下结论
图片的缩放类型会影响src,不会影响background。
background总会充满整个ImageView的大小。当设置background是一张图片的时候可能会导致图片会拉伸(除非图片宽高比和ImageView的宽高比一样)。
src和background可以同时存在,src会覆盖在background上面。
ImageView的padding不影响background的的绘制区域。background总会充满整个ImageView的大小。
ImageView的padding会影响src的绘制区域。
下面进行分析。
我们先看一下ImageView的构造函数精简版
public ImageView(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
//调用父类的方法
super(context, attrs, defStyleAttr, defStyleRes);
//...
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.ImageView, defStyleAttr, defStyleRes);
//注释1处,获取src属性
final Drawable d = a.getDrawable(R.styleable.ImageView_src);
if (d != null) {
//调用setImageDrawable方法
setImageDrawable(d);
}
//...
a.recycle();
}
我们在注释1处,获取src属性指定的drawable对象,然后调用了ImageView的setImageDrawable方法
/**
* 设置一个drawable作为ImageView的内容
*
* @param drawable 要被设置的Drawable对象,如果为null的话则清除ImageView的内容
*/
public void setImageDrawable(Drawable drawable) {
if (mDrawable != drawable) {
mResource = 0;
mUri = null;
final int oldWidth = mDrawableWidth;
final int oldHeight = mDrawableHeight;
//注释1处
updateDrawable(drawable);
//注释2处
if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
requestLayout();
}
//注释3处
invalidate();
}
}
上面方法的注释1处调用了updateDrawable方法。
private void updateDrawable(Drawable d) {
//...
boolean sameDrawable = false;
//将mDrawable赋值为d
mDrawable = d;
if (d != null) {
d.setCallback(this);
//...
//获取drawable的宽高
mDrawableWidth = d.getIntrinsicWidth();
mDrawableHeight = d.getIntrinsicHeight();
applyImageTint();
applyColorMod();
//注释1处
configureBounds();
} else {
mDrawableWidth = mDrawableHeight = -1;
}
}
在上面方法的注释1处,调用了configureBounds方法,这个方法就是用来确定drawable的绘制区域。
private fun configureBounds() {
//...
//drawable想要的宽高
val dwidth = mDrawableWidth
val dheight = mDrawableHeight
//ImageView控件的宽高,减去padding
val vwidth = getWidth() - mPaddingLeft - mPaddingRight
val vheight = getHeight() - mPaddingTop - mPaddingBottom
val fits = (dwidth < 0 || vwidth == dwidth) && (dheight < 0 || vheight == dheight)
if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) {
/* 如果drawable没有固有的尺寸,或者ImageView的缩放类型是ScaleType.FIT_XY,
* 则让drawable绘制区域占满ImageView可绘制的宽高范围。
*/
mDrawable.setBounds(0, 0, vwidth, vheight)
mDrawMatrix = null
} else {
// 否则需要自己处理缩放。
mDrawable.setBounds(0, 0, dwidth, dheight)
if (ScaleType.MATRIX == mScaleType) {
// Use the specified matrix as-is.
if (mMatrix.isIdentity()) {
mDrawMatrix = null
} else {
mDrawMatrix = mMatrix
}
} else if (fits) {
// drawable的宽高和ImageView可绘制的宽高相等,不需要转换。
mDrawMatrix = null
} else if (ScaleType.CENTER == mScaleType) {
// 将位图放置在控件中心,没有缩放
mDrawMatrix = mMatrix
mDrawMatrix.setTranslate(Math.round((vwidth - dwidth) * 0.5f),
Math.round((vheight - dheight) * 0.5f))
} else if (ScaleType.CENTER_CROP == mScaleType) {
//缩放模式为中心剪裁
mDrawMatrix = mMatrix
//计算缩放比例和剪裁区域
float scale;
float dx = 0, dy = 0;
if (dwidth * vheight > vwidth * dheight) {
scale = vheight.toFloat() / dheight.toFloat()
dx = (vwidth - dwidth * scale) * 0.5f
} else {
scale = vwidth.toFloat() / dwidth.toFloat()
dy = (vheight - dheight * scale) * 0.5f
}
mDrawMatrix.setScale(scale, scale)
mDrawMatrix.postTranslate(Math.round(dx), Math.round(dy))
} else if (ScaleType.CENTER_INSIDE == mScaleType) {
mDrawMatrix = mMatrix
float scale;
float dx;
float dy;
if (dwidth <= vwidth && dheight <= vheight) {
scale = 1.0f
} else {
scale = Math.min(vwidth.toFloat() / dwidth.toFloat(),
vheight.toFloat() / dheight.toFloat())
}
dx = Math.round((vwidth - dwidth * scale) * 0.5f).toFloat()
dy = Math.round((vheight - dheight * scale) * 0.5f).toFloat()
mDrawMatrix.setScale(scale, scale)
mDrawMatrix.postTranslate(dx, dy)
} else {
//ImageView的默认缩放类型是ScaleType.FIT_CENTER,所以会走到这里
//drawable的想要绘制区域大小
mTempSrc.set(0, 0, dwidth, dheight)
//ImageView可绘制区域的大小
mTempDst.set(0, 0, vwidth, vheight)
mDrawMatrix = mMatrix
//根据缩放类型,最终确定drawable的绘制区域大小
mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType))
}
}
}
我们注意关注一下下面这段代码。
//drawable的想要绘制区域大小
mTempSrc.set(0, 0, dwidth, dheight)
//ImageView可绘制区域的大小
mTempDst.set(0, 0, vwidth, vheight)
mDrawMatrix = mMatrix
//根据缩放类型,最终确定drawable的绘制区域大小
mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType))
- mTempSrc代表drawable的想要绘制区域大小;
- mTempDst代表ImageView可绘制区域的大小,这个可绘制区域是ImageView控件的大小减去padding。
- mDrawMatrix根据mTempSrc,mTempDst和缩放类型mScaleType最终决定drawable的绘制区域。
我们回到ImageView的setImageDrawable方法的注释2处,和注释3处。
//注释2处
if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
requestLayout();
}
//注释3处
invalidate();
这最终会导致View重绘。我们看下View的draw方法的精简版
public void draw(Canvas canvas) {
//...
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
//...
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
//...
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
//...
}
第1步是调用drawBackground(Canvas canvas) 方法
private void drawBackground(Canvas canvas) {
//注释1处
final Drawable background = mBackground;
if (background == null) {
return;
}
//注释2处
setBackgroundBounds();
// ...
//是否要移动画布
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
//注释3处,绘制背景
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
在注释1处,首先将mBackground赋值给background。我们看下View的构造函数。
public View(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
this(context);
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
//...
Drawable background = null;
//...
final int N = a.getIndexCount();
for (int i = 0; i < N; i++) {
int attr = a.getIndex(i);
switch (attr) {
case com.android.internal.R.styleable.View_background:
//获取background
background = a.getDrawable(attr);
break;
//...
}
}
//...
if (background != null) {
//注释1处
setBackground(background);
}
}
在构造函数的注释1处,调用了setBackground方法。
public void setBackground(Drawable background) {
//noinspection deprecation
setBackgroundDrawable(background);
}
public void setBackgroundDrawable(Drawable background) {
if (background == mBackground) {
return;
}
boolean requestLayout = false;
mBackgroundResource = 0;
//...
if (background != null) {
//...
// 比较当前的mBackground和新的background的宽高是否相等,来确定是否需要重新layout
if (mBackground == null
|| mBackground.getMinimumHeight() != background.getMinimumHeight()
|| mBackground.getMinimumWidth() != background.getMinimumWidth()) {
requestLayout = true;
}
//为mBackground赋值
mBackground = background;
if (background.isStateful()) {
background.setState(getDrawableState());
}
if (isAttachedToWindow()) {
background.setVisible(getWindowVisibility() == VISIBLE && isShown(), false);
}
applyBackgroundTint();
// Set callback last, since the view may still be initializing.
background.setCallback(this);
if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
mPrivateFlags &= ~PFLAG_SKIP_DRAW;
requestLayout = true;
}
} else {
//传入的background为null,就移除掉背景
mBackground = null;
//...
requestLayout = true;
}
if (requestLayout) {
requestLayout();
}
mBackgroundSizeChanged = true;
//请求重新绘制
invalidate(true);
invalidateOutline();
}
现在我们找到了mBackground,我们回到drawBackground(Canvas canvas)方法的注释2处。
我们注意下,mBackground设置的绘制区域就是整个ImageView的大小,不受padding的影响。
void setBackgroundBounds() {
if (mBackgroundSizeChanged && mBackground != null) {
//设置整个控件大大小作为background绘制的区域
mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
rebuildOutline();
}
}
我们回到drawBackground(Canvas canvas)方法的注释3处,将背景画出来。
//注释3处,绘制背景
background.draw(canvas);
现在drawBackground(Canvas canvas)方法完了,我们回到draw(Canvas canvas)方法的第3步。调用onDraw(canvas)方法。ImageView重写了这个方法,我们直接看ImageView的onDraw(canvas)方法。
@Override
protected void onDraw(Canvas canvas) {
//父类是空实现
super.onDraw(canvas);
//mDrawable为null则返回
if (mDrawable == null) {
return;
}
//没有绘制区域,返回
if (mDrawableWidth == 0 || mDrawableHeight == 0) {
return;
}
if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
mDrawable.draw(canvas);
} else {
final int saveCount = canvas.getSaveCount();
canvas.save();
//...
//注释1处
canvas.translate(mPaddingLeft, mPaddingTop);
//注释2处
if (mDrawMatrix != null) {
canvas.concat(mDrawMatrix);
}
//注释3处,绘制
mDrawable.draw(canvas);
canvas.restoreToCount(saveCount);
}
}
上面方法的注释1处,首先将画布移动到点(mPaddingLeft,mPaddingTop。
然后在注释2处,这个方法的意思就是用当前画布的矩阵前连接mDrawMatrix。咱也不知道是啥,但是可以猜测这里会限制mDrawable的绘制区域,让mDrawable的绘制区域不会超过ImageView右边和底部的padding。然后在注释3处,将mDrawable绘制出来。
结论再说一下
图片的缩放类型会影响src,不会影响background。
background总会充满整个ImageView的大小。当设置background是一张图片的时候可能会导致图片会拉伸(除非图片宽高比和ImageView的宽高比一样)。
src和background可以同时存在,src会覆盖在background上面。
ImageView的padding不影响background的的绘制区域。background总会充满整个ImageView的大小。
ImageView的padding会影响src的绘制区域。
参考链接:
- Android ImageView 的scaleType属性详解(一)
- Android ImageView 的 scaleType属性详解(二)