我们在布局文件中使用ImageView的时候,通常会有两种方法显示图片,设置background
属性或者设置src
属性。这两者有什么区别和联系呢?下面分析。
ImageView的源码版本:9.0
我们看个例子
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingLeft="40dp"
tools:context=".activity.ImageViewSrcBackgroundActivity">
<TextView
android:layout_width="match_parent"
android:layout_height="40dp"
android:gravity="center_vertical"
android:text="原图"
android:textColor="#000000"
android:textSize="16sp" />
<ImageView
android:id="@+id/ivOriginal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/balloon" />
<TextView
android:layout_width="match_parent"
android:layout_height="40dp"
android:gravity="center_vertical"
android:text="设置src属性,设置background属性为灰色,ImageView宽高为200dp*200dp"
android:textColor="#000000"
android:textSize="16sp" />
<ImageView
android:id="@+id/ivSrc"
android:layout_width="200dp"
android:layout_height="200dp"
android:background="#FDA1A1A3"
android:src="@drawable/balloon" />
<TextView
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="40dp"
android:gravity="center_vertical"
android:text="设置background属性,ImageView宽高为200dp*200dp"
android:textColor="#000000"
android:textSize="16sp" />
<ImageView
android:id="@+id/ivBackground"
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@drawable/balloon" />
</LinearLayout>
运行结果
Screenshot_1554942750.png
先说下结论
- 图片的缩放类型会影响src,不会影响background。
- background总会充满整个ImageView的大小(当然去掉padding)。当设置background是一张图片的时候可能会导致图片会拉伸(除非图片宽高比和ImageView的宽高比一样)。
- src和background可以同时存在,src会覆盖在background上面。
下面进行分析。
我们先看一下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);
//获取src属性
final Drawable d = a.getDrawable(R.styleable.ImageView_src);
if (d != null) {
//调用setImageDrawable方法
setImageDrawable(d);
}
//...
a.recycle();
}
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;
if (mDrawable != null) {
sameDrawable = mDrawable == d;
mDrawable.setCallback(null);
unscheduleDrawable(mDrawable);
if (!sCompatDrawableVisibilityDispatch && !sameDrawable && isAttachedToWindow()) {
mDrawable.setVisible(false, false);
}
}
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
//去掉所有的padding,就是ImageView可以绘制的宽高范围
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 {
// 我们需要自己处理缩放,所以我们让drawable使用固有的宽高。
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) {
// Center bitmap in view, no scaling.
mDrawMatrix = mMatrix
mDrawMatrix.setTranslate(Math.round((vwidth - dwidth) * 0.5f),
Math.round((vheight - dheight) * 0.5f))
} else if (ScaleType.CENTER_CROP == mScaleType) {
mDrawMatrix = mMatrix
val scale: Float
var dx = 0f
var dy = 0f
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
val scale: Float
val dx: Float
val dy: Float
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))
}
}
}
在configureBounds方法方法中我们也注意到了,图片的缩放类型会影响src。
我们回到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) {
//绘制背景
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) {
computeOpaqueFlags();
if (background == mBackground) {
return;
}
boolean requestLayout = false;
mBackgroundResource = 0;
/*
* Regardless of whether we're setting a new background or not, we want
* to clear the previous drawable. setVisible first while we still have the callback set.
*/
if (mBackground != null) {
if (isAttachedToWindow()) {
mBackground.setVisible(false, false);
}
mBackground.setCallback(null);
unscheduleDrawable(mBackground);
}
if (background != null) {
Rect padding = sThreadLocal.get();
if (padding == null) {
padding = new Rect();
sThreadLocal.set(padding);
}
// Compare the minimum sizes of the old Drawable and the new. If there isn't an old or
// if it has a different minimum size, we should layout again
if (mBackground == null
|| mBackground.getMinimumHeight() != background.getMinimumHeight()
|| mBackground.getMinimumWidth() != background.getMinimumWidth()) {
requestLayout = true;
}
// Set mBackground before we set this as the callback and start making other
// background drawable state change calls. In particular, the setVisible call below
// can result in drawables attempting to start animations or otherwise invalidate,
// which requires the view set as the callback (us) to recognize the drawable as
// belonging to it as per verifyDrawable.
//为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
/* Remove the background */
//mBackground赋值为null
mBackground = null;
//...
/*
* When the background is set, we try to apply its padding to this
* View. When the background is removed, we don't touch this View's
* padding. This is noted in the Javadocs. Hence, we don't need to
* requestLayout(), the invalidate() below is sufficient.
*/
// The old background's minimum size could have affected this
// View's layout, so let's requestLayout
requestLayout = true;
}
if (requestLayout) {
requestLayout();
}
mBackgroundSizeChanged = true;
//请求重新绘制
invalidate(true);
invalidateOutline();
}
现在我们找到了mBackground,我们回到drawBackground(Canvas canvas)方法的注释2处。
我们注意下,mBackground设置的绘制区域就是整个ImageView的大小(去掉padding),也就是说mBackground会充满整个ImageView,这就是为什么我们设置一个图片作为背景的时候,图片会被拉伸的原因。
void setBackgroundBounds() {
if (mBackgroundSizeChanged && mBackground != null) {
mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
rebuildOutline();
}
}
现在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; // nothing to draw (empty bounds)
}
if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
mDrawable.draw(canvas);
} else {
final int saveCount = canvas.getSaveCount();
canvas.save();
//...
canvas.translate(mPaddingLeft, mPaddingTop);
if (mDrawMatrix != null) {
canvas.concat(mDrawMatrix);
}
//绘制
mDrawable.draw(canvas);
canvas.restoreToCount(saveCount);
}
}
结论再说一下。
- 图片的缩放类型会影响src,不会影响background。
- background总会充满整个ImageView的大小(当然去掉padding)。当设置为background是一张图片的时候可能会导致图片会拉伸(除非图片宽高比和ImageView的宽高比一样)。
- src和background可以同时存在,src会覆盖在background上面。
P.S. 关于ImageView的缩放属性可以参考如下链接:
网友评论