ImageView backgroud 和src的区别

我们在布局文件中使用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

先说下结论

  1. 图片的缩放类型会影响src,不会影响background。

  2. background总会充满整个ImageView的大小。当设置background是一张图片的时候可能会导致图片会拉伸(除非图片宽高比和ImageView的宽高比一样)。

  3. src和background可以同时存在,src会覆盖在background上面。

  4. ImageView的padding不影响background的的绘制区域。background总会充满整个ImageView的大小。

  5. 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))
        
  1. mTempSrc代表drawable的想要绘制区域大小;
  2. mTempDst代表ImageView可绘制区域的大小,这个可绘制区域是ImageView控件的大小减去padding。
  3. 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绘制出来。

结论再说一下

  1. 图片的缩放类型会影响src,不会影响background。

  2. background总会充满整个ImageView的大小。当设置background是一张图片的时候可能会导致图片会拉伸(除非图片宽高比和ImageView的宽高比一样)。

  3. src和background可以同时存在,src会覆盖在background上面。

  4. ImageView的padding不影响background的的绘制区域。background总会充满整个ImageView的大小。

  5. ImageView的padding会影响src的绘制区域。

参考链接:

  1. Android ImageView 的scaleType属性详解(一)
  2. Android ImageView 的 scaleType属性详解(二)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351