基础知识

ImageView backgroud 和src的区别

2019-04-11  本文已影响123人  leilifengxingmw

我们在布局文件中使用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的大小(当然去掉padding)。当设置background是一张图片的时候可能会导致图片会拉伸(除非图片宽高比和ImageView的宽高比一样)。
  3. 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);
    }
}

结论再说一下。

  1. 图片的缩放类型会影响src,不会影响background。
  2. background总会充满整个ImageView的大小(当然去掉padding)。当设置为background是一张图片的时候可能会导致图片会拉伸(除非图片宽高比和ImageView的宽高比一样)。
  3. src和background可以同时存在,src会覆盖在background上面。

P.S. 关于ImageView的缩放属性可以参考如下链接:

  1. Android ImageView 的scaleType属性详解(一)
  2. Android ImageView 的 scaleType属性详解(二)
上一篇下一篇

猜你喜欢

热点阅读