【Android】ImageView源码简析
引文
这里是【重要声明】:
首先非常非常非常感谢您能阅读这篇文章,重要的谢谢当然是说三遍。
【1】因为Android系统中与View体系相关的内容较为复杂庞大,而一篇文章根本不可能讲述完所有的要点,因此文章中对于某些属性细节仅为介绍,而其实现以及具体的使用与分析等引用了【其它博主】的相应【博文】,借此希望能略微免除读者再额外搜索查阅的烦恼。在此对相应文章的作者表示感谢,由于没有预先与相关博主进行沟通联系,如若侵犯了您的权益,请您联系我,立刻删除相应的链接。再次表示感谢。
【2】文中有些方法内容的简析在可能我们平时编程时基本不会调用,但知其然与知其所以然应该能帮助我们更好的理解ImageView。
【3】作者水平有限,文章会有很多Bug,希望小伙伴们多多批评,我将及时学习改正,我们大家共同学习。
ImageView是Android开发中常用的图像显示控件。网上有很多关于其某某具体属性分析的文章,但是搜索来搜索去都没有发现相应的总结。因此希望借这篇文章将其代码简要的分析一下。站在前人的肩膀上,也不能总是吃老本啊,总归也是要拓展一下的。
本次源码的分析建立在Android API 25的基础上,毕竟截止到写作时传说中Android O(26)还只是beta_4版本啊。
我们先忽略它的直接与间接子类,只看继承关系。首先,通过官网的解释(如下图):
可以很清楚看到ImageView直接继承于View。
一、内部成员变量及其枚举类与构造器简析
1、其成员变量(与相应注释)主要如下:
private Uri mUri;
private int mResource = 0;
//【ImageView】中利用【矩阵】来处理缩放等操作
private Matrix mMatrix;
//缩放类型
private ScaleType mScaleType;
private boolean mHaveFrame = false;
private boolean mAdjustViewBounds = false;
private int mMaxWidth = Integer.MAX_VALUE;
private int mMaxHeight = Integer.MAX_VALUE;
//以下变量与显示的Drawable有关
private ColorFilter mColorFilter = null; //色彩滤镜
private boolean mHasColorFilter = false;//是否含有色彩滤镜
private Xfermode mXfermode;//图像混合模式
private int mAlpha = 255;//透明度
private final int mViewAlphaScale = 256;
private boolean mColorMod = false;
private Drawable mDrawable = null;
private BitmapDrawable mRecycleableBitmapDrawable = null;
private ColorStateList mDrawableTintList = null;//着色列表
private PorterDuff.Mode mDrawableTintMode = null;//图像(渲染)混合
private boolean mHasDrawableTint = false;
private boolean mHasDrawableTintMode = false;
private int[] mState = null;//图像当前的状态
private boolean mMergeState = false;
private int mLevel = 0;
[一篇关于Level的博文](http://blog.csdn.net/zhangphil/article/details/48936209)
private int mDrawableWidth;
private int mDrawableHeight;
private Matrix mDrawMatrix = null;//【图像】矩阵
// 以下两个RectF同样在图像缩放时调用,具体见下文
private final RectF mTempSrc = new RectF();
private final RectF mTempDst = new RectF();
private boolean mCropToPadding;//是否【裁剪】图片适应【内边距】
private int mBaseline = -1;//视图内,基线的偏移量
private boolean mBaselineAlignBottom = false;
//兼容性模式依赖于程序的targetSdkVersion
private static boolean sCompatDone;
//对于老版本的APP,AdjustViewBounds behavior将处于兼容模式
private static boolean sCompatAdjustViewBounds;
//是否在从stream中创建对象时传递资源
private static boolean sCompatUseCorrectStreamDensity;
// 当前版本小于24时,初始化此变量为true否则为false,与是否可见(visable)有关
private static boolean sCompatDrawableVisibilityDispatch;
2、重要的内部枚举类
说到ImageView的成员变量,不得不说其内部一个重要的枚举类 ===> ScaleType。
看名字就可以直截了当地知道Scale(比例)Type(类型)。这个类是用来调整图片的比例(缩放)类型,决定了图片在View上显示时的样子,如进行何种比例的缩放,及显示图片的整体还是部分
其内部共有8个变量。分别是:
属性名称 | 作用 |
---|---|
matrix | 用矩阵来绘制(从左上角起始的矩阵区域) |
fitXY | 把图片不按比例扩大/缩小到View的大小显示(确保图片会完整显示,并充满View) |
fitStart | 把图片按比例扩大/缩小到View的宽度,显示在View的上部分位置(图片会完整显示) |
fitCenter | 把图片按比例扩大/缩小到View的宽度,居中显示(图片会完整显示) |
fitEnd | 把图片按比例扩大/缩小到View的宽度,显示在View的下部分位置(图片会完整显示) |
center | 按图片的原来size居中显示,当图片宽超过View的宽,则截取图片的居中部分显示,当图片宽小于View的宽,则图片居中显示 |
centerCrop | 按比例扩大/缩小图片的size居中显示,使得图片的高等于View的高,使得图片宽等于或大于View的宽 |
centerInside | 将图片的内容完整居中显示,使得图片按比例缩小或原来的大小(图片比View小时)使得图片宽等于或小于View的宽 (图片会完整显示) |
阅读源码可以发现,缩放类型主要在调用setScaleType()方法时设定。
public void setScaleType(ScaleType scaleType) {
if (scaleType == null) {
throw new NullPointerException();
}
if (mScaleType != scaleType) {
//根据传入的值,设置当前的缩放类型
mScaleType = scaleType;
//View中的方法。缓存机制,(imageview在此跳过)避免浪费内存
setWillNotCacheDrawing(mScaleType == ScaleType.CENTER);
//请求父View重新发起measure,layout,draw等流程进行重绘
requestLayout();
//使整个视图无效,重新绘制。(注意只能在UI线程调用)
invalidate();
}
}
3、ImageView构造器
在构造器中执行的第一个方法是initImageView(),源码如下:
private void initImageView() {
mMatrix = new Matrix();
mScaleType = ScaleType.FIT_CENTER;
if (!sCompatDone) {
final int targetSdkVersion = mContext.getApplicationInfo().targetSdkVersion;
sCompatAdjustViewBounds = targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR1;
sCompatUseCorrectStreamDensity = targetSdkVersion > Build.VERSION_CODES.M;
sCompatDrawableVisibilityDispatch = targetSdkVersion < Build.VERSION_CODES.N;
sCompatDone = true;
}
}
阅读代码可以发现,与名称一致,这个方法主要执行部分初始化操作。诸如图像(缩放)矩阵的初始化以及初始缩放类型的设置,很明显,初始缩放类型为【ScaleType.FIT_CENTER】,即完整地居中显示图像。然后进行了部分兼容性适配方面的任务如设置目标SDK版本等。
接下来,通过获取资源数组TypedArray,进行相应的操作。
//获取资源数组
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ImageView, defStyleAttr, defStyleRes);
//获取资源中的初始Drawable
final Drawable d = a.getDrawable(R.styleable.ImageView_src);
if (d != null) {
//将获取的Drawable,设置为ImageView的显示内容。
setImageDrawable(d);
}
//初始化基线
mBaselineAlignBottom = a.getBoolean(R.styleable.ImageView_baselineAlignBottom, false);
mBaseline = a.getDimensionPixelSize(R.styleable.ImageView_baseline, -1);
//初始化边界的自适应
setAdjustViewBounds(a.getBoolean(R.styleable.ImageView_adjustViewBounds, false));
//初始化最大宽、高
setMaxWidth(a.getDimensionPixelSize(R.styleable.ImageView_maxWidth, Integer.MAX_VALUE));
setMaxHeight(a.getDimensionPixelSize(R.styleable.ImageView_maxHeight, Integer.MAX_VALUE));
//初始化缩放类型
final int index = a.getInt(R.styleable.ImageView_scaleType, -1);
if (index >= 0) {
setScaleType(sScaleTypeArray[index]);
}
[Android中Tint属简介](http://blog.csdn.net/zhuoxiuwu/article/details/50976337)
if (a.hasValue(R.styleable.ImageView_tint)) {
mDrawableTintList = a.getColorStateList(R.styleable.ImageView_tint);
mHasDrawableTint = true;
mDrawableTintMode = PorterDuff.Mode.SRC_ATOP;
mHasDrawableTintMode = true;
}
if (a.hasValue(R.styleable.ImageView_tintMode)) {
mDrawableTintMode = Drawable.parseTintMode(a.getInt(
R.styleable.ImageView_tintMode, -1), mDrawableTintMode);
mHasDrawableTintMode = true;
}
//初始化ImageView中Drawable的着色
applyImageTint();
//初始化透明度
final int alpha = a.getInt(R.styleable.ImageView_drawableAlpha, 255);
if (alpha != 255) {
setImageAlpha(alpha);
}
mCropToPadding = a.getBoolean(
R.styleable.ImageView_cropToPadding, false);
//☆☆☆特别注意:资源数组必须在结束使用后释放
a.recycle();
其中在applyImageTint()方法中,设置Drawable的图像【着色】属性。
(Tint 这个属性是Android L以后添加的,附上一篇关于Tint的解析博文)
private void applyImageTint() {
if (mDrawable != null && (mHasDrawableTint || mHasDrawableTintMode)) {
mDrawable = mDrawable.mutate();
if (mHasDrawableTint) {
mDrawable.setTintList(mDrawableTintList);
}
if (mHasDrawableTintMode) {
mDrawable.setTintMode(mDrawableTintMode);
}
//Drawable在应用着色之前可能没有完成状态的设置,这里进行重新设置、
if (mDrawable.isStateful()) {
mDrawable.setState(getDrawableState());
}
}
}
上述即为ImageView的构造器简析。
二、部分重写的方法简析
1、有关View的测量、布局与绘制的方法
ps:见【Android_View】ImageView源码简析笔记(二)
2、其它方法(一)
1)Drawable确认
@Override
protected boolean verifyDrawable(@NonNull Drawable dr) {
return mDrawable == dr || super.verifyDrawable(dr);
}
这个方法是重写了View中方法,原始注释翻译为:如果您的视图子类要显示自己的Drawable对象,那么它应该覆盖重写此函数,并为其显示的任何Drawable返回true。 这样可以安排这些可绘制的动画。
我们看一下View中的原始方法:
@CallSuper
protected boolean verifyDrawable(@NonNull Drawable who) {
//我们应该避免验证与滚动条【scroll bar】有关的内容,以免陷入无效的循环。
return who == mBackground || (mForegroundInfo != null && mForegroundInfo.mDrawable == who);
}
按照原方法的注释,这个方法用来验证当前显示的Drawable是否是我们希望ImageView显示的图像,如果是则返回true,否则返回调用父类的相应方法后的结果(很明显,此处的调用方为继承View的子类,而父类指的是View本身)。
2)Drawable状态切换
@Override
public void jumpDrawablesToCurrentState() {
super.jumpDrawablesToCurrentState();
if (mDrawable != null) mDrawable.jumpToCurrentState();
}
该方法调用了Drawable中的jumpToCurrentState(),代码如下:
/**
* If this Drawable does transition animations between states, ask that it immediately jump to the current state and skip any active animations.
*/
public void jumpToCurrentState() {
}
可以发现,方法体内并没有相应的执行语句,注释的意思为:如果当前的Drawable在两个状态转换中执行了相应的动画,则请求立刻跳转到指定的状态并略过动画。
3)重绘选中的Drawable
@Override
public void invalidateDrawable(@NonNull Drawable dr) {
if (dr == mDrawable) {
if (dr != null) {
// 更新缓存信息
final int w = dr.getIntrinsicWidth();
final int h = dr.getIntrinsicHeight();
if (w != mDrawableWidth || h != mDrawableHeight) {
mDrawableWidth = w;
mDrawableHeight = h;
// 更新依赖于边界信息的矩阵
configureBounds();
}
}
//在这种情况下,我们可以使得整个视图无效,因为很难知道drawable实际上在哪里。这是因为应用的偏移和变换操作等使之变得复杂。理论上,我们可以得到drawable的边界等,并通过转换和补偿等来操作它们,但这些并不值得我们这么做。
invalidate();
} else {
super.invalidateDrawable(dr);
}
}
这个方法主要用来重新绘制Drawable。我们知道,视图的绘制需要完成测量、布局、绘制三个步骤,很明显requestLayout()方法的调用是必须执行上述三个步骤的,而invalidate()方法只能在Draw流程中起作用,而不会完成先前的测量与布局流程。
由于ImageView中invalidate()的实现主要调用View的相应方法,因此我们直接看View.invalidate():
/**
This is where the invalidate() work actually happens. A full invalidate() causes the drawing cache to be invalidated, but this function can be called with invalidateCache set to false to skip that invalidation step for cases that do not need it (for example, a component that remains at the same dimensions with the same content).
*/
void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
根据注释我们可以知道,在视图重绘实际中起作用的就是上述方法。在调用该方法时,如果传入true,则进行完整的重绘,也就是使得绘图缓存无效;而传入false,则会保留住缓存内容。
而通过ImageView的源码,我们可以发现,此处传入了true,即完全重绘。
public void invalidate() {
invalidate(true);
}
此方法主要在drawableStateChanged()中调用。
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
final Drawable drawable = mDrawable;
if (drawable != null && drawable.isStateful()
&& drawable.setState(getDrawableState())) {
invalidateDrawable(drawable);
}
}
4)判断是否有重叠
@Override
public boolean hasOverlappingRendering() {
return (getBackground() != null && getBackground().getCurrent() != null);
}
自定义 View 时重写 hasOverlappingRendering ()指定 View 是否有 Overlapping 的情况,提高渲染性能。
上述getBackground()实质调用了View中的方法,返回对象亦为Drawable。
/**
* Gets the background drawable
* @return The drawable used as the background for this view, if any.
* @see #setBackground(Drawable)
* @attr ref android.R.styleable#View_background
*/
public Drawable getBackground() {
return mBackground;
}
5)辅助功能
/** @hide */
@Override
public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
super.onPopulateAccessibilityEventInternal(event);
final CharSequence contentDescription = getContentDescription();
if (!TextUtils.isEmpty(contentDescription)) {
event.getText().add(contentDescription);
}
}
附一篇文章,有关于Android辅助功能
6)获取与设置 是否适应边界
public boolean getAdjustViewBounds() {
return mAdjustViewBounds;
}
@android.view.RemotableViewMethod
public void setAdjustViewBounds(boolean adjustViewBounds) {
mAdjustViewBounds = adjustViewBounds;
if (adjustViewBounds) {
setScaleType(ScaleType.FIT_CENTER);
}
}
很明显,如果希望ImageView调整其边界以保持drawable的宽高比,则将将其设置为true。
可以看到,如果传入true值,会将缩放状态设置为ScaleType.FIT_CENTER。
7)获取 与 设置 视图的最大宽度MaxHeight
@android.view.RemotableViewMethod
public void setMaxWidth(int maxWidth) {
mMaxWidth = maxWidth;
}
public int getMaxHeight() {
return mMaxHeight;
}
对上述方法,官方的注释为:
为此视图提供最大宽度的可选参数。 只有{@link #setAdjustViewBounds(boolean)}设置为true时才有效。
要将图像设置为最大为100 x 100,同时保留原始宽高比,请执行以下操作:
1)将adjustViewBounds设置为true
2)将maxWidth和maxHeight设置为100
3)将高度和宽度布局参数设置为WRAP_CONTENT。
(最大高度MaxHeight 的操作与之类似)