Android源码系列之深入理解ImageView的ScaleType属性
做Android开发的童靴们肯定对系统自带的控件使用的都非常熟悉,比如Button、TextView、ImageView等。如果你问我具体使用,我会给说:拿ImageView来说吧,首先创建一个新的项目,在项目布局文件中应用ImageView控件,代码如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#bbaacc" > <ImageView android:src="@drawable/ic_launcher" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#aabbcc" /> </LinearLayout>
上边布局文件为了便于查看各种属性效果,故意加了两个背景颜色,这对我们今天的源码分析影响不大。接着运行一下代码,效果图如下:
恩,不错,运行结果正如所愿,屏幕上显示的正是我们设置的图片,这时候心中不由欣喜ImageView的使用就是这样简单,so easy嘛!呵呵,如果真是这么想那就大错特错了,上边的示例仅仅是在布局文件中使用了ImageView的src,layout_width和layout_height这三个属性罢了,它其他的重要属性我们还没有用到,今天这篇文章就是主要结合源码讲解ImageView的另一个重要的属性------ScaleType,其他的一些属性等将来需要的话再做详细解说。好了,现在正式进入主题。
ScaleType属性主要是用来定义图片(Bitmap)如何在ImageView中展示的,姑且就认为是展示吧,系统给我们提供了8种可选属性:matrix、fitXY、fitStart、fitCenter、fitEnd、center、centerCrop和centerInside。每一种属性对应的展示效果是不一样的,下面我们先来做一个实验来说明每一种属性的显示效果,我从之前的项目中挑选了两张图片,一张图片的实际尺寸是720*1152,另一张是我把第一张图翻转放缩得到的,它的实际尺寸是96*60,之所以采用两张图片是为了便于对比和观察结果,原图片如下:
OK,测试图片准备好了,接下来我们在布局文件中分别使用ScaleType的每一个属性值,我们开始写布局文件,内容如下:
<?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#bbccaa" > <TableLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="10dp" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="10dp" android:text="图片尺寸的宽和高都远远大于ImageView的尺寸" /> <TableRow android:layout_width="match_parent" android:layout_height="wrap_content" > <ImageView android:layout_width="300px" android:layout_height="300px" android:background="#aabbcc" android:scaleType="matrix" android:src="@drawable/test" /> <ImageView android:layout_width="300px" android:layout_height="300px" android:layout_marginLeft="10dp" android:background="#aabbcc" android:scaleType="fitXY" android:src="@drawable/test" /> <ImageView android:layout_width="300px" android:layout_height="300px" android:layout_marginLeft="10dp" android:background="#aabbcc" android:scaleType="fitStart" android:src="@drawable/test" /> <ImageView android:layout_width="300px" android:layout_height="300px" android:layout_marginLeft="10dp" android:background="#aabbcc" android:scaleType="fitCenter" android:src="@drawable/test" /> </TableRow> <TableRow android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="5dp" > <TextView android:layout_width="300px" android:layout_height="wrap_content" android:gravity="center" android:text="matrix" /> <TextView android:layout_width="300px" android:layout_height="wrap_content" android:gravity="center" android:text="fitXY" /> <TextView android:layout_width="300px" android:layout_height="wrap_content" android:gravity="center" android:text="fitStart" /> <TextView android:layout_width="300px" android:layout_height="wrap_content" android:gravity="center" android:text="fitCenter" /> </TableRow> <TableRow android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="10dp" > <ImageView android:layout_width="300px" android:layout_height="300px" android:background="#aabbcc" android:scaleType="fitEnd" android:src="@drawable/test" /> <ImageView android:layout_width="300px" android:layout_height="300px" android:layout_marginLeft="10dp" android:background="#aabbcc" android:scaleType="center" android:src="@drawable/test" /> <ImageView android:layout_width="300px" android:layout_height="300px" android:layout_marginLeft="10dp" android:background="#aabbcc" android:scaleType="centerCrop" android:src="@drawable/test" /> <ImageView android:layout_width="300px" android:layout_height="300px" android:layout_marginLeft="10dp" android:background="#aabbcc" android:scaleType="centerInside" android:src="@drawable/test" /> </TableRow> <TableRow android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="5dp" > <TextView android:layout_width="300px" android:layout_height="wrap_content" android:gravity="center" android:text="fitEnd" /> <TextView android:layout_width="300px" android:layout_height="wrap_content" android:gravity="center" android:text="center" /> <TextView android:layout_width="300px" android:layout_height="wrap_content" android:gravity="center" android:text="centerCrop" /> <TextView android:layout_width="300px" android:layout_height="wrap_content" android:gravity="center" android:text="centerInside" /> </TableRow> </TableLayout> </ScrollView>
为了快速进入今天文章主题,布局文件并没有按照平时开发中所遵循的Android开发规范来写。布局中使用的是TableLayout标签嵌套TableRow的方式,分成两行,每一行是4列,恰好8种属性可以合理对比查看。当然了这里有更高效的写法来实现同样的效果,比如使用RelativeLayout布局等。
在布局文件中我们定义了每个ImageView的宽高都是固定的300像素,这个尺寸远小于或者是远大于测试的图片尺寸,另外为了便于观察效果我给每个ImageView的背景都设置了颜色,接着运行大图和小图的测试结果,效果如下:
通过上述实验结果对比,就可以得出部分属性的展示效果。比如fitXY属性,当ImageView的属性设置成了fitXY时,图片的宽和高就会相应的拉伸或者是压缩来填充满整个ImageView,注意这种拉放缩不成比例。当ImageView的属性设置成了matrix时,如果图片宽高大于ImageView的宽高时,图片的显示就是从ImageView的左上角开始平铺,超出部分不再显示;如果图片宽高小于ImageView的宽高时,图片的显示也是从ImageView的左上角开始平铺,缺少部分空白显示出来或者是显示ImageView的背景。以上仅仅是根据运行结果来得出的结果,权威结论还要通过查看源码来得出,本文分析的源码是Android2.2版本。
分析ImageView的源码首先从它的构造方法开始,看一下构造方法里边都做了什么工作。构造方法如下:
public ImageView(Context context) { super(context); initImageView(); } public ImageView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ImageView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initImageView(); TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ImageView, defStyle, 0); Drawable d = a.getDrawable(com.android.internal.R.styleable.ImageView_src); if (d != null) { setImageDrawable(d); } ////////////////////////////////////////////////// // // 以下源码部分属性初始化不涉及主核心,不再贴出 // ////////////////////////////////////////////////// a.recycle(); //need inflate syntax/reader for matrix }
通过查看构造方法发现,在三个构造方法中都调用了initImageView()这个方法,这个方法是干嘛使的,我们稍后在看。其次是根据在布局文件中定义的属性来初始化相关属性,在测试布局中我们仅仅是用了ImageView的src,layout_width,layout_height和scaleType属性(background属性暂时忽略)。那也就是说在构造函数的初始化中就是对相关属性进行了赋值操作,通过解析src属性我们获取到了一个Drawable的实例对象d,如果d是非空的话就把d作为参数又调用了setImageDrawable(d)函数,我们看看一下这个函数主要做了什么工作,源码如下:
/** * Sets a drawable as the content of this ImageView. * * @param drawable The drawable to set */ public void setImageDrawable(Drawable drawable) { if (mDrawable != drawable) { mResource = 0; mUri = null; int oldWidth = mDrawableWidth; int oldHeight = mDrawableHeight; updateDrawable(drawable); if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) { requestLayout(); } invalidate(); } }
此方法类型是public的,目的是干嘛使的不再解释了,注释上说的是把给定的drawable作为当前ImageView的展示内容,接着是个条件判断,在刚刚完成初始化的时候mDrawable属性还没有被赋值此时为空,因此判断成立程序进入条件语句继续执行,在这里把属性mResource和mUri归零操作并计入mDrawableWidth和mDrawableHeight的初始值,紧接着把传递进来的drawable参数传递给了updateDrawable()方法,那我们继续跟进updateDrawable()看看这里边又做了什么操作,源码如下:
private void updateDrawable(Drawable d) { if (mDrawable != null) { mDrawable.setCallback(null); unscheduleDrawable(mDrawable); } mDrawable = d; if (d != null) { d.setCallback(this); if (d.isStateful()) { d.setState(getDrawableState()); } d.setLevel(mLevel); mDrawableWidth = d.getIntrinsicWidth(); mDrawableHeight = d.getIntrinsicHeight(); applyColorMod(); configureBounds(); } else { mDrawableWidth = mDrawableHeight = -1; } }
该方法首先进行了非空判断,此时mDrawable的值依然是空,所以条件判断不成立跳过此部分,紧接着把传递进来的非空参数d的字赋值给了属性mDrawable,到这里mDrawable才算是完成了赋值操作。然后又进行了条件判断,并设置d的callback为当前ImageView(因为ImageView的父类View实现了Drawable的Callback接口)接下来又把图片的宽和高分别赋值给了mDrawableWidth和mDrawableHeight,紧接着又调用了applyColorMod()方法,当我们没有给ImageView设置透明度或者是颜色过滤器时该方法不会执行。然后调用configureBounds()方法,此方法是我们今天要讲的和ScaleType属性息息相关的重点,不耽误时间了赶紧瞅一下源码吧,(*^__^*) 嘻嘻……
private void configureBounds() { if (mDrawable == null || !mHaveFrame) { return; } int dwidth = mDrawableWidth; int dheight = mDrawableHeight; int vwidth = getWidth() - mPaddingLeft - mPaddingRight; int vheight = getHeight() - mPaddingTop - mPaddingBottom; boolean fits = (dwidth < 0 || vwidth == dwidth) && (dheight < 0 || vheight == dheight); ////////////////////////////////////////代码块一//////////////////////////////////////// if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) { /* If the drawable has no intrinsic size, or we're told to scaletofit, then we just fill our entire view. */ mDrawable.setBounds(0, 0, vwidth, vheight); mDrawMatrix = null; ////////////////////////////////////////代码块二//////////////////////////////////////// } else { // We need to do the scaling ourself, so have the drawable // use its native size. 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) { // The bitmap fits exactly, no transform needed. mDrawMatrix = null; ////////////////////////////////////////代码块四//////////////////////////////////////// } else if (ScaleType.CENTER == mScaleType) { // Center bitmap in view, no scaling. mDrawMatrix = mMatrix; mDrawMatrix.setTranslate((int) ((vwidth - dwidth) * 0.5f + 0.5f), (int) ((vheight - dheight) * 0.5f + 0.5f)); ////////////////////////////////////////代码块五//////////////////////////////////////// } else if (ScaleType.CENTER_CROP == mScaleType) { mDrawMatrix = mMatrix; 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((int) (dx + 0.5f), (int) (dy + 0.5f)); ////////////////////////////////////////代码块六//////////////////////////////////////// } 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((float) vwidth / (float) dwidth, (float) vheight / (float) dheight); } dx = (int) ((vwidth - dwidth * scale) * 0.5f + 0.5f); dy = (int) ((vheight - dheight * scale) * 0.5f + 0.5f); mDrawMatrix.setScale(scale, scale); mDrawMatrix.postTranslate(dx, dy); ////////////////////////////////////////代码块七//////////////////////////////////////// } else { // Generate the required transform. mTempSrc.set(0, 0, dwidth, dheight); mTempDst.set(0, 0, vwidth, vheight); mDrawMatrix = mMatrix; mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType)); ////////////////////////////////////////代码块八//////////////////////////////////////// } } }
configureBoundd()函数比较长,为了方便分析源码,我把该函数代码分成了8小块,每一个小块都是一个单独的逻辑。每一块的详解如下:
代码块一:
该模块代码首先做了一个条件判断,如果当前mDrawable为空或者是mHaveFrame为false则函数直接返回不再往下执行,由于后边的逻辑主要是根据ScaleType属性的类型来判断图片的展示方式,所以再后来这个函数肯定是能往下走的通的,由于篇幅的原因不再深入讲解该函数的调用时机,我会在之后的文章中专门根据源码讲解一下Android系统下View的绘制流程,在之后的绘制流程中会提到configureBounds()的调用时机。该代码块的逻辑是获取图片的宽高存储在dwidth,dheight中,然后又获取到了ImageView的显示图片区域的宽高存放在vwidth和vheight中。然后定义了一个boolean类型的变量,该变量若为true就表示不需要对图片进行放缩处理。
代码块二:
该代码块的逻辑是当获取到的图片尺寸的宽高未知或者是ImageView的ScaleType属性为FIT_XY时,将mDrawable的显示边界设置成控件ImageView的显示区域大小,并且把mDrawMatrix对象设置成null。需要说明的是setBounds方法用来设置Drawable的绘制区域的,最终在ImageView的onDraw方法中把mDrawable表示的图片绘制到ImageView上。
代码块三:
如果图片宽高不为0并且ImageView的ScaleType属性不是FIT_XY时,就会把mDrawable的绘制区域设置成图片的原始大小。接着进行判断ImageView的ScaleType属性值是否是MATRIX,如果是MATRIX类型,就会把当前mMatrix赋值给mDrawMatrix。
代码块四:
代码块四是一个if(fits)的判断,如果fits的值为true,就表示图片大小等于ImageView的大小,不需要对图片进行放缩处理了。
代码块五:
当mScaleType的类型为CENTER时,实际是将图片进行移位操作,直接点说就是把图片的中心点移动到ImageView的中心点,如果图片的宽高大于ImageView的宽高此时只显示ImageView所包含的部分,大于ImageView的部分不再显示。
【注意:CENTER属性只对图片进行移动操作而不会进行放缩操作】。
代码块六:
代码块六是当mScaleType==CENTER_CROP时,进行了一个条件判断:if(dwidth *vheight >vwidth *dheight),看到这句代码的时候我并没有理解其含义,然后我把这句代码转换了一下写法:if(dwidth / vwidth > dheight / vheight),通过这种转换写法然后再看就比较明白了,主要是用来判断宽高比的,就是说用来判断是图片的宽比较接近ImageView控件的宽还是图片的高比较接近ImageView控件的高。如果是图片的高比较接近ImageView的高,通过计算获取需要放缩的scale的值,再计算出需要对图片的宽进行移动的值,最后通过对mDrawMatrix属性进行设置放缩和移动来达到控制图片进行放缩和移动的效果,同样的逻辑处理了当图片的宽比较接近ImageView的宽的情况。从代码可以总结CENTER_CROP属性的特点是:对图片的宽高进行放缩处理,使一边达到ImageView控件的宽高,另一边进行进行移动居中显示若超出则不再显示。
代码块七:
代码块七是当mScaleType==CENTER_INSIDE时,首先判断图片宽高是否小于ImageView宽高,如果图片宽高小于ImageView的宽高,则scale=1.0f,也就是说不对图片进行放缩处理而是直接移动图片进行居中显示,否则通过Math.min((float)vwidth / (float)dwidth, (float) vheight / (float)dheight);计算出需要对图片进行的放缩值,然后放缩图片宽高并对图片移动居中显示。从代码可以总结CENTER_INSIDE的特点是:控制图片尺寸,对图片宽高进行压缩处理,根据图片和控件的宽高比拿最大的一边进行压缩使之同控件一边相同,另一边小于控件。
代码块八:
代码块八是对mScaleType为FIT_CENTER,FIT_START,FIT_END的情况下统一做了处理,先设置mTempSrc和mTempDst的边界后,通过调用mDrawMatrix的setRectToRect()方法来对图片进行放缩和移动操作,使图片最大边始终等于ImageView相应的边。结合代码和代码测试结果可以得出如下结论:
当图片的高大于宽时:
1.当mScaleType == FIT_START时,对图片进行等比放缩,使图片的高与ImageView的高相等,移动图片使之左对齐。
2.当mScaleType == FIT_CENTER时,对图片进行等比放缩,使图片的高与ImageView的高相等,移动图片使之居中对齐。
3.当mScaleType == FIT_END时,对图片进行等比放缩,使图片的高与ImageView的高相等,移动图片使之右对齐。
当图片的宽大于高时:
1.当mScaleType == FIT_START时,对图片进行等比放缩,使图片的宽与ImageView的宽相等,移动图片使之上对齐。
2.当mScaleType == FIT_CENTER时,对图片进行等比放缩,使图片的宽与ImageView的宽相等,移动图片使之居中对齐。
3.当mScaleType == FIT_END时,对图片进行等比放缩,使图片的宽与ImageView的宽相等,移动图片使之下对齐。
到这里mScaleType的8种用根据法算是分析完了,现在稍做总结:
FIT_XY:对原图宽高进行放缩,该放缩不保持原比例来填充满ImageView。
MATRIX:不改变原图大小从ImageView的左上角开始绘制,超过ImageView部分不再显示。
CENTER:对原图居中显示,超过ImageView部分不再显示。
CENTER_CROP:对原图居中显示后进行等比放缩处理,使原图最小边等于ImageView的相应边。
CENTER_INSIDE:若原图宽高小于ImageView宽高,这原图不做处理居中显示,否则按比例放缩原图宽(高)是之等于ImageView的宽(高)。
FIT_START:对原图按比例放缩使之等于ImageView的宽高,若原图高大于宽则左对齐否则上对其。
FIT_CENTER:对原图按比例放缩使之等于ImageView的宽高使之居中显示。
FIT_END:对原图按比例放缩使之等于ImageView的宽高,若原图高大于宽则右对齐否则下对其。
还记得在博文开始的时候说到在ImageView的构造方法中都调用了initImageView()方法么?他的源码如下:
private void initImageView() { mMatrix = new Matrix(); mScaleType = ScaleType.FIT_CENTER; }
可以看到,当我们没有在布局文件中使用scaleType属性或者是没有手动调用setScaleType方法时,那么mScaleType的默认值就是FIT_CENTER。
好了,有关ImageView的ScaleType的讲解就算结束了,如有错误欢迎指正。以后如有其它属性需要详解,再做记录吧。
原文地址:http://blog.csdn.net/llew2011/article/details/50855655
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。