Android自定义AvatarImageView实现头像显示效果
看看效果图:
我们项目中头像显示一般都是圆形的,但是有时候不排除各种样式(不一定是个规则的形状),比如 上次ui给了我一个 圆形下面少了一块。我们一般实现自定义形状的图形有三种方式:porterduffxfermode 、bitmapshader、clippath。下面我都会分别说明,我这里实现使用的第一种方式(实现还是比较简单的)。
1.porterduffxfermode
这是由tomas proter和 tom duff命名的图像转换模式,它有16个枚举值来控制canvas上 上下两个图层的交互(先画的图层在下层)。
(蓝色的在上层)
1.porterduff.mode.clear 所绘制不会提交到画布上
2.porterduff.mode.src 显示上层绘制图片
3.porterduff.mode.dst 显示下层绘制图片
4.porterduff.mode.src_over 正常绘制显示,上下层绘制叠盖。
5.porterduff.mode.dst_over 上下层都显示。下层居上显示。
6.porterduff.mode.src_in 取两层绘制交集。显示上层。
7.porterduff.mode.dst_in 取两层绘制交集。显示下层。
8.porterduff.mode.src_out 取上层绘制非交集部分。
9.porterduff.mode.dst_out 取下层绘制非交集部分。
10.porterduff.mode.src_atop 取下层非交集部分与上层交集部分
11.porterduff.mode.dst_atop 取上层非交集部分与下层交集部分
12.porterduff.mode.xor 异或:去除两图层交集部分
13.porterduff.mode.darken 取两图层全部区域,交集部分颜色加深
14.porterduff.mode.lighten 取两图层全部,点亮交集部分颜色
15.porterduff.mode.multiply 取两图层交集部分叠加后颜色
16.porterduff.mode.screen 取两图层全部区域,交集部分变为透明色
1.1思路
会玩ps的朋友肯定知道,如果有两个图层,我们想把上面图层裁切成下面图层的形状,只需要调下面图层的选区,然后选中上面的图层,蒙板就可以了。
那么我们就可以利用porterduff.mode的 src_in 或 dst_in 来取得两个图层的交集,从而把图像裁切成我们想要的各种样式。我们需要一个形状图层和一个显示图层。并且显示图层要全面覆盖形状图层。
1.2 实现
继承imageview,复写了imageview的四个setimage方法(为了更好的兼容性),在setimagedrawable方法中得到前景图片。
@override public void setimagebitmap(bitmap bm) { super.setimagebitmap(bm); mbitmap = bm; setbitmaps(); } /** * 系统会调用这个方法设置前景 src */ @override public void setimagedrawable(drawable drawable) { super.setimagedrawable(drawable); mbitmap = getbitmapfromdrawable(drawable); setbitmaps(); } @override public void setimageresource(int resid) { super.setimageresource(resid); mbitmap = getbitmapfromdrawable(getdrawable()); setbitmaps(); } @override public void setimageuri(uri uri) { super.setimageuri(uri); mbitmap = getbitmapfromdrawable(getdrawable()); setbitmaps(); }
获取背景图层,invalidate()会调用ondraw方法重绘。
private void setbitmaps(){ if(null==getbackground()){ throw new illegalargumentexception(string.format("background is null.")); }else{ backgroundbitmap = getbitmapfromdrawable(getbackground()); invalidate(); } }
当然要在onmeasure获取view的高度和宽度,以便对两个图层进行缩放(一般来说头像显示view都是个正方形)。
@override protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { super.onmeasure(widthmeasurespec,heightmeasurespec); /** * 获得控件的宽高,默认measurespec.exactly ( match_parent , accurate ) * 并且布局文件中应该设置 控件的宽高相等 */ viewwidth = measurespec.getsize(widthmeasurespec); viewheight = measurespec.getsize(heightmeasurespec); }
然后就是使用porterduffxfermode使两个图层交互得到最终的bitmap。
private bitmap createimage() { paint paint = new paint(); paint.setantialias(true); bitmap finalbmp = bitmap.createbitmap(viewwidth,viewheight, bitmap.config.argb_8888); /** * 产生一个同样大小的画布 */ canvas canvas = new canvas(finalbmp); /** * 首先背景图片 */ canvas.drawbitmap(backgroundbitmap, 0, 0, paint); /** * 使用src_in,取两层绘制交集,显示上层。 */ paint.setxfermode(new porterduffxfermode(porterduff.mode.src_in)); /** * 绘制前景图片 */ canvas.drawbitmap(mbitmap, 0, 0, paint); return finalbmp; }
开始重绘(主要是进行缩放和把最终的图像绘制在view上显示)。
@override protected void ondraw(canvas canvas) { if(mbitmap!=null && backgroundbitmap!=null){ /** * 对图片给进行缩放 */ int min = math.min(viewwidth, viewheight); backgroundbitmap = bitmap.createscaledbitmap(backgroundbitmap, min, min, false); mbitmap = bitmap.createscaledbitmap(mbitmap, min, min, false); /** * 把最后的bitmap画上去 */ canvas.drawbitmap(createimage(), 0, 0, null); } }
贴上工具函数
/** * drawable转bitmap */ private bitmap getbitmapfromdrawable(drawable drawable) { super.setscaletype(scaletype.center_crop); if (drawable == null) { return null; } if (drawable instanceof bitmapdrawable) { return ((bitmapdrawable) drawable).getbitmap(); } try { bitmap bitmap; bitmap = bitmap.createbitmap(drawable.getintrinsicwidth(), drawable.getintrinsicheight(),bitmap.config.argb_8888); canvas canvas = new canvas(bitmap); drawable.setbounds(0, 0, canvas.getwidth(), canvas.getheight()); drawable.draw(canvas); return bitmap; } catch (outofmemoryerror e) { return null; } }
是不是很简单,然后就是使用了,
1.3 使用
没有新增任何属性值,在布局中使用如下。
<cn.fanrunqi.avatarimageview.avatarimageview android:layout_width="100dp" android:layout_height="100dp" android:background="@drawable/oval_shape" <!--android:background="@drawable/bg_a"--> android:src="@drawable/c" />
①、这里的android:background定义的就是我们的形状图层,它可以是一个xxx_shape.xml的布局文件,比如。
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval" android:uselevel="false"> <solid android:color="#fff"/> <size android:width="100dp" android:height="100dp"/> </shape>
也可以是 一个 图片(注意是xx.png,可包含透明像素,如)
②、android:src定义的就是我们要显示的头像
2.bitmapshader
我们可以称为“着色器”,通过下面代码可以得到一个着色器。
mbitmapshader = new bitmapshader(mbitmap, shader.tilemode.clamp, shader.tilemode.clamp);
参数
① mbitmap:你要绘制的图片
② emun shader.tilemode 定义了三种着色模式:
clamp 拉伸
repeat 重复
mirror 镜像
好比你拿一张分辨率和电脑屏幕不一样的图片设置为壁纸时,选择的三种方式一样。
我们可以给画笔设置着色器,这样画笔就能在 canvas的相应形状上画出我们的图片mbitmap。
mbitmappaint.setshader(mbitmapshader);
canvas.drawcircle(getwidth() / 2, getheight() / 2, mdrawableradius, mbitmappaint);
当然我们一般设置的模式为clamp 拉伸(当图片mbitmap的宽高小于view的时候要拉伸),但是我们一般不拉伸(变形了),所以一般还要给着色器设置一个matrix,去适当的放大或者缩小图片。
mbitmapshader.setlocalmatrix(mshadermatrix);
2.1 circleimageview源码分析
著名的项目circleimageview就是用着色器实现的,实现思路上面已经说了,代码有详细的注释,理解起来应该没什么问题。
public class circleimageview extends imageview { //缩放类型 private static final scaletype scale_type = scaletype.center_crop; 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 boolean default_border_overlay = false; private final rectf mdrawablerect = new rectf(); private final rectf mborderrect = new rectf(); private final matrix mshadermatrix = new matrix(); //这个画笔最重要的是关联了mbitmapshader 使canvas在执行的时候可以切割原图片(mbitmapshader是关联了原图的bitmap的) private final paint mbitmappaint = new paint(); //这个描边,则与本身的原图bitmap没有任何关联, private final paint mborderpaint = new paint(); //这里定义了 圆形边缘的默认宽度和颜色 private int mbordercolor = default_border_color; private int mborderwidth = default_border_width; private bitmap mbitmap; private bitmapshader mbitmapshader; // 位图渲染 private int mbitmapwidth; // 位图宽度 private int mbitmapheight; // 位图高度 private float mdrawableradius;// 图片半径 private float mborderradius;// 带边框的的图片半径 private colorfilter mcolorfilter; //初始false private boolean mready; private boolean msetuppending; private boolean mborderoverlay; //构造函数 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); typedarray a = context.obtainstyledattributes(attrs, r.styleable.circleimageview, defstyle, 0); //通过typedarray提供的一系列方法getxxxx取得我们在xml里定义的参数值; // 获取边界的宽度 mborderwidth = a.getdimensionpixelsize(r.styleable.circleimageview_border_width, default_border_width); // 获取边界的颜色 mbordercolor = a.getcolor(r.styleable.circleimageview_border_color, default_border_color); mborderoverlay = a.getboolean(r.styleable.circleimageview_border_overlay, default_border_overlay); //调用 recycle() 回收typedarray,以便后面重用 a.recycle(); init(); } /** * 作用就是保证第一次执行setup函数里下面代码要在构造函数执行完毕时调用 */ private void init() { //在这里scaletype被强制设定为center_crop,就是将图片水平垂直居中,进行缩放。 super.setscaletype(scale_type); mready = true; if (msetuppending) { setup(); msetuppending = false; } } @override public scaletype getscaletype() { return scale_type; } /** * 这里明确指出 此种imageview 只支持center_crop 这一种属性 * * @param scaletype */ @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) { //如果图片不存在就不画 if (getdrawable() == null) { return; } //绘制内圆形 图片 画笔为mbitmappaint canvas.drawcircle(getwidth() / 2, getheight() / 2, mdrawableradius, mbitmappaint); //如果圆形边缘的宽度不为0 我们还要绘制带边界的外圆形 边界画笔为mborderpaint if (mborderwidth != 0) { canvas.drawcircle(getwidth() / 2, getheight() / 2, 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(int bordercolor) { if (bordercolor == mbordercolor) { return; } mbordercolor = bordercolor; mborderpaint.setcolor(mbordercolor); invalidate(); } public void setbordercolorresource( int bordercolorres) { setbordercolor(getcontext().getresources().getcolor(bordercolorres)); } 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(); } /** * 以下四个函数都是 * 复写imageview的setimagexxx()方法 * 注意这个函数先于构造函数调用之前调用 * @param bm */ @override public void setimagebitmap(bitmap bm) { super.setimagebitmap(bm); mbitmap = bm; setup(); } @override public void setimagedrawable(drawable drawable) { super.setimagedrawable(drawable); mbitmap = getbitmapfromdrawable(drawable); setup(); } @override public void setimageresource( int resid) { super.setimageresource(resid); mbitmap = getbitmapfromdrawable(getdrawable()); setup(); } @override public void setimageuri(uri uri) { super.setimageuri(uri); mbitmap = getbitmapfromdrawable(getdrawable()); setup(); } @override public void setcolorfilter(colorfilter cf) { if (cf == mcolorfilter) { return; } mcolorfilter = cf; mbitmappaint.setcolorfilter(mcolorfilter); invalidate(); } /** * drawable转bitmap * @param drawable * @return */ private bitmap getbitmapfromdrawable(drawable drawable) { if (drawable == null) { return null; } if (drawable instanceof bitmapdrawable) { //通常来说 我们的代码就是执行到这里就返回了。返回的就是我们最原始的bitmap 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 (outofmemoryerror e) { return null; } } /** * 这个函数很关键,进行图片画笔边界画笔(paint)一些重绘参数初始化: * 构建渲染器bitmapshader用bitmap来填充绘制区域,设置样式以及内外圆半径计算等, * 以及调用updateshadermatrix()函数和 invalidate()函数; */ private void setup() { //因为mready默认值为false,所以第一次进这个函数的时候if语句为真进入括号体内 //设置msetuppending为true然后直接返回,后面的代码并没有执行。 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(); // 设置含边界显示区域,取的是circleimageview的布局实际大小,为方形 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) { //demo里始终执行 //通过inset方法 使得图片显示的区域从mborderrect大小上下左右内移边界的宽度形成区域 mdrawablerect.inset(mborderwidth, mborderwidth); } //这里计算的是内圆的最小半径,也即去除边界宽度的半径 mdrawableradius = math.min(mdrawablerect.height() / 2, mdrawablerect.width() / 2); //设置渲染器的变换矩阵也即是mbitmap用何种缩放形式填充 updateshadermatrix(); //手动触发ondraw()函数 完成最终的绘制 invalidate(); } /** * 这个函数为设置bitmapshader的matrix参数,设置最小缩放比例,平移参数。 * 作用:保证图片损失度最小和始终绘制图片正*的那部分 */ private void updateshadermatrix() { float scale; float dx = 0; float dy = 0; mshadermatrix.set(null); // 这里不好理解 这个不等式也就是(mbitmapwidth / mdrawablerect.width()) > (mbitmapheight / mdrawablerect.height()) //取最小的缩放比例 if (mbitmapwidth * mdrawablerect.height() > mdrawablerect.width() * mbitmapheight) { //y轴缩放 x轴平移 使得图片的y轴方向的边的尺寸缩放到图片显示区域(mdrawablerect)一样) scale = mdrawablerect.height() / (float) mbitmapheight; dx = (mdrawablerect.width() - mbitmapwidth * scale) * 0.5f; } else { //x轴缩放 y轴平移 使得图片的x轴方向的边的尺寸缩放到图片显示区域(mdrawablerect)一样) scale = mdrawablerect.width() / (float) mbitmapwidth; dy = (mdrawablerect.height() - mbitmapheight * scale) * 0.5f; } // shaeder的变换矩阵,我们这里主要用于放大或者缩小。 mshadermatrix.setscale(scale, scale); // 平移 mshadermatrix.posttranslate((int) (dx + 0.5f) + mdrawablerect.left, (int) (dy + 0.5f) + mdrawablerect.top); // 设置变换矩阵 mbitmapshader.setlocalmatrix(mshadermatrix); } }
3.clippath
我们知道如下,可以新建一个路径,然后通过path.addxxx()方法得到一个路径。
path path = new path();
然后可以把canvas修剪成我们路径区域的形状,就是画布形状已经确定,最后只用把图形画上去就行了。
以下代码可以把图形bitmap画在一个圆上,得到一个圆形头像。
@override protected void ondraw(canvas canvas) { path path = new path(); //按照逆时针方向添加一个圆 path.addcircle(float x, float y, mradius, direction.ccw); //先将canvas保存 canvas.save(); //把canvas修剪成指定的路径区域 canvas.clippath(path); //绘制图形bitmap canvas.drawbitmap(bitmap,float left, float top, mpaint); //恢复canvas canvas.restore(); }
这种方式明显最简单,你还可以一个个坐标点的添加形成一个路径。但是形状比较复杂的情况下,还是第一种实现比较方便。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
推荐阅读
-
Android自定义AvatarImageView实现头像显示效果
-
Android自定义View实现仿驾考宝典显示分数效果(收藏)
-
基于Android自定义控件实现刮刮乐效果
-
Android 自定义view实现进度条加载效果实例代码
-
Android自定义Drawable实现圆角效果
-
Android实现个人资料页面头像背景模糊显示包(状态栏)
-
Android自定义实现顶部粘性下拉刷新效果
-
Android编程使用自定义shape实现shadow阴影效果的方法
-
Android自定义ViewGroup实现堆叠头像的点赞Layout
-
Android 使用自定义RecyclerView控件实现Gallery效果