ImageView backgroud 和src的区别

我们在布局文件中使用ImageView的时候,通常会有两种方法显示图片,设置background属性或者设置src属性。这两者有什么区别和联系呢?下面分析。

ImageView的源码版本:9.0

我们看个例子




    

    

    

    


    

    



运行结果

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属性详解(二)

你可能感兴趣的:(ImageView backgroud 和src的区别)