首先来说说我最近遇到的奇葩事!
上了一周的火,患了牙周炎,吃药一直不好,不吃药了,居然好了,哈哈。。。
其次,就来说说为啥想写这篇博客?
好吧,因为面试的时候被问到了,问得自己哑口无言的,那么,为了增加自己的知识储备量,我需要动手实践一下啦。。。为了下次被问到的时候不再尴尬。。。也为了自己以后实现这类需求的时候能够信手拈来啊。。。
然后呢?我们就来说说实现思路啦!
其实,这个实现是有个开源项目的,那么,我们也就是一个学习的过程啦,学习过程,当然还是要记录一下的,方便以后自己回顾啊。。。下面我就说说自己依样画葫芦的过程以及过程中遇到的问题。
那么,大概的实现思路是什么呢?其实我有想到俩,第一个,很简单,就是在一个ImageView上面绘制一个遮罩,遮罩设置成和图片一样大小,但是只有中间的最大圆显示出来遮罩下面的东西,其余的地方都绘制成和承载背景一个颜色,那么就能看起来是个圆形图片啦。第二个,我们就只用绘制一层,避免过度绘制,就是把我们图片的承托工具绘制成圆形的,再把图片根据承托背景调整,最终显示,这样绘制出来的效果会比第一个好,既然说了这是按照开源项目依样画葫芦,那么肯定也按照他们的思路,用好的那个办法啦。。。下面说说“copy”过程。。。。
首先,就像很大自定义View一样,新建一个类,我们命名为CircleImageView,并实现前三个构造函数,然后呢,在我们attrs文件里面,添加我们可能会用到的基本属性啦。
<declare-styleable name="CircleImageView"> <attr name="border_width" format="dimension"/> <attr name="border_color" format="color"/> <attr name="border_overlay" format="boolean"/> </declare-styleable>
其次呢?我们就像自定义View一样啊,定义我们需要用到的一些变量。
private static final int COLOR_DRAWABLE_DIMENSION = 2; private static final int DEFAULT_BORDER_WIDTH = 0; private static final int DEFAULT_BORDER_COLOR = Color.BLACK; private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP; private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888; private static final boolean DEFAULT_BORDER_OVERLAY = false; private final RectF mDrawableRect = new RectF(); private final RectF mBorderRect = new RectF(); private final Matrix mShaderMatrix = new Matrix(); private int mBorderWidth = DEFAULT_BORDER_WIDTH; private int mBorderColor = DEFAULT_BORDER_COLOR; private Paint mBitmapPaint = new Paint(); private Paint mBorderPaint = new Paint(); private Bitmap mBitmap; private BitmapShader mBitmapShader; private int mBitmapWidth; private int mBitmapHeight; private float mDrawableRadius; private float mBorderRadius; private ColorFilter mColodrFilter; private boolean mReady; private boolean mSetUpPending; private boolean mBorderOverlay;
然后呢?在构造函数里面获取自定义参数。
public CircleImageView(Context context) { super(context); init(); } public CircleImageView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public CircleImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyleAttr, 0); mBorderWidth = typedArray.getDimensionPixelSize(R.styleable.CircleImageView_border_width, DEFAULT_BORDER_WIDTH); mBorderColor = typedArray.getColor(R.styleable.CircleImageView_border_color, DEFAULT_BORDER_COLOR); mBorderOverlay = typedArray.getBoolean(R.styleable.CircleImageView_border_overlay, DEFAULT_BORDER_OVERLAY); typedArray.recycle(); init(); }
等一下,init函数是什么?干什么用的呢?我们先看下代码。
private void init(){ super.setScaleType(SCALE_TYPE); mReady = true; if (mSetUpPending){ setUp(); mSetUpPending = false; } }
仔细研究发现,好像是有个变量初始化,还有个根据变量看是否执行setUp函数的,作用就是保证第一次执行setup函数里下面代码要在构造函数执行完毕时调用,那么,setUp函数又是干什么的呢?一连串的问号???继续看代码。
private void setUp(){ if (!mReady){ mSetUpPending = true; return; } if (mBitmap == null){ return; } // 构建渲染器,用mBitmap来填充绘制区域 ,参数值代表如果图片太小的话 就直接拉伸 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); mBitmapHeight = mBitmap.getHeight(); mBitmapWidth = mBitmap.getWidth(); mBorderRect.set(0, 0, getWidth(), getHeight()); //计算 圆形带边界部分(外圆)的最小半径,取mBorderRect的宽高减去一个边缘大小的一半的较小值(这个地方我比较纳闷为什么求外圆半径需要先减去一个边缘大小) mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2, (mBorderRect.width() - mBorderWidth) / 2); // 初始图片显示区域为mBorderRect(CircleImageView的布局实际大小) mDrawableRect.set(mBorderRect); if (!mBorderOverlay){ //通过inset方法 使得图片显示的区域从mBorderRect大小上下左右内移边界的宽度形成区域 mDrawableRect.inset(mBorderWidth, mBorderWidth); } //这里计算的是内圆的最小半径,也即去除边界宽度的半径 mDrawableRadius = Math.min(mDrawableRect.height() / 2, mDrawableRect.width() / 2); //设置渲染器的变换矩阵也即是mBitmap用何种缩放形式填充 updateShaderMatrix(); //手动触发ondraw()函数 完成最终的绘制 invalidate(); }
哦,原来是对代码自定义属性进行取值的,还有就是构建渲染器BitmapShader用Bitmap来填充绘制区域,设置样式和内外圆半径计算等,以及调用updateShaderMatrix()函数和 invalidate()函数;那么,updateShaderMatrix函数又是干什么的呢?一看名字就知道啦,肯定和Shader,Matrix 有关啦,对,他就是拿来计算缩放比例和平移位置,以及设置BitmapShader的Matrix 参数的。下面展示代码:
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.height() - mBitmapHeight * scale) * 0.5f; } else { scale = mDrawableRect.width() / (float) mBorderWidth; dy = (mDrawableRect.height() - mBorderWidth * scale) * 0.5f; } // 缩放 mShaderMatrix.setScale(scale, scale); //平移 mShaderMatrix.postTranslate((int) (dx + 0.5f) + mDrawableRect.left, (int)(dy + 0.5f) + mDrawableRect.top); //设置变换矩阵 mBitmapShader.setLocalMatrix(mShaderMatrix); }
再然后呢?想象一下,ImageView类的自定义控件,肯定是显示图片是主要部分啦,但是显示图片分为很多种类,根据BitMap,资源ID显示,根据Drawable显示,根据URL显示等。目前我们先实现这四种吧。。
@Override public void setImageBitmap(Bitmap bm) { super.setImageBitmap(bm); mBitmap = bm; setUp(); } @Override public void setImageDrawable(@Nullable Drawable drawable) { super.setImageDrawable(drawable); mBitmap = getBitmapFromDrawable(drawable); setUp(); } @Override public void setImageResource(@DrawableRes int resId) { super.setImageResource(resId); mBitmap = getBitmapFromDrawable(getDrawable()); setUp(); } @Override public void setImageURI(Uri uri) { super.setImageURI(uri); mBitmap = getBitmapFromDrawable(getDrawable()); setup(); }
看了以上代码以后,又有疑惑了,getBitmapFromDrawable 函数又是什么?呀,这就是个将Drawable转换为Bitmap的函数啦,代码实现如下。。。
private Bitmap getBitmapFromDrawable(Drawable drawable){ if (drawable == null){ return null; } if (drawable instanceof BitmapDrawable){ return ((BitmapDrawable) drawable).getBitmap(); } Bitmap bitmap; if (drawable instanceof ColorDrawable){ bitmap = Bitmap.createBitmap(COLOR_DRAWABLE_DIMENSION, COLOR_DRAWABLE_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; }
对了,千万记住,别忘了还有最重要的一步哟,那就是重写OnDraw函数啦,那onDraw函数里面到底实现了些什么呢?当然就是绘制啦,show time:
@Override protected void onDraw(Canvas canvas) { if (getDrawable() == null){ return; } canvas.drawCircle(getWidth() / 2, getHeight() / 2, mDrawableRadius, mBitmapPaint); if (mBorderWidth != 0){ canvas.drawCircle(getWidth() / 2, getHeight() / 2, mBorderRadius, mBorderPaint); } }
做完了以上几个步骤,那么剩下的,就是一些细枝末节的补充工作啦。。完整代码就不贴出了,我主要是介绍一下思路,完整代码Google一下,GitHub上有开源原项目代码哦。。。嘻嘻。。。
说完思路啦,最后呢?我们就来总结总结啦。。。
- 就如你们看到的一样,自定义的View,不论是ImageView还是TextView,都逃不过这几个步骤啦。
- 自定义View的属性
- 继承View,重写构造函数,并在构造函数里获取我们的自定义属性
- 重写onMeasure方法(由于这个我们需要控制显示大小以及形状,所以是要重写的,一般的自定义View不需要实现)
- 重写OnLayout方法(这一次我们没有用到,因为我们的布局很简单,没有强制设置该图片要定位在布局中的某个位置,不然,我们也需要实现的哟)
- 重写onDraw方法。。。这一步,一般都要有的,毕竟,我们自定义view就是需要显示我们想要显示的内容啊,那么如何显示呢,肯定是需要先有内容再显示,内容怎么来呢?绘制。。。所以,这一步基本上是必备的哟。。小伙伴们一定记住哟。。。