欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android自定义AvatarImageView实现头像显示效果

程序员文章站 2023-12-17 09:53:10
看看效果图: 我们项目中头像显示一般都是圆形的,但是有时候不排除各种样式(不一定是个规则的形状),比如 上次ui给了我一个 圆形下面少了一块。我们一般实现自定义形状的...

看看效果图:

Android自定义AvatarImageView实现头像显示效果

我们项目中头像显示一般都是圆形的,但是有时候不排除各种样式(不一定是个规则的形状),比如 上次ui给了我一个 圆形下面少了一块。我们一般实现自定义形状的图形有三种方式:porterduffxfermode 、bitmapshader、clippath。下面我都会分别说明,我这里实现使用的第一种方式(实现还是比较简单的)。

1.porterduffxfermode

  这是由tomas proter和 tom duff命名的图像转换模式,它有16个枚举值来控制canvas上 上下两个图层的交互(先画的图层在下层)。

Android自定义AvatarImageView实现头像显示效果

(蓝色的在上层)

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自定义AvatarImageView实现头像显示效果

②、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();
}

这种方式明显最简单,你还可以一个个坐标点的添加形成一个路径。但是形状比较复杂的情况下,还是第一种实现比较方便。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

上一篇:

下一篇: