- package de.hdodenhof.circleimageview;
- public class CircleImageView extends ImageView {
-
- private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP;//决定了图片在View上显示时的样子,如进行何种比例的缩放,及显示图片的整体还是部分,等等
-
- private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
- private static final int COLORDRAWABLE_DIMENSION = 2;
-
- private static final int DEFAULT_BORDER_WIDTH = 0;//默然图片的边围圆圈线的宽度
- private static final int DEFAULT_BORDER_COLOR = Color.BLACK;//默认图片圆圈的颜色
- private static final int DEFAULT_FILL_COLOR = Color.TRANSPARENT;//圆圈背景的填充颜色
- private static final boolean DEFAULT_BORDER_OVERLAY = false;//
-
- private final RectF mDrawableRect = new RectF();//我们的view的大小是一定的,用户通过layout_width属性指定,用户也有可能设置padding属性,这就会要求我们的圆形头像
- //要距离这个CircleImageView的边距是padding值,也就是说圆形图片需要画在那个位置是不确定的,需要CircleImageView的宽高,以及设置padding属性来定位。我们使用一个矩形Recf来表示这个CircleImageView的剩余有效区域
- //并且将会在这个有效区域内部画出一个圆形
-
- private final RectF mBorderRect = new RectF();//为圆形头像画出一个边围圆圈,mBorderRect用于记录该圆圈的矩形位置,这个矩形的位置和上面图片矩形位置是一样的。
-
- private final Matrix mShaderMatrix = new Matrix();
- private final Paint mBitmapPaint = new Paint();//图像画笔
- private final Paint mBorderPaint = new Paint();//边围圆圈的画笔
- private final Paint mFillPaint = new Paint();//图片底部的view(CircleImageView)的填充颜色,也就是圆形头像的背景填充色
-
- private int mBorderColor = DEFAULT_BORDER_COLOR;
- private int mBorderWidth = DEFAULT_BORDER_WIDTH;
- private int mFillColor = DEFAULT_FILL_COLOR;
-
- private Bitmap mBitmap;
- private BitmapShader mBitmapShader;
- private int mBitmapWidth;
- private int mBitmapHeight;
-
- private float mDrawableRadius;
- private float mBorderRadius;
-
- private ColorFilter mColorFilter;
-
- private boolean mReady;
- private boolean mSetupPending;
- private boolean mBorderOverlay ;
- private boolean mDisableCircularTransformation;
-
- public CircleImageView(Context context) {
- super(context);
-
- init();
- }
-
- public CircleImageView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public CircleImageView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- System.out.println("CircleImageView(Context context, AttributeSet attrs, int defStyle" );
-
-
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyle, 0);
- mBorderWidth = a.getDimensionPixelSize(R.styleable.CircleImageView_civ_border_width, DEFAULT_BORDER_WIDTH);
- mBorderColor = a.getColor(R.styleable.CircleImageView_civ_border_color, DEFAULT_BORDER_COLOR);
- mBorderOverlay = a.getBoolean(R.styleable.CircleImageView_civ_border_overlay, DEFAULT_BORDER_OVERLAY);
- mFillColor = a.getColor(R.styleable.CircleImageView_civ_fill_color, DEFAULT_FILL_COLOR);
-
- a.recycle();
-
- init();
- }
-
- private void init() {
- System.out.println("init ");
- super.setScaleType(SCALE_TYPE);
- mReady = true;
-
- if (mSetupPending) {
- setup();
- mSetupPending = false;
- }
- }
-
- @Override
- public ScaleType getScaleType() {
- return SCALE_TYPE;
- }
-
- @Override
- public void setScaleType(ScaleType scaleType) {
- if (scaleType != SCALE_TYPE) {
- throw new IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType));
- }
- }
-
- @Override
- public void setAdjustViewBounds(boolean adjustViewBounds) {
- if (adjustViewBounds) {
- throw new IllegalArgumentException("adjustViewBounds not supported.");
- }
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- System.out.println("onDraw(Canvas canvas) ");
- if (mDisableCircularTransformation) {
- super.onDraw(canvas);
- return;
- }
-
- if (mBitmap == null) {
- return;
- }
-
- if (mFillColor != Color.TRANSPARENT) {
- canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mFillPaint);
- }
- canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint);
- if (mBorderWidth != 0) {
- canvas.drawCircle(mBorderRect.centerX(), mBorderRect.centerY(), mBorderRadius, mBorderPaint);
- }
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- setup();
- }
-
- public int getBorderColor() {
- return mBorderColor;
- }
-
- public void setBorderColor(@ColorInt int borderColor) {
- if (borderColor == mBorderColor) {
- return;
- }
-
- mBorderColor = borderColor;
- mBorderPaint.setColor(mBorderColor);
- invalidate();
- }
-
- public void setBorderColorResource(@ColorRes int borderColorRes) {
- setBorderColor(getContext().getResources().getColor(borderColorRes));
- }
-
- public int getFillColor() {
- return mFillColor;
- }
-
- public void setFillColor(@ColorInt int fillColor) {
- if (fillColor == mFillColor) {
- return;
- }
-
- mFillColor = fillColor;
- mFillPaint.setColor(fillColor);
- invalidate();
- }
-
- public void setFillColorResource(@ColorRes int fillColorRes) {
- setFillColor(getContext().getResources().getColor(fillColorRes));
- }
-
- public int getBorderWidth() {
- return mBorderWidth;
- }
-
- public void setBorderWidth(int borderWidth) {
- if (borderWidth == mBorderWidth) {
- return;
- }
-
- mBorderWidth = borderWidth;
- setup();
- }
-
- public boolean isBorderOverlay() {
- return mBorderOverlay;
- }
-
- public void setBorderOverlay(boolean borderOverlay) {
- if (borderOverlay == mBorderOverlay) {
- return;
- }
-
- mBorderOverlay = borderOverlay;
- setup();
- }
-
- public boolean isDisableCircularTransformation() {
- return mDisableCircularTransformation;
- }
-
- public void setDisableCircularTransformation(boolean disableCircularTransformation) {
- if (mDisableCircularTransformation == disableCircularTransformation) {
- return;
- }
-
- mDisableCircularTransformation = disableCircularTransformation;
- initializeBitmap();
- }
-
- @Override
- public void setImageBitmap(Bitmap bm) {
- super.setImageBitmap(bm);
- initializeBitmap();
- }
-
- @Override
- public void setImageDrawable(Drawable drawable) {
- System.out.println(" setImageDrawable(Drawable drawable) ");
- super.setImageDrawable(drawable);
-
- initializeBitmap();
- }
-
- @Override
- public void setImageResource(@DrawableRes int resId) {
- super.setImageResource(resId);
- initializeBitmap();
- }
-
- @Override
- public void setImageURI(Uri uri) {
- super.setImageURI(uri);
- initializeBitmap();
- }
-
- @Override
- public void setColorFilter(ColorFilter cf) {
- if (cf == mColorFilter) {
- return;
- }
-
- mColorFilter = cf;
- applyColorFilter();
- invalidate();
- }
-
- @Override
- public ColorFilter getColorFilter() {
- return mColorFilter;
- }
-
- private void applyColorFilter() {
- if (mBitmapPaint != null) {
- mBitmapPaint.setColorFilter(mColorFilter);
- }
- }
-
- private Bitmap getBitmapFromDrawable(Drawable drawable) {
- System.out.println("getBitmapFromDrawable(Drawable drawable ");
- if (drawable == null) {
- return null;
- }
-
- if (drawable instanceof BitmapDrawable) {
- return ((BitmapDrawable) drawable).getBitmap();
- }
-
- try {
- Bitmap bitmap;
-
- if (drawable instanceof ColorDrawable) {
- bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, COLORDRAWABLE_DIMENSION, BITMAP_CONFIG);
- } else {
- bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), BITMAP_CONFIG);
- }
-
- Canvas canvas = new Canvas(bitmap);
- drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
- drawable.draw(canvas);
- return bitmap;
- } catch (Exception e) {
- e.printStackTrace();
- return null;
- }
- }
-
- private void initializeBitmap() {
- System.out.println("initializeBitmap() ");
- if (mDisableCircularTransformation) {
- mBitmap = null;
- } else {
- mBitmap = getBitmapFromDrawable(getDrawable());
- }
- setup();
- }
-
- private void setup() {
- System.out.println(" setup() ");
- if (!mReady) {
- System.out.println("(!mReady) ");
- mSetupPending = true;
- return;
- }
-
- if (getWidth() == 0 && getHeight() == 0) {
- System.out.println("getWidth() == 0 && getHeight() == ");
- return;
- }
-
- if (mBitmap == null) {
- System.out.println("mBitmap == null");
- invalidate();
- return;
- }
-
- mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
-
- mBitmapPaint.setAntiAlias(true);
- mBitmapPaint.setShader(mBitmapShader);
-
- mBorderPaint.setStyle(Paint.Style.STROKE);
- mBorderPaint.setAntiAlias(true);
- mBorderPaint.setColor(mBorderColor);
- mBorderPaint.setStrokeWidth(mBorderWidth);
-
- mFillPaint.setStyle(Paint.Style.FILL);
- mFillPaint.setAntiAlias(true);
- mFillPaint.setColor(mFillColor);
-
- mBitmapHeight = mBitmap.getHeight();
- mBitmapWidth = mBitmap.getWidth();
-
- mBorderRect.set(calculateBounds());
- mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2.0f, (mBorderRect.width() - mBorderWidth) / 2.0f);
- System.out.println(" mDrawableRect.set(mBorderRect);");
- mDrawableRect.set(mBorderRect);
- if (!mBorderOverlay) {
- mDrawableRect.inset(mBorderWidth, mBorderWidth);
- }
- mDrawableRadius = Math.min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f);
-
- applyColorFilter();
- updateShaderMatrix();
- invalidate();
- }
-
- private RectF calculateBounds() {
- int availableWidth = getWidth() - getPaddingLeft() - getPaddingRight();
- int availableHeight = getHeight() - getPaddingTop() - getPaddingBottom();
-
- int sideLength = Math.min(availableWidth, availableHeight);
-
- float left = getPaddingLeft() + (availableWidth - sideLength) / 2f;
- float top = getPaddingTop() + (availableHeight - sideLength) / 2f;
-
- return new RectF(left, top, left + sideLength, top + sideLength);
- }
-
- private void updateShaderMatrix() {
- float scale;
- float dx = 0;
- float dy = 0;
-
- mShaderMatrix.set(null);
-
- if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) {
- scale = mDrawableRect.height() / (float) mBitmapHeight;
- dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f;
- } else {
- scale = mDrawableRect.width() / (float) mBitmapWidth;
- dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f;
- }
-
- mShaderMatrix.setScale(scale, scale);
- mShaderMatrix.postTranslate((int) (dx + 0.5f) + mDrawableRect.left, (int) (dy + 0.5f) + mDrawableRect.top);
-
- mBitmapShader.setLocalMatrix(mShaderMatrix);
- }
-
- }
布局文件:
<?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:orientation="vertical">
<de.hdodenhof.circleimageview.CircleImageView
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="300dp"
android:src="@mipmap/mydog"
/>
</LinearLayout>
程序代码比较长,我们分段分析。
(一)话题引出
首先我们先看下构造器,第57行代码,一般情况下我们创建该view对象的时候都会执行57行的构造器代码。构造器中首先是调用了父类的构造器,然后输出构造器方法名称,然后处理自定义属性,然后调用init方法进行控件的初始化操作。
在这里们需要注意几个问题:
(1) 控件的属性分为原生系统属性和我们自己定义的自定义属性。这些属性的值都是通过加载分析布局文件得到的。所以构造器中需要调用super方法处理原生系统属性,然后处理自定义属性。
(2)表面上看,系统在执行的时候首先会执行了构造器,然后执行了init方法,init方法中调用了setUp方法。 其实真正执行的时候并不是这个顺序。
(3)我们先来看下系统最初开始运行时将会执行哪一部分方法。
上面的图片中显示首先是输出了setImageDrawable方法,然后是initialzeBitmap方法,然后是getBitmapDrawable方法,随后是setUp方法,然后是构造器方法,然后是init方法,然后是setUp方法。
这个执行顺序与上面第二条分析相悖,显然在构造器方法输出之前执行了很多方法。为什么会这样?
其实系统在调用构造器方法的时候会执行构造器方法,这个时候因为构造器方法中首先调用了super方法请求父类处理原生系统属性, 在布局文件中
android
:src=
"@mipmap/mydog"属性属于系统属性,所以父类的构造器中会处理该属性,也就是调用CircleImageView控件的setImageDrawable方法设置图片。所以会出现程序首先输出了setImageDrawable方法的现象。
在setImageDrawable方法中又调用了initialzeBitmap方法处理drawable生成bitmap;
getBitmapFromDrawable方法我们稍后分析。在
initializeBitmap方法内部又调用了setUp方法
(4) 上文说到initializeBitmap方法内部又调用了setUp方法,这个时候会执行setUp方法。
首先我们来分析一下setUp方法中做了什么,setUp方法中真正实质性的内容是代码311行到339行。
- 311行是创建一个着色器,需要一个bitmap,只要你设置了图片,那么根据前面3的分析可以看出这个bitmap是已经获得到了。
- 代码313到330 主要是设置一些属性,其中318行等几行使用到了自定义属性,你需要知道现在程序之所以可以到这里是因为在CircleImageView的构造器中调用了super构造器,然后调用了setImageDrawable,然后调用了initialzeBitmap,然后调用了setUp方法;所以这个时候你自已定义的自定义属性还没被初始化,这会导致自定义属性不起作用(设置了却还没来得及加载),所以这个时候不能执行setUp函数中的代码。
- 代码331行调用了invalidate方法请求重绘,该方法会导致系统调用ondraw方法。我们知道view的显示过程是创建---》测量---布局--画图;而现在你的构造器方法还没哟执行完却要从构造器中调用onDraw方法,所以从这层含义以及上面的第二点提到的含义中我们断定这个时候setUp不能执行,原因就是CircleImageView没有完成初始化工作,自定义属性没有加载赋值,并且setUp方法会导致onDraw方法 调用,而这个时候尚且不能执行onDraw。
- 综合以上两点,针对此种情况(由父类构造器间接调用setUp)需要进行拦截setUp方法的执行
(二)如何把圆形头像绘制出来?
你一定要明白图片是怎么绘制出来的;你可能会这样想:圆形头像的绘制不是很简单吗?不就是给这个CircleImageView设置一个背景图片然后在这个CircleImageView上面切出一个圆形来显示图片吗??而没有被包含到切入范围的位置不显示图片。。如果你这样想就错了。
上文我们分析到当你在xml布局文件中指定了图片之后,系统在CircleImageView的构造器中通过调用super构造器会获取 到图片并调用控件的setBitmapDrawable方法给控件设置图片,那么是不是这就意味着此时当setBitmapDrawable方法执行之后就会控件就会显示图片了呢??其实即使执行完了setBitmapDrawable方法控件也不会显示图片。
原因是,你首先要先理解构造器的作用是什么,构造器的作用是初始化资源,也就是初步构造一个对象,setBitmapDrawable方法被父类构造器调用到并不意味着此时控件就会显示图片。setBitmapDrawable方法仅仅是用于保存图片对象用于以后的绘制。因为构造器没有完成的时候,控件的大小宽度均为0。一个控件在初始化完成(构造器执行完成)之后会经历measure--layout--draw三个过程,最终图片的显示是在draw方法中实现的。可以实例验证一下:
测试用例一:
public class
TestCircleImageView
extends
ImageView {
public TestCircleImageView(Context context) {
super(context);
}
public TestCircleImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TestCircleImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onDraw(Canvas canvas) {
// super.onDraw(canvas);
}
}
布局文件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<de.hdodenhof.circleimageview.TestCircleImageView
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="300dp"
android:src="@mipmap/mydog"
/>
</LinearLayout>
在上面的TestCircleImageView控件中我们重写了onDraw方法,请注意在onDraw方法中并没有调用super.onDraw方法;布局文件中给控件设置了图片。程序的运行结果是一片空白,也就是图片没有被绘制出来,原因就是因为TestCircleImageView的ondraw方法中并没有去调用super.onDraw方法去绘制图片。
测试用例二
将上面的测试用用例中的onDraw方法中的super.onDraw方法前面的注释去掉,其他保持不变。运行程序你会发现图片可以正常显示出来,图片的大小也就是控件的大小。
从上面的这两个测试用例中我们可以看出控件背景图片的绘制是通过super.onDraw绘制出来的。
回到前面的那个话题:你可能会这样想,圆形头像的绘制不是很简单吗?不就是给这个CircleImageView设置一个背景图片然后在这个CircleImageView上面切出一个圆形来显示图片吗??而没有被包含到切入范围的位置不显示图片,简单实现代码如下。。如果你这样想就错了。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int mDrawableRadius=Math.min(getWidth(),getHeight());
canvas.drawCircle(getWidth()/2, getHeight()/2, mDrawableRadius, new Paint());
}
如果你是通过上面的方法来画出一个显示圆形头像的图片,那么上面的方法并不能现这样的效果,原因是canva上面已经画出了整个图片,然后又在上面画出了一个圆形,圆形中的图片无法正常显示,圆形周围显示图片,这显然不是我们想要的。
那么在这里是如何实现绘制一个圆形头像的呢??
我们知道当你更换图片的时候会调用setBitmapDrawable等几个方法,这几个方法都是用于接收更换的图片,然后正真实现显示图片是在onDraw方法方法中实现的。我们以setBitmapDrawable方法来分析这个过程:
- @Override
- public void setImageBitmap(Bitmap bm) {
- super.setImageBitmap(bm);
- initializeBitmap();// 在这里感觉有个缺点,
- }
你需要注意是在setImageBitmap方法中首先是调用了super方法,你去查看一下父类ImageView的setImageBitmap 方法的实现 ,super方法完成了两件事,一是保存需要更换的图片,二是调用
setImageDrawable,而
setImageDrawable方法最后又调用了
invalidate();方法,该方法会请求系统对view进行重新绘制,也就是请求系统调用onDraw方法,从而实现了控件图片的更改。正是因为这样的原因所以我们绘制圆形头像的操作需要放置在onDraw方法实现,这样才能保证即使用户更换图片也是现实圆形头像,而不是现实方形整个图片。
此时或许你会认为:在setImageBitmap方法中调用了super方法,而super方法中又间接调用到了invalidate方法,而invalidate方法会导致系统调用onDraw方法,onDraw方法执行完成之后才会执行initialzeBitmap方法。其实过程并不是这样的。现在你可以回头看看最上面的图片中显示的方法调用顺序的输出信息,你会发现initialzeBitmap方法在onDraw方法之前被调用了,这是为什么呢???其实super方法间接调用到了invalidate方法,而invalidate方法的说明文档中说改方法并不是直接导致onDraw方法的调用,而是在未来某个时刻系统会调用onDraw方法,所以才会出现initialzeBitmap方法在onDraw方法之前被调用的情况。关于这一点的理解,可以解释onDraw方法中的111到113行的实现逻辑以及实现原因,为什么要这样写??什么情况下才会出现bitmap为null。
回到之前的那个话题(
如何实现绘制一个圆形头像的呢??
),setImageBitmap方法中调用了initializeBitmap方法,该方法主要实现的逻辑是在287行调用
mBitmap = getBitmapFromDrawable(getDrawable());
getBitmapFormDrawable方法内部实现重要。260行代码判断控件CircleImageView设置的背景图片是不是一个BitmapDrawable,如果是的话那么就能可以直接获取到其中的bitmap;如果不是那么就往下执行。
272行到274行是gitBitmapFromDrawable方法的核心实现,getBitmapFromDrawable方法中使用到了方法的成员变量mBitmap(关于这个变量的重要作用会在后文总结),272行根据这个mBitmap创建一个canvas对象,那么以后这个canvas就会将绘图操作绘制到bitmap上,274行代码是将当前控件的背景图片drawable通过绘制到canvas上面,其实就是绘制到mBitmap上面,这里更准确的表达是drawable对象借助canvas将自己绘制到bitmap上面。所以我们可以说mBitmap是控件的背景图片的副本,我们想要一个bitmap类型的图片,所以需要根据控件的背景图片得到一个bitmap。这个mBitmap就是绘制圆形头像的基础。
当initialzeBitmap方法执行完成之后我们就准备好了mBitmap,在initialzeBitmap方法的最后调用了setUp方法。
我们来看一下setUp方法中实现的主要逻辑,311行代码 mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 创建了衣蛾着色器,并且这个着色器绑定到了bitmap;314行 mBitmapPaint.setShader(mBitmapShader); 将着色器设置到画笔中,经过这两个过程,以后这个画笔画出的图像就是mBitmap中的图像,同时也是控件的背景图像。
118行 canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint); 在onDraw方法通过 canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint);实现了绘制指定圆形头像的功能。
至此圆形头像如何绘制出来的思路基本就说完了,我们来总结下就是,每当用户或者程序更改控件的图片的时候,也即是setImageDrawable等几个方法被调用,这个时候我们需要根据当前的drawable图像创建一个对应的bitmap,如何创建呢??就是
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
这三行代码。创建完了bitmap之后,将该着色器绑定到bitmap,然后把着色器设置到画笔中,最后使用画笔画的时候就会画出圆形头像。
------
从上面的图片中我们可以看出只有圆形头像的内部才会新式图片,圆形头像外部并不现实图片,主要是画笔的作用,画笔绑定到了mbitmap,
canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint)。。
那么着色器的作用是什么呢??
mBitmapShader = new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP);
参数1:bitmap
参数2,参数3:TileMode;
TileMode的取值有三种: CLAMP (0): replicate the edge color if the shader draws outside of its original bounds 其实我也不太理解。
以上内容讲述了圆形头像实现的原理过程。下面我们来说一下编码的事情
(三)编码实现圆形头像
上面我们提到了当用户更换头像的时候如何实现更换圆形头像,其过程就是更新mBitmap,然后在onDraw方法中重新绘制头像。所以setImageDrawable等几个方法中需要调用InitialzeBitmap方法处理生成bitmap,initialzeBitmap方法中又要调用setUp方法来创建着色器,并设置画笔,最后等待系统调用onDraw方法。
(四)setUp函数
(1)我们先来描述一下控件首次创建时的过程:CircleImageView的构造器被调用---调用super构造器---调用CircleImageView的setBitmapDrawable方法----调用到super的setBitmapDrawable方法----调用到invalidate方法请求重绘
然后是initialzeBitmap方法执行---setUp函数被调用---此时setUp中的第一if为true,使得 mSetupPending = true;并返回
此时程序返回了,接下来就会执行自定义属性的加载----调用init-----init中的if条件成立---setUp,此时因为setUp函数是在构造器中被调用的,所以此时控件的width和height为0,所以setUp中的第二个if为ture ---init方法结束--构造器结束。
上面的分析过程中我认为init方法中没有必要调用setUp函数,因为init方法是被构造器调用的,构造器调用init方法时控件还没完成测量工作,此时的宽高都为0,所以setUp总是在第二个if条件处会返回,不会往下执行,所以init中调用setUp没必要。
(2)setUp函数中
- if (!mReady) {
- System.out.println("(!mReady) ");
- mSetupPending = true;
- return;
- }
主要使用与处理拦截setUp被构造器调用的这种情况,具体原因可以回头看看分析一。
截止到此处我们已经指出了setUp方法两次被调用,第一次是在 父类构造器中,通过setBitmapDrawable方法导致setUp方法被调用了,并且这次调用是不合适的调用(因为此时我们的自定义属性还没来得及加载处理设置,如果强行执行setUp,那么会导致用户设置的自定义属性值得不到使用,而是会使用了默认的自定义属性值);
第二次setUp的调用是在init方法中,这次调用是我们程序员主动调用的,我个人认为这次调用没有什么作用。
至此在第二次setUp调用结束之后,init方法也就执行结束了,那么现在的问题是,两次setUp的调用都被拦截返回了,setUp方法中需要实现设置画笔等操作,没有设置画笔那么图像能显示出来吗?? 的确,两次setUp方法的调用都被拦截了,如果setUp方法得不到完整的执行,那么着色器无法创建,画笔无法设置绑定到bitmap,同样也就会无法显示圆形头像。
当CircleImageView的构造器执行完成之后,此时的CircleImageView还不会显示在屏幕上,他没有大小,也没有位置,他还需要经历measure--layout--draw才会显示到界面。但CircleImageView被测量设置到大小的时候我们圆形头像的区域也要跟着变化,原因是圆形头像绘制的区域是CircleImageView的width减去padding等边距剩余的有效区域,所以当CircleImageView的大小变化的时候,圆形头像的位置大小也会变化。并且CircleImageView在构造执行完成之后系统会给他分配大小,此时正是显示圆形头像的良好时机。所以上面的实现代码中我们重写了onSizechanged方法,该方法会在构造器执行完之后被调用,并且担负起绘制图片的工作(也就是调用setUp),setUp方法的最后调用了invalidate方法,从而通知系统更新显示最新设置的mBitmap。
所以setUp方法的第三次调用是通过onSizeChanged方法调用了,并且setUp方法的最后必须要调用invalidate方法请求系统绘制最新版的mBitmap。
(3)
if (mBitmap == null) {
System.out.println("mBitmap == null");
invalidate();
return;
}
如果控件设置图片为null,那么就不需要显示bitmap,其实这里根本不会出现图片为null的情况。 这里mBitmap为null,说明控件的图片没有被绘制出来,通知重新绘制。
(六)updateShaderMatrix 函数
我们知道控件的大小是一定的,width-padding才是可以显示图片的有效区域。那么图片也有大小,如何在有有限的区域内显示一张完整的图片呢?使用矩阵实现对图片的缩放,也就是updateShaderMatrix函数。
- package de.hdodenhof.circleimageview;
- public class CircleImageView extends ImageView {
-
- private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP;//决定了图片在View上显示时的样子,如进行何种比例的缩放,及显示图片的整体还是部分,等等
-
- private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
- private static final int COLORDRAWABLE_DIMENSION = 2;
-
- private static final int DEFAULT_BORDER_WIDTH = 0;//默然图片的边围圆圈线的宽度
- private static final int DEFAULT_BORDER_COLOR = Color.BLACK;//默认图片圆圈的颜色
- private static final int DEFAULT_FILL_COLOR = Color.TRANSPARENT;//圆圈背景的填充颜色
- private static final boolean DEFAULT_BORDER_OVERLAY = false;//
-
- private final RectF mDrawableRect = new RectF();//我们的view的大小是一定的,用户通过layout_width属性指定,用户也有可能设置padding属性,这就会要求我们的圆形头像
- //要距离这个CircleImageView的边距是padding值,也就是说圆形图片需要画在那个位置是不确定的,需要CircleImageView的宽高,以及设置padding属性来定位。我们使用一个矩形Recf来表示这个CircleImageView的剩余有效区域
- //并且将会在这个有效区域内部画出一个圆形
-
- private final RectF mBorderRect = new RectF();//为圆形头像画出一个边围圆圈,mBorderRect用于记录该圆圈的矩形位置,这个矩形的位置和上面图片矩形位置是一样的。
-
- private final Matrix mShaderMatrix = new Matrix();
- private final Paint mBitmapPaint = new Paint();//图像画笔
- private final Paint mBorderPaint = new Paint();//边围圆圈的画笔
- private final Paint mFillPaint = new Paint();//图片底部的view(CircleImageView)的填充颜色,也就是圆形头像的背景填充色
-
- private int mBorderColor = DEFAULT_BORDER_COLOR;
- private int mBorderWidth = DEFAULT_BORDER_WIDTH;
- private int mFillColor = DEFAULT_FILL_COLOR;
-
- private Bitmap mBitmap;
- private BitmapShader mBitmapShader;
- private int mBitmapWidth;
- private int mBitmapHeight;
-
- private float mDrawableRadius;
- private float mBorderRadius;
-
- private ColorFilter mColorFilter;
-
- private boolean mReady;
- private boolean mSetupPending;
- private boolean mBorderOverlay ;
- private boolean mDisableCircularTransformation;
-
- public CircleImageView(Context context) {
- super(context);
-
- init();
- }
-
- public CircleImageView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public CircleImageView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- System.out.println("CircleImageView(Context context, AttributeSet attrs, int defStyle" );
-
-
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyle, 0);
- mBorderWidth = a.getDimensionPixelSize(R.styleable.CircleImageView_civ_border_width, DEFAULT_BORDER_WIDTH);
- mBorderColor = a.getColor(R.styleable.CircleImageView_civ_border_color, DEFAULT_BORDER_COLOR);
- mBorderOverlay = a.getBoolean(R.styleable.CircleImageView_civ_border_overlay, DEFAULT_BORDER_OVERLAY);
- mFillColor = a.getColor(R.styleable.CircleImageView_civ_fill_color, DEFAULT_FILL_COLOR);
-
- a.recycle();
-
- init();
- }
-
- private void init() {
- System.out.println("init ");
- super.setScaleType(SCALE_TYPE);
- mReady = true;
-
- if (mSetupPending) {
- setup();
- mSetupPending = false;
- }
- }
-
- @Override
- public ScaleType getScaleType() {
- return SCALE_TYPE;
- }
-
- @Override
- public void setScaleType(ScaleType scaleType) {
- if (scaleType != SCALE_TYPE) {
- throw new IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType));
- }
- }
-
- @Override
- public void setAdjustViewBounds(boolean adjustViewBounds) {
- if (adjustViewBounds) {
- throw new IllegalArgumentException("adjustViewBounds not supported.");
- }
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- System.out.println("onDraw(Canvas canvas) ");
- if (mDisableCircularTransformation) {
- super.onDraw(canvas);
- return;
- }
-
- if (mBitmap == null) {
- return;
- }
-
- if (mFillColor != Color.TRANSPARENT) {
- canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mFillPaint);
- }
- canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint);
- if (mBorderWidth != 0) {
- canvas.drawCircle(mBorderRect.centerX(), mBorderRect.centerY(), mBorderRadius, mBorderPaint);
- }
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- setup();
- }
-
- public int getBorderColor() {
- return mBorderColor;
- }
-
- public void setBorderColor(@ColorInt int borderColor) {
- if (borderColor == mBorderColor) {
- return;
- }
-
- mBorderColor = borderColor;
- mBorderPaint.setColor(mBorderColor);
- invalidate();
- }
-
- public void setBorderColorResource(@ColorRes int borderColorRes) {
- setBorderColor(getContext().getResources().getColor(borderColorRes));
- }
-
- public int getFillColor() {
- return mFillColor;
- }
-
- public void setFillColor(@ColorInt int fillColor) {
- if (fillColor == mFillColor) {
- return;
- }
-
- mFillColor = fillColor;
- mFillPaint.setColor(fillColor);
- invalidate();
- }
-
- public void setFillColorResource(@ColorRes int fillColorRes) {
- setFillColor(getContext().getResources().getColor(fillColorRes));
- }
-
- public int getBorderWidth() {
- return mBorderWidth;
- }
-
- public void setBorderWidth(int borderWidth) {
- if (borderWidth == mBorderWidth) {
- return;
- }
-
- mBorderWidth = borderWidth;
- setup();
- }
-
- public boolean isBorderOverlay() {
- return mBorderOverlay;
- }
-
- public void setBorderOverlay(boolean borderOverlay) {
- if (borderOverlay == mBorderOverlay) {
- return;
- }
-
- mBorderOverlay = borderOverlay;
- setup();
- }
-
- public boolean isDisableCircularTransformation() {
- return mDisableCircularTransformation;
- }
-
- public void setDisableCircularTransformation(boolean disableCircularTransformation) {
- if (mDisableCircularTransformation == disableCircularTransformation) {
- return;
- }
-
- mDisableCircularTransformation = disableCircularTransformation;
- initializeBitmap();
- }
-
- @Override
- public void setImageBitmap(Bitmap bm) {
- super.setImageBitmap(bm);
- initializeBitmap();
- }
-
- @Override
- public void setImageDrawable(Drawable drawable) {
- System.out.println(" setImageDrawable(Drawable drawable) ");
- super.setImageDrawable(drawable);
-
- initializeBitmap();
- }
-
- @Override
- public void setImageResource(@DrawableRes int resId) {
- super.setImageResource(resId);
- initializeBitmap();
- }
-
- @Override
- public void setImageURI(Uri uri) {
- super.setImageURI(uri);
- initializeBitmap();
- }
-
- @Override
- public void setColorFilter(ColorFilter cf) {
- if (cf == mColorFilter) {
- return;
- }
-
- mColorFilter = cf;
- applyColorFilter();
- invalidate();
- }
-
- @Override
- public ColorFilter getColorFilter() {
- return mColorFilter;
- }
-
- private void applyColorFilter() {
- if (mBitmapPaint != null) {
- mBitmapPaint.setColorFilter(mColorFilter);
- }
- }
-
- private Bitmap getBitmapFromDrawable(Drawable drawable) {
- System.out.println("getBitmapFromDrawable(Drawable drawable ");
- if (drawable == null) {
- return null;
- }
-
- if (drawable instanceof BitmapDrawable) {
- return ((BitmapDrawable) drawable).getBitmap();
- }
-
- try {
- Bitmap bitmap;
-
- if (drawable instanceof ColorDrawable) {
- bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, COLORDRAWABLE_DIMENSION, BITMAP_CONFIG);
- } else {
- bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), BITMAP_CONFIG);
- }
-
- Canvas canvas = new Canvas(bitmap);
- drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
- drawable.draw(canvas);
- return bitmap;
- } catch (Exception e) {
- e.printStackTrace();
- return null;
- }
- }
-
- private void initializeBitmap() {
- System.out.println("initializeBitmap() ");
- if (mDisableCircularTransformation) {
- mBitmap = null;
- } else {
- mBitmap = getBitmapFromDrawable(getDrawable());
- }
- setup();
- }
-
- private void setup() {
- System.out.println(" setup() ");
- if (!mReady) {
- System.out.println("(!mReady) ");
- mSetupPending = true;
- return;
- }
-
- if (getWidth() == 0 && getHeight() == 0) {
- System.out.println("getWidth() == 0 && getHeight() == ");
- return;
- }
-
- if (mBitmap == null) {
- System.out.println("mBitmap == null");
- invalidate();
- return;
- }
-
- mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
-
- mBitmapPaint.setAntiAlias(true);
- mBitmapPaint.setShader(mBitmapShader);
-
- mBorderPaint.setStyle(Paint.Style.STROKE);
- mBorderPaint.setAntiAlias(true);
- mBorderPaint.setColor(mBorderColor);
- mBorderPaint.setStrokeWidth(mBorderWidth);
-
- mFillPaint.setStyle(Paint.Style.FILL);
- mFillPaint.setAntiAlias(true);
- mFillPaint.setColor(mFillColor);
-
- mBitmapHeight = mBitmap.getHeight();
- mBitmapWidth = mBitmap.getWidth();
-
- mBorderRect.set(calculateBounds());
- mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2.0f, (mBorderRect.width() - mBorderWidth) / 2.0f);
- System.out.println(" mDrawableRect.set(mBorderRect);");
- mDrawableRect.set(mBorderRect);
- if (!mBorderOverlay) {
- mDrawableRect.inset(mBorderWidth, mBorderWidth);
- }
- mDrawableRadius = Math.min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f);
-
- applyColorFilter();
- updateShaderMatrix();
- invalidate();
- }
-
- private RectF calculateBounds() {
- int availableWidth = getWidth() - getPaddingLeft() - getPaddingRight();
- int availableHeight = getHeight() - getPaddingTop() - getPaddingBottom();
-
- int sideLength = Math.min(availableWidth, availableHeight);
-
- float left = getPaddingLeft() + (availableWidth - sideLength) / 2f;
- float top = getPaddingTop() + (availableHeight - sideLength) / 2f;
-
- return new RectF(left, top, left + sideLength, top + sideLength);
- }
-
- private void updateShaderMatrix() {
- float scale;
- float dx = 0;
- float dy = 0;
-
- mShaderMatrix.set(null);
-
- if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) {
- scale = mDrawableRect.height() / (float) mBitmapHeight;
- dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f;
- } else {
- scale = mDrawableRect.width() / (float) mBitmapWidth;
- dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f;
- }
-
- mShaderMatrix.setScale(scale, scale);
- mShaderMatrix.postTranslate((int) (dx + 0.5f) + mDrawableRect.left, (int) (dy + 0.5f) + mDrawableRect.top);
-
- mBitmapShader.setLocalMatrix(mShaderMatrix);
- }
-
- }
布局文件:
<?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:orientation="vertical">
<de.hdodenhof.circleimageview.CircleImageView
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="300dp"
android:src="@mipmap/mydog"
/>
</LinearLayout>
程序代码比较长,我们分段分析。
(一)话题引出
首先我们先看下构造器,第57行代码,一般情况下我们创建该view对象的时候都会执行57行的构造器代码。构造器中首先是调用了父类的构造器,然后输出构造器方法名称,然后处理自定义属性,然后调用init方法进行控件的初始化操作。
在这里们需要注意几个问题:
(1) 控件的属性分为原生系统属性和我们自己定义的自定义属性。这些属性的值都是通过加载分析布局文件得到的。所以构造器中需要调用super方法处理原生系统属性,然后处理自定义属性。
(2)表面上看,系统在执行的时候首先会执行了构造器,然后执行了init方法,init方法中调用了setUp方法。 其实真正执行的时候并不是这个顺序。
(3)我们先来看下系统最初开始运行时将会执行哪一部分方法。
上面的图片中显示首先是输出了setImageDrawable方法,然后是initialzeBitmap方法,然后是getBitmapDrawable方法,随后是setUp方法,然后是构造器方法,然后是init方法,然后是setUp方法。
这个执行顺序与上面第二条分析相悖,显然在构造器方法输出之前执行了很多方法。为什么会这样?
其实系统在调用构造器方法的时候会执行构造器方法,这个时候因为构造器方法中首先调用了super方法请求父类处理原生系统属性, 在布局文件中
android
:src=
"@mipmap/mydog"属性属于系统属性,所以父类的构造器中会处理该属性,也就是调用CircleImageView控件的setImageDrawable方法设置图片。所以会出现程序首先输出了setImageDrawable方法的现象。
在setImageDrawable方法中又调用了initialzeBitmap方法处理drawable生成bitmap;
getBitmapFromDrawable方法我们稍后分析。在
initializeBitmap方法内部又调用了setUp方法
(4) 上文说到initializeBitmap方法内部又调用了setUp方法,这个时候会执行setUp方法。
首先我们来分析一下setUp方法中做了什么,setUp方法中真正实质性的内容是代码311行到339行。
- 311行是创建一个着色器,需要一个bitmap,只要你设置了图片,那么根据前面3的分析可以看出这个bitmap是已经获得到了。
- 代码313到330 主要是设置一些属性,其中318行等几行使用到了自定义属性,你需要知道现在程序之所以可以到这里是因为在CircleImageView的构造器中调用了super构造器,然后调用了setImageDrawable,然后调用了initialzeBitmap,然后调用了setUp方法;所以这个时候你自已定义的自定义属性还没被初始化,这会导致自定义属性不起作用(设置了却还没来得及加载),所以这个时候不能执行setUp函数中的代码。
- 代码331行调用了invalidate方法请求重绘,该方法会导致系统调用ondraw方法。我们知道view的显示过程是创建---》测量---布局--画图;而现在你的构造器方法还没哟执行完却要从构造器中调用onDraw方法,所以从这层含义以及上面的第二点提到的含义中我们断定这个时候setUp不能执行,原因就是CircleImageView没有完成初始化工作,自定义属性没有加载赋值,并且setUp方法会导致onDraw方法 调用,而这个时候尚且不能执行onDraw。
- 综合以上两点,针对此种情况(由父类构造器间接调用setUp)需要进行拦截setUp方法的执行
(二)如何把圆形头像绘制出来?
你一定要明白图片是怎么绘制出来的;你可能会这样想:圆形头像的绘制不是很简单吗?不就是给这个CircleImageView设置一个背景图片然后在这个CircleImageView上面切出一个圆形来显示图片吗??而没有被包含到切入范围的位置不显示图片。。如果你这样想就错了。
上文我们分析到当你在xml布局文件中指定了图片之后,系统在CircleImageView的构造器中通过调用super构造器会获取 到图片并调用控件的setBitmapDrawable方法给控件设置图片,那么是不是这就意味着此时当setBitmapDrawable方法执行之后就会控件就会显示图片了呢??其实即使执行完了setBitmapDrawable方法控件也不会显示图片。
原因是,你首先要先理解构造器的作用是什么,构造器的作用是初始化资源,也就是初步构造一个对象,setBitmapDrawable方法被父类构造器调用到并不意味着此时控件就会显示图片。setBitmapDrawable方法仅仅是用于保存图片对象用于以后的绘制。因为构造器没有完成的时候,控件的大小宽度均为0。一个控件在初始化完成(构造器执行完成)之后会经历measure--layout--draw三个过程,最终图片的显示是在draw方法中实现的。可以实例验证一下:
测试用例一:
public class
TestCircleImageView
extends
ImageView {
public TestCircleImageView(Context context) {
super(context);
}
public TestCircleImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TestCircleImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onDraw(Canvas canvas) {
// super.onDraw(canvas);
}
}
布局文件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<de.hdodenhof.circleimageview.TestCircleImageView
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="300dp"
android:src="@mipmap/mydog"
/>
</LinearLayout>
在上面的TestCircleImageView控件中我们重写了onDraw方法,请注意在onDraw方法中并没有调用super.onDraw方法;布局文件中给控件设置了图片。程序的运行结果是一片空白,也就是图片没有被绘制出来,原因就是因为TestCircleImageView的ondraw方法中并没有去调用super.onDraw方法去绘制图片。
测试用例二
将上面的测试用用例中的onDraw方法中的super.onDraw方法前面的注释去掉,其他保持不变。运行程序你会发现图片可以正常显示出来,图片的大小也就是控件的大小。
从上面的这两个测试用例中我们可以看出控件背景图片的绘制是通过super.onDraw绘制出来的。
回到前面的那个话题:你可能会这样想,圆形头像的绘制不是很简单吗?不就是给这个CircleImageView设置一个背景图片然后在这个CircleImageView上面切出一个圆形来显示图片吗??而没有被包含到切入范围的位置不显示图片,简单实现代码如下。。如果你这样想就错了。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int mDrawableRadius=Math.min(getWidth(),getHeight());
canvas.drawCircle(getWidth()/2, getHeight()/2, mDrawableRadius, new Paint());
}
如果你是通过上面的方法来画出一个显示圆形头像的图片,那么上面的方法并不能现这样的效果,原因是canva上面已经画出了整个图片,然后又在上面画出了一个圆形,圆形中的图片无法正常显示,圆形周围显示图片,这显然不是我们想要的。
那么在这里是如何实现绘制一个圆形头像的呢??
我们知道当你更换图片的时候会调用setBitmapDrawable等几个方法,这几个方法都是用于接收更换的图片,然后正真实现显示图片是在onDraw方法方法中实现的。我们以setBitmapDrawable方法来分析这个过程:
- @Override
- public void setImageBitmap(Bitmap bm) {
- super.setImageBitmap(bm);
- initializeBitmap();// 在这里感觉有个缺点,
- }
你需要注意是在setImageBitmap方法中首先是调用了super方法,你去查看一下父类ImageView的setImageBitmap 方法的实现 ,super方法完成了两件事,一是保存需要更换的图片,二是调用
setImageDrawable,而
setImageDrawable方法最后又调用了
invalidate();方法,该方法会请求系统对view进行重新绘制,也就是请求系统调用onDraw方法,从而实现了控件图片的更改。正是因为这样的原因所以我们绘制圆形头像的操作需要放置在onDraw方法实现,这样才能保证即使用户更换图片也是现实圆形头像,而不是现实方形整个图片。
此时或许你会认为:在setImageBitmap方法中调用了super方法,而super方法中又间接调用到了invalidate方法,而invalidate方法会导致系统调用onDraw方法,onDraw方法执行完成之后才会执行initialzeBitmap方法。其实过程并不是这样的。现在你可以回头看看最上面的图片中显示的方法调用顺序的输出信息,你会发现initialzeBitmap方法在onDraw方法之前被调用了,这是为什么呢???其实super方法间接调用到了invalidate方法,而invalidate方法的说明文档中说改方法并不是直接导致onDraw方法的调用,而是在未来某个时刻系统会调用onDraw方法,所以才会出现initialzeBitmap方法在onDraw方法之前被调用的情况。关于这一点的理解,可以解释onDraw方法中的111到113行的实现逻辑以及实现原因,为什么要这样写??什么情况下才会出现bitmap为null。
回到之前的那个话题(
如何实现绘制一个圆形头像的呢??
),setImageBitmap方法中调用了initializeBitmap方法,该方法主要实现的逻辑是在287行调用
mBitmap = getBitmapFromDrawable(getDrawable());
getBitmapFormDrawable方法内部实现重要。260行代码判断控件CircleImageView设置的背景图片是不是一个BitmapDrawable,如果是的话那么就能可以直接获取到其中的bitmap;如果不是那么就往下执行。
272行到274行是gitBitmapFromDrawable方法的核心实现,getBitmapFromDrawable方法中使用到了方法的成员变量mBitmap(关于这个变量的重要作用会在后文总结),272行根据这个mBitmap创建一个canvas对象,那么以后这个canvas就会将绘图操作绘制到bitmap上,274行代码是将当前控件的背景图片drawable通过绘制到canvas上面,其实就是绘制到mBitmap上面,这里更准确的表达是drawable对象借助canvas将自己绘制到bitmap上面。所以我们可以说mBitmap是控件的背景图片的副本,我们想要一个bitmap类型的图片,所以需要根据控件的背景图片得到一个bitmap。这个mBitmap就是绘制圆形头像的基础。
当initialzeBitmap方法执行完成之后我们就准备好了mBitmap,在initialzeBitmap方法的最后调用了setUp方法。
我们来看一下setUp方法中实现的主要逻辑,311行代码 mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 创建了衣蛾着色器,并且这个着色器绑定到了bitmap;314行 mBitmapPaint.setShader(mBitmapShader); 将着色器设置到画笔中,经过这两个过程,以后这个画笔画出的图像就是mBitmap中的图像,同时也是控件的背景图像。
118行 canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint); 在onDraw方法通过 canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint);实现了绘制指定圆形头像的功能。
至此圆形头像如何绘制出来的思路基本就说完了,我们来总结下就是,每当用户或者程序更改控件的图片的时候,也即是setImageDrawable等几个方法被调用,这个时候我们需要根据当前的drawable图像创建一个对应的bitmap,如何创建呢??就是
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
这三行代码。创建完了bitmap之后,将该着色器绑定到bitmap,然后把着色器设置到画笔中,最后使用画笔画的时候就会画出圆形头像。
------
从上面的图片中我们可以看出只有圆形头像的内部才会新式图片,圆形头像外部并不现实图片,主要是画笔的作用,画笔绑定到了mbitmap,
canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint)。。
那么着色器的作用是什么呢??
mBitmapShader = new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP);
参数1:bitmap
参数2,参数3:TileMode;
TileMode的取值有三种: CLAMP (0): replicate the edge color if the shader draws outside of its original bounds 其实我也不太理解。
以上内容讲述了圆形头像实现的原理过程。下面我们来说一下编码的事情
(三)编码实现圆形头像
上面我们提到了当用户更换头像的时候如何实现更换圆形头像,其过程就是更新mBitmap,然后在onDraw方法中重新绘制头像。所以setImageDrawable等几个方法中需要调用InitialzeBitmap方法处理生成bitmap,initialzeBitmap方法中又要调用setUp方法来创建着色器,并设置画笔,最后等待系统调用onDraw方法。
(四)setUp函数
(1)我们先来描述一下控件首次创建时的过程:CircleImageView的构造器被调用---调用super构造器---调用CircleImageView的setBitmapDrawable方法----调用到super的setBitmapDrawable方法----调用到invalidate方法请求重绘
然后是initialzeBitmap方法执行---setUp函数被调用---此时setUp中的第一if为true,使得 mSetupPending = true;并返回
此时程序返回了,接下来就会执行自定义属性的加载----调用init-----init中的if条件成立---setUp,此时因为setUp函数是在构造器中被调用的,所以此时控件的width和height为0,所以setUp中的第二个if为ture ---init方法结束--构造器结束。
上面的分析过程中我认为init方法中没有必要调用setUp函数,因为init方法是被构造器调用的,构造器调用init方法时控件还没完成测量工作,此时的宽高都为0,所以setUp总是在第二个if条件处会返回,不会往下执行,所以init中调用setUp没必要。
(2)setUp函数中
- if (!mReady) {
- System.out.println("(!mReady) ");
- mSetupPending = true;
- return;
- }
主要使用与处理拦截setUp被构造器调用的这种情况,具体原因可以回头看看分析一。
截止到此处我们已经指出了setUp方法两次被调用,第一次是在 父类构造器中,通过setBitmapDrawable方法导致setUp方法被调用了,并且这次调用是不合适的调用(因为此时我们的自定义属性还没来得及加载处理设置,如果强行执行setUp,那么会导致用户设置的自定义属性值得不到使用,而是会使用了默认的自定义属性值);
第二次setUp的调用是在init方法中,这次调用是我们程序员主动调用的,我个人认为这次调用没有什么作用。
至此在第二次setUp调用结束之后,init方法也就执行结束了,那么现在的问题是,两次setUp的调用都被拦截返回了,setUp方法中需要实现设置画笔等操作,没有设置画笔那么图像能显示出来吗?? 的确,两次setUp方法的调用都被拦截了,如果setUp方法得不到完整的执行,那么着色器无法创建,画笔无法设置绑定到bitmap,同样也就会无法显示圆形头像。
当CircleImageView的构造器执行完成之后,此时的CircleImageView还不会显示在屏幕上,他没有大小,也没有位置,他还需要经历measure--layout--draw才会显示到界面。但CircleImageView被测量设置到大小的时候我们圆形头像的区域也要跟着变化,原因是圆形头像绘制的区域是CircleImageView的width减去padding等边距剩余的有效区域,所以当CircleImageView的大小变化的时候,圆形头像的位置大小也会变化。并且CircleImageView在构造执行完成之后系统会给他分配大小,此时正是显示圆形头像的良好时机。所以上面的实现代码中我们重写了onSizechanged方法,该方法会在构造器执行完之后被调用,并且担负起绘制图片的工作(也就是调用setUp),setUp方法的最后调用了invalidate方法,从而通知系统更新显示最新设置的mBitmap。
所以setUp方法的第三次调用是通过onSizeChanged方法调用了,并且setUp方法的最后必须要调用invalidate方法请求系统绘制最新版的mBitmap。
(3)
if (mBitmap == null) {
System.out.println("mBitmap == null");
invalidate();
return;
}
如果控件设置图片为null,那么就不需要显示bitmap,其实这里根本不会出现图片为null的情况。 这里mBitmap为null,说明控件的图片没有被绘制出来,通知重新绘制。
(六)updateShaderMatrix 函数
我们知道控件的大小是一定的,width-padding才是可以显示图片的有效区域。那么图片也有大小,如何在有有限的区域内显示一张完整的图片呢?使用矩阵实现对图片的缩放,也就是updateShaderMatrix函数。