Android Drawable完全解析(一):Drawable源码分析(上) Android Drawable完全解析(一):Drawable源码分析(中) Android Drawable完全解析(一):Drawable源码分析(下)
昨天下班前,分析了View实例将Drawable作为背景绘制到屏幕上面的流程,今天继续分析Drawable在ImageView中的绘制流程!
####3:Drawable绘制流程 ####3.3:Drawable在ImageView中的绘制流程 ImageView使用Drawable的方式大体以下几种:
- 在xml中直接设置android:background="@mipmap/voice" <ImageView android:layout_width="200dp" android:layout_height="100dp" android:background="@mipmap/voice" />
- 在xml中直接设置android:src="@mipmap/voice" <ImageView android:layout_width="200dp" android:layout_height="100dp" android:src="@mipmap/voice" />
- Java代码中调用 setImageResource(@DrawableRes int resId)
- Java代码中调用 setImageDrawable(@Nullable Drawable drawable)
- Java代码中调用 setBackgroundDrawable,实质是调用View.setBackgroundDrawable,上篇文章已分析。
下面就这几种方式逐一分析: 首先上原图:
####3.3.1:android:background="@mipmap/voice"
1 <ImageView 2 android:layout_width="200dp" 3 android:layout_height="100dp" 4 android:background="@mipmap/voice" 5 />
实际效果: 
可见直接使用android:background,图片作为背景完全铺满ImageView尺寸,会根据ImageView的范围缩放。 既然在xml中布局ImageView,那么肯定是调用ImageView(Context context, @Nullable AttributeSet attrs),看一下关键代码:
1public class ImageView extends View { 2 public ImageView(Context context, @Nullable AttributeSet attrs) { 3 this(context, attrs, 0); 4 } 5**** 6 public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, 7 int defStyleRes) { 8 //调用View的构造函数 9 super(context, attrs, defStyleAttr, defStyleRes); 10 **** 11 } 12} 13一路追踪下去: 14public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource { 15 public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { 16 this(context); 17 final TypedArray a = context.obtainStyledAttributes( 18 attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes); 19 **** 20 Drawable background = null; 21 **** 22 final int N = a.getIndexCount(); 23 for (int i = 0; i < N; i++) { 24 int attr = a.getIndex(i); 25 switch (attr) { 26 //获取在xml中设置的android:background="@mipmap/voice" 27 case com.android.internal.R.styleable.View_background: 28 background = a.getDrawable(attr); 29 break; 30 ***** 31 } 32 } 33 **** 34 if (background != null) { 35 setBackground(background); 36 } 37 **** 38 } 39 public void setBackground(Drawable background) { 40 //[Android Drawable完全解析(一):Drawable源码分析(中)](http://www.jianshu.com/p/2213c62e4738) 41 setBackgroundDrawable(background); 42 } 43}
可见: 在xml中直接设置android:background="@mipmap/voice"实质是通过调用View.setBackgroundDrawable(Drawable background)将图片绘制到屏幕上!
View.setBackgroundDrawable(Drawable background)在上一篇文章:Android Drawable完全解析(一):Drawable源码分析(中)有过分析! 为什么背景图会铺满整个ImageView,是因为在View绘制过程中,将背景Drawable的绘制范围设置为和View的尺寸一致:
1 void setBackgroundBounds() { 2 if (mBackgroundSizeChanged && mBackground != null) { 3 mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop); 4 mBackgroundSizeChanged = false; 5 rebuildOutline(); 6 } 7 }
####3.3.2:android:src="@mipmap/voice"
1 <ImageView 2 android:layout_width="200dp" 3 android:layout_height="100dp" 4 android:src="@mipmap/voice" 5 />
实际效果: 
可见直接使用android:src,默认情况下图片会根据ImageView的尺寸在保留自身宽高比例下进行缩放,最后在ImageView的中心显示。 既然在xml中布局ImageView,那么肯定是调用ImageView(Context context, @Nullable AttributeSet attrs),同样看一下关键代码:
1public class ImageView extends View { 2 public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, 3 int defStyleRes) { 4 //super上面分析过了,绘制的是android:background="@mipmap/voice" 5 super(context, attrs, defStyleAttr, defStyleRes); 6 //主要设置了 ImageView实例中图像边界 与 ImageView边界间的缩放关系 7 initImageView(); 8 final TypedArray a = context.obtainStyledAttributes( 9 attrs, R.styleable.ImageView, defStyleAttr, defStyleRes); 10 //将xml中使用android:src="@mipmap/voice"设置的图片生成Drawable实例 11 final Drawable d = a.getDrawable(R.styleable.ImageView_src); 12 if (d != null) { 13 //将src生成的Drawable实例设置为ImageView的内容 14 setImageDrawable(d); 15 } 16 **** 17 //在我们的例子中,没有设置scaleType属性,则index = -1; 18 final int index = a.getInt(R.styleable.ImageView_scaleType, -1); 19 if (index >= 0) { 20 //在我们例子中,index = -1,下面代码不执行 21 setScaleType(sScaleTypeArray[index]); 22 } 23 //解析在xml中设置的tint和tintMode属性值 24 if (a.hasValue(R.styleable.ImageView_tint)) { 25 mDrawableTintList = a.getColorStateList(R.styleable.ImageView_tint); 26 mHasDrawableTint = true; 27 // Prior to L, this attribute would always set a color filter with 28 // blending mode SRC_ATOP. Preserve that default behavior. 29 mDrawableTintMode = PorterDuff.Mode.SRC_ATOP; 30 mHasDrawableTintMode = true; 31 } 32 if (a.hasValue(R.styleable.ImageView_tintMode)) { 33 mDrawableTintMode = Drawable.parseTintMode(a.getInt( 34 R.styleable.ImageView_tintMode, -1), mDrawableTintMode); 35 mHasDrawableTintMode = true; 36 } 37 //根据当前ImageView的ColorStateList对 通过src生成的Drawable实例进行着色 38 applyImageTint(); 39 final int alpha = a.getInt(R.styleable.ImageView_drawableAlpha, 255); 40 //设置透明度 41 if (alpha != 255) { 42 setImageAlpha(alpha); 43 } 44 mCropToPadding = a.getBoolean( 45 R.styleable.ImageView_cropToPadding, false); 46 a.recycle(); 47 //need inflate syntax/reader for matrix 48 } 49 private void initImageView() { 50 **** 51 //设置mScaleType = ScaleType.FIT_CENTER;可见ImageView中 52 //mScaleType默认就是ScaleType.FIT_CENTER 53 mScaleType = ScaleType.FIT_CENTER; 54 **** 55 } 56 public void setImageDrawable(@Nullable Drawable drawable) { 57 if (mDrawable != drawable) { 58 **** 59 //对src生成的Drawable实例设置一系列属性 60 updateDrawable(drawable); 61 **** 62 //最后调用invalidate()触发draw 63 invalidate(); 64 } 65 } 66 private void updateDrawable(Drawable d) { 67 **** 68 //将ImageView实例之前关联的Drawable实例的动画监听移除, 69 //并停止其已经在执行的动画,解除其所有事件 70 if (mDrawable != null) { 71 mDrawable.setCallback(null); 72 unscheduleDrawable(mDrawable); 73 if (isAttachedToWindow()) { 74 mDrawable.setVisible(false, false); 75 } 76 } 77 //将mDrawable赋值为通过src属性生成的Drawable实例 78 mDrawable = d; 79 if (d != null) { 80 //为通过src生成的Drawable实例设置动画监听为ImageView实例自身; 81 //并设置其布局方向,状态数组,Drawable动画是否开启,Drawable的level值。 82 d.setCallback(this); 83 d.setLayoutDirection(getLayoutDirection()); 84 if (d.isStateful()) { 85 d.setState(getDrawableState()); 86 } 87 if (isAttachedToWindow()) { 88 d.setVisible(getWindowVisibility() == VISIBLE && isShown(), true); 89 } 90 d.setLevel(mLevel); 91 mDrawableWidth = d.getIntrinsicWidth(); 92 mDrawableHeight = d.getIntrinsicHeight(); 93 //根据当前ImageView实例的ColorStateList对其进行着色 94 applyImageTint(); 95 //未执行实质代码 96 applyColorMod(); 97 //设置Drawable实例的绘制范围不变,并根据ImageView实例内容区域和 98 //Drawable实例原始绘制范围,确定Drawable实例在实际绘制时候的缩放。 99 configureBounds(); 100 } else { 101 mDrawableWidth = mDrawableHeight = -1; 102 } 103 } 104 private void applyImageTint() { 105 **** 106 //根据当前ImageView的ColorStateList对 通过src生成的Drawable实例进行着色 107 mDrawable.setTintList(mDrawableTintList); 108 **** 109 } 110 private void applyColorMod() { 111 //对应通过 src生成的Drawble实例来说,ImageView并未调用setColorFilter 112 //mColorMod也为默认的false值,所以下面代码实质未执行 113 if (mDrawable != null && mColorMod) { 114 mDrawable = mDrawable.mutate(); 115 //如果当前ImageView实例调用过setColorFilter, 116 //则对 通过src生成的Drawable实例设置相同的ColorFilter 117 if (mHasColorFilter) { 118 mDrawable.setColorFilter(mColorFilter); 119 } 120 mDrawable.setXfermode(mXfermode); 121 mDrawable.setAlpha(mAlpha * mViewAlphaScale >> 8); 122 } 123 } 124 private void configureBounds() { 125 //通过src生成的Drawable实例 原始宽高 126 final int dwidth = mDrawableWidth; 127 final int dheight = mDrawableHeight; 128 //ImageView实例的内容区域宽高(去除了padding值) 129 final int vwidth = getWidth() - mPaddingLeft - mPaddingRight; 130 final int vheight = getHeight() - mPaddingTop - mPaddingBottom; 131 **** 132 if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) { 133 //当ImageView设置过android:scaleType="fitXY" 或setScaleType(ScaleType.FIT_XY), 134 //则将此Drawable实例的绘制范围设定为ImageView实例的内容区域 135 mDrawable.setBounds(0, 0, vwidth, vheight); 136 mDrawMatrix = null; 137 } else { 138 //对应我们例子中,未设置android:scaleType情况下, 139 //通过src生成的Drawable实例的绘制范围就是其原始范围 140 mDrawable.setBounds(0, 0, dwidth, dheight); 141 //下面代码设置了mDrawMatrix的属性 142 //在initImageView()方法中已知: 143 //ImageView中mScaleType默认就是ScaleType.FIT_CENTER 144 if (ScaleType.MATRIX == mScaleType) { 145 **** 146 } else if (fits) { 147 **** 148 } else if (ScaleType.CENTER == mScaleType) { 149 **** 150 } else if (ScaleType.CENTER_CROP == mScaleType) { 151 **** 152 } else if (ScaleType.CENTER_INSIDE == mScaleType) { 153 **** 154 } else { 155 //ImageView中mScaleType默认就是ScaleType.FIT_CENTER 156 //则根据ImageView实例内容区域的范围和Drawable实例实际宽高来设置mDrawMatrix 157 mTempSrc.set(0, 0, dwidth, dheight); 158 mTempDst.set(0, 0, vwidth, vheight); 159 mDrawMatrix = mMatrix; 160 mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType)); 161 } 162 } 163 } 164} 165 166ImageView实例生成后,肯定还是执行onDraw方法将自身绘制到屏幕上,继续追踪代码: 167 168public class ImageView extends View { 169 @Override 170 protected void onDraw(Canvas canvas) { 171 super.onDraw(canvas); 172 **** 173 //在上面分析过 mDrawMatrix不为null 174 //mDrawMatrix的属性根据ImageView实例内容区域的范围和Drawable实例实际宽高来配置 175 if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) { 176 //如果矩阵mDrawMatrix为空,且ImageView的上下padding值都为0 177 //则直接将Drawable实例绘制到画布上 178 mDrawable.draw(canvas); 179 } else { 180 **** 181 //我们例子中,矩阵mDrawMatrix不为空,则将其设置到ImageView的画布上 182 if (mDrawMatrix != null) { 183 canvas.concat(mDrawMatrix); 184 } 185 //然后在画布上面绘制Drawable实例 186 mDrawable.draw(canvas); 187 canvas.restoreToCount(saveCount); 188 } 189 } 190}
至此, android:src="@mipmap/voice"整个流程就分析完了,流程总结如下:
在ImageView构造函数中:
1:设置缩放类型默认为 ScaleType.FIT_CENTER(图像居中等比例缩放) 2:在ImageView构造函数中,解析xml中android:src属性获取Drawable实例; 3:为生成的Drawable实例设置一系列属性:
- 设置动画监听为ImageView实例自身:d.setCallback(this);
- 设置布局方向和ImageView实例一致:d.setLayoutDirection(getLayoutDirection());
- 设置状态数组和ImageView实例一致:d.setState(getDrawableState());
- 设置动画是否可见和 ImageView可见性一致:d.setVisible(getWindowVisibility() == VISIBLE && isShown(), true);
- 设置动画当前Level值和ImageView的mLevel值一致:d.setLevel(mLevel);
- 根据当前ImageView实例的ColorStateList对其进行着色:applyImageTint();
- 设置绘制范围为原始绘制范围setBounds 且 根据ImageView、Drawable实例的范围 和 缩放类型 来设置Matrix mDrawMatrix(用于onDraw):configureBounds();
4:如果我们在xml中还设置了缩放类型,着色,着色模式,透明度, 则为mScaleType重新赋值,并为生成的Drawable实例逐一设置着色,着色模式,透明度
在ImageView的onDraw方法中:
1:如果矩阵mDrawMatrix为空,且ImageView的上下padding值都为0,则直接将Drawable实例绘制到画布上 2:其余情况下:
- 如果矩阵mDrawMatrix不为空,则将其设置到ImageView的画布上;
- 然后在画布上面绘制Drawable实例
本质上还是执行了Drawable.draw(@NonNull Canvas canvas)将src生成的Drawable实例绘制到ImageView实例所在的画布
####3.3.3:setImageDrawable(@Nullable Drawable drawable) setImageDrawable在上面分析过程中出现过
1 public void setImageDrawable(@Nullable Drawable drawable) { 2 if (mDrawable != drawable) { 3 mResource = 0; 4 mUri = null; 5 final int oldWidth = mDrawableWidth; 6 final int oldHeight = mDrawableHeight; 7 updateDrawable(drawable); 8 if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) { 9 requestLayout(); 10 } 11 //invalidate会引发重绘,调用onDraw方法,直接看上面onDraw的流程分析即可 12 invalidate(); 13 } 14 }
####3.3.4:setImageResource(@DrawableRes int resId)
1 public void setImageResource(@DrawableRes int resId) { 2 **** 3 //在updateDrawable(Drawable d)中:mDrawable = d; 4 //此处将mDrawable重置为null 5 updateDrawable(null); 6 //为mResource赋值为传入的资源ID,mUri重置为null 7 mResource = resId; 8 mUri = null; 9 resolveUri(); 10 **** 11 //引发重绘 12 invalidate(); 13 } 14 private void resolveUri() { 15 //updateDrawable(null)已经将mDrawable重置为null 16 if (mDrawable != null) { 17 return; 18 } 19 if (getResources() == null) { 20 return; 21 } 22 Drawable d = null; 23 //在中setImageResource(@DrawableRes int resId)已知:mResource = resId; 24 if (mResource != 0) { 25 //通过setImageResource传入的resId通常不为0,执行如下: 26 try { 27 //通过传入的图片资源ID获取Drawable实例 28 d = mContext.getDrawable(mResource); 29 } catch (Exception e) { 30 Log.w(LOG_TAG, "Unable to find resource: " + mResource, e); 31 // Don't try again. 32 mUri = null; 33 } 34 } else if (mUri != null) { 35 d = getDrawableFromUri(mUri); 36 if (d == null) { 37 Log.w(LOG_TAG, "resolveUri failed on bad bitmap uri: " + mUri); 38 // Don't try again. 39 mUri = null; 40 } 41 } else { 42 return; 43 } 44 //将通过传入的图片资源ID生成的Drawable实例作为参数, 45 //调用updateDrawable,上面已经分析过此方法 46 updateDrawable(d); 47 }
setImageResource(@DrawableRes int resId)整个流程就分析完了,流程总结如下:
- 1:首先执行updateDrawable(null),将已经绘制完毕的mDrawable动画停止,移除所有事件及动画监听,并重置为null
- 2:用Resource实例通过resId获取Drawable实例,作为参数执行updateDrawable
- 3:ImageView实例执行重绘,详见之前onDraw的分析
至此,Drawable在ImageView中的绘制流程就分析完毕了!Drawable源码分析也告一段落,如有错误或者翻译问题请各位大神留言!
'Android Drawable完全解析' 系列未完待续...