图片基础知识梳理(1) - ImageView 的 ScaleType 属性解析
一、概述
在使用ImageView
的过程当中,经常需要通过scaleType
来对原始的图像进行处理,使得它能在空间中合理地展示。
二、scaleType
的分类
首先,我们简单介绍一下scaleType
的分类:
2.1 通过Matrix
设置
这种情况下,对应的模式只有一种:
ScaleType.MATRIX
最终,在这种情况下,我们可以同setImageMatrix(Matrix matrix)
来改变。
2.2 填充类型
这一类属性的特点就是通过拉伸或者压缩图片,使得原图片中所有元素都能够展现,并且至少填满控件x,y
轴的其中一个。
一共有四类:
-
ScaleType.FIX_XY
:不考虑原图的比例,拉伸或者压缩使得它等于控件的宽高。
下面三种都会维持原图的比例,使得它们的x,y
都小于等于控件的宽高,只是最终的图形放的位置不同。
-
ScaleType.FIT_START
:放置在左上角。 -
ScaleType.FIT_CENTER
:放置在中间。 -
ScaleType.FIT_END
:放置在右下角。
2.3 中心重合类型
下面的三种类型都会使得控件的中心和图片中心重合:
-
ScaleType.CENTER
要求一点:- 原图的中心和控件的中心重合
-
ScaleType.CENTER_CROP
要求三点:- 整个控件能够被填满
- 原图的比例不变
- 原图的中心和控件的中心重合。
- 保证原图的
x,y
轴上的元素至少有一个在控件中能被完全展示,那么有一下两种情况,在下面的操作做完之后,裁剪掉多余的部分:- 如果原图没有填满控件,那么会慢慢按比例放大,直到填满控件;
- 如果原图已经填满控件,那么它会慢慢缩小,直到某一边和控件重合。
-
ScaleType.CENTER_INSIDE
要求三点:- 原图的所有像素位于控件内部
- 原图的比例不变
- 图片的中心和控件的中心重合。
它不要求原始图片填满x,y
轴的任意一个,因此,如果原图的长宽都小于等于控件的长宽,不会进行放大操作,这也是它和ScaleType.FIT_CENTER
的区别。
三、示例
下面,我们通过一个简单的Demo
来展示一下各种类型的具体表现,我们有两个大小一样的ImageView
和两个大小不同的原图,其中左边的ImageView
要比原图小,右边的ImageView
要比原图大。
-
ScaleType.FIX_XY
:
-
ScaleType.FIT_START
:
-
ScaleType.FIT_CENTER
-
ScaleType.FIT_END
-
ScaleType.CENTER
-
ScaleType.CENTER_CROP
-
ScaleType.CENTER_INSIDE
四、源码分析
4.1 给ImageVIew
设置src
的接口
在ImageView
当中,设置图片的接口主要有下面几个函数:
public void setImageBitmap(Bitmap bm)
public void setImageResource(@DrawableRes int resId)
public void setImageURI(@Nullable Uri uri)
public void setImageDrawable(@Nullable Drawable drawable)
4.2 setImageBitmap
的流程
我们就以平时常用的setImageBitmap
为例,分析一下它整个的流程:
- 第一步:当我们调用
setImageBitmap
之后,它会把Bitmap
封装在BitmapDrawable
当中,之后调用了setImageDrawable(Drawable drawable)
方法:public void setImageBitmap(Bitmap bm) { mDrawable = null; if (mRecycleableBitmapDrawable == null) { mRecycleableBitmapDrawable = new BitmapDrawable(mContext.getResources(), bm); } else { mRecycleableBitmapDrawable.setBitmap(bm); } setImageDrawable(mRecycleableBitmapDrawable); }
-
第二步:调用
setImageDrawable
public void setImageDrawable(@Nullable Drawable drawable) { //如果不是同一个资源. if (mDrawable != drawable) { mResource = 0; mUri = null; //旧的宽高. final int oldWidth = mDrawableWidth; final int oldHeight = mDrawableHeight; //关键方法 updateDrawable(drawable); //如果宽高不同,才请求重新布局. if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) { requestLayout(); } //只要替换了资源就需要重新绘制. invalidate(); } }
上面的关键方法在
updateDrawable
当中:private void updateDrawable(Drawable d) { //.... if (d != null) { //... //这里面根据scaleType配置mDrawMatrix. configureBounds(); } else { mDrawableWidth = mDrawableHeight = -1; } }
4.3
configureBounds
改变Matrix
在
configureBounds
里就会根据我们所配置的scaleType
来决定mDrawable
如何显示,在这里面有一个重要的变量mDrawMatrix
,我们前面说到的所有变换都是通过它来实现的,当然,我们除了可以让系统自己根据scaleType
来生成matrix
,也可以通过setImageMatrix
手动的指定自己的变换:private void configureBounds() { if (mDrawable == null || !mHaveFrame) { return; } //1.得到原始资源的宽高. final int dwidth = mDrawableWidth; final int dheight = mDrawableHeight; //2.得到控件的宽高,这里去掉了控件的padding. final int vwidth = getWidth() - mPaddingLeft - mPaddingRight; final int vheight = getHeight() - mPaddingTop - mPaddingBottom; //3.表示原始资源已经能够填满控件. final boolean fits = (dwidth < 0 || vwidth == dwidth) && (dheight < 0 || vheight == dheight); //4.假如有一边是wrap_content,或者是FIX_XY,那么填满整个控件. if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) { mDrawable.setBounds(0, 0, vwidth, vheight); mDrawMatrix = null; } else { mDrawable.setBounds(0, 0, dwidth, dheight); //如果scaleType是matrix. if (ScaleType.MATRIX == mScaleType) { //单位矩阵的情况,设为null. if (mMatrix.isIdentity()) { mDrawMatrix = null; } else { //否则最后的DrawMatrix就是我们传入的Matrix. mDrawMatrix = mMatrix; } } else if (fits) { //如果原始资源已经填满控件,那么不需要考虑其它的变换了. mDrawMatrix = null; } else if (ScaleType.CENTER == mScaleType) { //当scaleType为center的时候. mDrawMatrix = mMatrix; //移动到中心,初始时候,控件和原始资源的(0,0)点是重合的. mDrawMatrix.setTranslate(Math.round((vwidth - dwidth) * 0.5f), Math.round((vheight - dheight) * 0.5f)); } else if (ScaleType.CENTER_CROP == mScaleType) { mDrawMatrix = mMatrix; //当scaleType是centerCrop的时候. float scale; float dx = 0, dy = 0; //取需要变换最小的轴,进行等比缩放. if (dwidth * vheight > vwidth * dheight) { scale = (float) vheight / (float) dheight; dx = (vwidth - dwidth * scale) * 0.5f; } else { scale = (float) vwidth / (float) dwidth; dy = (vheight - dheight * scale) * 0.5f; } //先缩放,再移动到中心点. mDrawMatrix.setScale(scale, scale); mDrawMatrix.postTranslate(Math.round(dx), Math.round(dy)); } else if (ScaleType.CENTER_INSIDE == mScaleType) { //当scaleType是centetInside时 mDrawMatrix = mMatrix; float scale; float dx; float dy; //如果原始资源的宽高都小于控件的宽高,那么不做缩放. if (dwidth <= vwidth && dheight <= vheight) { scale = 1.0f; } else { //否则等比缩放. scale = Math.min((float) vwidth / (float) dwidth, (float) vheight / (float) dheight); } dx = Math.round((vwidth - dwidth * scale) * 0.5f); dy = Math.round((vheight - dheight * scale) * 0.5f); //和上面类似,也是先缩放后平移. mDrawMatrix.setScale(scale, scale); mDrawMatrix.postTranslate(dx, dy); } else { //设置两个区域的大小. mTempSrc.set(0, 0, dwidth, dheight); mTempDst.set(0, 0, vwidth, vheight); mDrawMatrix = mMatrix; //这里处理FIX_START,FIX_END,FIX_CENTER的情况. mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType)); } } }
4.4
onDraw
中进行绘制那么这个
mDrawMatrix
是在什么时候使用的呢,我们看一下onDraw
方法:protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) { mDrawable.draw(canvas); } else { final int saveCount = canvas.getSaveCount(); //创建一个新的图层. canvas.save(); if (mCropToPadding) { final int scrollX = mScrollX; final int scrollY = mScrollY; canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop, scrollX + mRight - mLeft - mPaddingRight, scrollY + mBottom - mTop - mPaddingBottom); } //移动到去掉padding的左上角. canvas.translate(mPaddingLeft, mPaddingTop); //根据mDrawMatrix进行变换. if (mDrawMatrix != null) { canvas.concat(mDrawMatrix); } //再在这个变换上面绘制我们的资源. mDrawable.draw(canvas); //把合成完成的图片绘制上去. canvas.restoreToCount(saveCount); } }
4.5 小结
我们总结一下,整个
scaleType
的原理就是在configureBounds
中配置了mDrawMatrix
,而在onDraw
当中会根据mDrawMatrix
来对图层进行变换,在这个变换之后的图层上进行绘制mDrawable
,之后再恢复图层。五、
ImageView
的src
和background
的区别上面,我们看到的都是
src
设置的效果,我们回忆一下,通过设置android:background
也可以设置一个图片给它,其实background
是View
的属性,在我们之前分析View
的绘制流程的时候,draw(canvas)
中有一步就是绘制背景:private void drawBackground(Canvas canvas) { //1.设置背景的边界. setBackgroundBounds(); //2.如果有滚动,那么背景需要相应的滚动. final int scrollX = mScrollX; final int scrollY = mScrollY; if ((scrollX | scrollY) == 0) { background.draw(canvas); } else { canvas.translate(scrollX, scrollY); //3.绘制背景. background.draw(canvas); canvas.translate(-scrollX, -scrollY); } }
我们来看一下设置背景的边界的函数,可以看到,这里没有考虑
padding
值,也就是说我们通过background
设置的图片是填满整个控件,并且不考虑padding
的:void setBackgroundBounds() { if (mBackgroundSizeChanged && mBackground != null) { //没有考虑padding部分. mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop); mBackgroundSizeChanged = false; rebuildOutline(); } }
最后再结合一下第四节的知识,我们是先绘制背景,然后才在
ImageView
的onDraw
函数当中在canvas
上绘制的,因此,src
的图片一定会绘制在backgroud
之上。