前言
在上一篇博客 Android 圆形头像的两种实现方式 中,我们塔伦了实现圆形头像的两种实现方式。
-
第一种: 使用 Paint 的 Xfermode 实战
-
第二种: 使用 BitmapShader 实现
今天,让我们一起来看一下怎样实现正 N 变形圆角头像的实现。
在讲解之前,让我们先来看一下怎样使用我们的控件
老规矩,在讲解怎样实现以前,我们先一起来看一下怎样使用我们的自定义控件。
自定义属性说明
1<attr name="type"> 2 <enum name="circle" value="0" /> 3 <enum name="round" value="1" /> 4 <enum name="polygon" value="2"/> 5</attr> 6 7<declare-styleable name="MultiImageView"> 8 <attr name="type"/> 9 <attr name="miv_border_width" format="dimension" />10 <attr name="miv_border_color" format="color" />11 <attr name="miv_border_overlay" format="boolean" />12 <attr name="miv_fill_color" format="color" />13 <attr name="miv_corner_radius" format="dimension"/>14 <attr name="miv_sides" format="integer"/>15 <attr name="miv_rotate_angle" format="float"/>16</declare-styleable>
| 参数 | 说明 |
|---|---|
| type | 相应的值有 circle,round,polygon |
| miv_border_width | 表示边界 Path 的宽度 (默认值是 0 ) |
| miv_border_color | 表示边界 Path 的 Color |
| miv_border_overlay | 表示边界 Path 是否要覆盖在图片上面 |
| miv_fill_color | 表示填充圆的颜色,默认是 Translate,即不可见 |
| miv_corner_radius | 只有当 type round 或者 polygon 的时候才生效,表示边界 Path 圆角半径的大小, |
| miv_sides | 正 N 边形的变数,只有 type 为 polygon 的时候,该属性才生效 |
| miv_rotate_angle | 旋转的角度,只有 type 为 polygon 的时候,该属性才生效 |
指定圆形头像
1<com.xj.shapeview.MultiImageView2 android:layout_width="100dp"3 android:layout_height="100dp"4 android:src="@mipmap/tanyan"5 app:type="circle"6/>
指定圆角矩形
1<com.xj.shapeview.MultiImageView2 android:layout_marginLeft="15dp"3 android:layout_width="100dp"4 android:layout_height="100dp"5 android:src="@mipmap/tanyan"6 app:type="round"7 app:miv_corner_radius="15dp"/>
指定正 N 边形
正五边形
1<com.xj.shapeview.MultiImageView2 android:layout_width="100dp"3 android:layout_height="100dp"4 android:src="@mipmap/tanyan"5 app:type="polygon"6 app:miv_sides="5"7 app:miv_corner_radius="25dp"/>
如果需要其旋转相应的角度,我们只需指定 app:miv_rotate_angle="180" 即可,这里以 180 度为列子讲解说明
如果需要正六边形,只需要更改为 app:miv_sides="6"
效果图
相应的布局文件实现
1<?xml version="1.0" encoding="utf-8"?> 2 3<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" 4 xmlns:app="http://schemas.android.com/apk/res-auto" 5 xmlns:tools="http://schemas.android.com/tools" 6 android:layout_width="match_parent" 7 android:layout_height="match_parent"> 8 9 10 <GridLayout 11 android:columnCount="3" 12 android:rowCount="3" 13 14 android:layout_width="match_parent" 15 android:layout_height="match_parent" 16 android:orientation="vertical" 17 > 18 19 20 <com.xj.shapeview.MultiImageView 21 android:layout_width="100dp" 22 android:layout_height="100dp" 23 android:src="@mipmap/tanyan" 24 app:type="circle" 25 app:miv_sides="6" 26 app:miv_corner_radius="15dp"/> 27 28 <com.xj.shapeview.MultiImageView 29 android:layout_marginLeft="15dp" 30 android:layout_width="100dp" 31 android:layout_height="100dp" 32 android:src="@mipmap/tanyan" 33 app:miv_sides="5" 34 app:type="round" 35 app:miv_corner_radius="15dp"/> 36 37 <com.xj.shapeview.MultiImageView 38 android:layout_width="100dp" 39 android:layout_height="100dp" 40 android:src="@mipmap/tanyan" 41 app:type="polygon" 42 app:miv_sides="5" 43 app:miv_corner_radius="25dp"/> 44 45 <com.xj.shapeview.MultiImageView 46 android:layout_width="100dp" 47 android:layout_height="100dp" 48 android:src="@mipmap/tanyan" 49 app:type="polygon" 50 app:miv_sides="5" 51 52 app:miv_corner_radius="25dp" 53 app:miv_rotate_angle="180"/> 54 55 <com.xj.shapeview.MultiImageView 56 android:layout_width="100dp" 57 android:layout_height="100dp" 58 android:src="@mipmap/tanyan" 59 app:miv_sides="7" 60 app:type="polygon" 61 app:miv_corner_radius="0dp" 62 app:miv_rotate_angle="0"/> 63 64 65 <com.xj.shapeview.MultiImageView 66 android:layout_width="100dp" 67 android:layout_height="100dp" 68 android:src="@mipmap/tanyan" 69 app:miv_sides="6" 70 app:type="polygon" 71 app:miv_corner_radius="0dp" 72 app:miv_border_overlay="true" 73 app:miv_fill_color="@color/colorAccent"/> 74 75 <com.xj.shapeview.MultiImageView 76 android:layout_width="100dp" 77 android:layout_height="100dp" 78 android:src="@mipmap/tanyan" 79 app:miv_sides="6" 80 app:type="polygon" 81 app:miv_corner_radius="0dp" 82 app:miv_rotate_angle="0" 83 app:miv_border_overlay="true" 84 app:miv_border_width="1dp" 85 app:miv_border_color="@android:color/darker_gray" 86 /> 87 88 89 <com.xj.shapeview.MultiImageView 90 android:layout_width="100dp" 91 android:layout_height="100dp" 92 android:src="@mipmap/tanyan" 93 app:miv_sides="6" 94 app:type="polygon" 95 app:miv_corner_radius="0dp" 96 app:miv_rotate_angle="0" 97 app:miv_border_overlay="false" 98 app:miv_border_width="1dp" 99 app:miv_border_color="@android:color/black"100 />101102103104 <com.xj.shapeview.MultiImageView105 android:layout_width="100dp"106 android:layout_height="100dp"107 android:src="@mipmap/tanyan"108 app:miv_sides="7"109 app:type="polygon"110 app:miv_corner_radius="10dp"111 app:miv_rotate_angle="0"112113 />114115116 </GridLayout>117</ScrollView>
正 N 边形圆角头像的实现原理分析
要实现正 N 变形主要有几个难点
-
怎样让我们的头像变成正 N 边形
-
怎样绘制正 N 边形
-
怎样绘制带圆角的正 N 边形
怎样让我们的头像变成正 N 边形?
其实这个问题在上篇博客已经讲到,有两种实现方式。
-
第一种: 使用 Paint 的 Xfermode 实战
-
第二种: 使用 BitmapShader 实现
今天,这边博客主要以 BitmapShader 为例子实现。
核心代码实现
1mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 2mBitmapPaint.setAntiAlias(true); 3mBitmapPaint.setShader(mBitmapShader); 4 5@Override 6protected void onDraw(Canvas canvas) { 7 8 ----- 910 Path path = getPath(canvas,mType,(int)mDrawableRadius*2,(int)mDrawableRadius*2,mDrawableRadius,mSides,mCornerRadius);1112 canvas.drawPath(path,mBitmapPaint);13}
核心思路分析:
-
拿到 Bitmap,并使用 BitmapShader 进行包装
-
将 mBitmapShader 设置给画笔 Paint
-
第三步,在 onDraw 方法,将其绘制出来
怎样绘制正 N 边形
这里的思想主要来自该博客 如何用Canvas画一个正多边形
数学原理分析
首先,我们先来看一张图片
从图中可以看一看到,我们若想绘制出一个正 N 边形,那么我们只需要计算出各个点的坐标,然后使用 Path 连接起来即可。
那我们要怎样计算出各个点的坐标呢
-
从图中不难得出,圆心角 a 的度数为 360/n,弧度计算为 2π/n
-
如果把圆心的坐标为(0,0),那么顶点P1的坐标为[X1=cos(a),Y1=sin(a)]。
-
以此类推,顶点Pn坐标为[Xn=cos(an),Yn=sin(an)]。圆心的实际坐标是外接矩形的中心:[Ox=(rect.right+rect.left)/2 , Oy=(rect.top+rect.bottom)/2]。所以Pn的实际坐标是[Xn+Ox,Yn+Oy]。
最后我们把把 P0-P1…Pn 连起来,就是我们要的结果了。
核心伪代码实现
1float a = 2π / n ; // 角度 2Path path = new Path(); 3for( int i = 0; i < = n; i++ ){ 4 float x = R * cos(a * i); 5 float y = R * sin(a * i); 6 if (i = 0){ 7 path.moveTo(x,y); // 移动到第一个顶点 8 }else{ 9 path.lineTo(x,y); // 10 }11}12drawPath(path);
实际代码实现
在上面的例子中,我们假设我们的圆形坐标是 (0,0), 但实际上并不是,实际上在 Android 中我们的圆心坐标是 (width/2,height/2)。因此,我们在计算坐标的时候需要加上
1圆心坐标2float mX = (rect.right + rect.left) / 2;3float my = (rect.top + rect.bottom) / 2;45// PN点的 x,y 坐标6float nextX = mX + Double.valueOf(r * Math.cos(alpha)).floatValue();7float nextY = my + Double.valueOf(r * Math.sin(alpha)).floatValue();
当然我们这里以可以用 canvas 的 translate 方法来移动。
1public static void drawPolygon (RectF rect, Canvas canvas, Paint paintByLevel, int number) { 2 if(number < 3) { 3 return; 4 } 5 float r = (rect.right - rect.left) / 2; 6 float mX = (rect.right + rect.left) / 2; 7 float my = (rect.top + rect.bottom) / 2; 8 Path path = new Path(); 9 for (int i = 0; i <= number; i++) {10 // - 0.5 : Turn 90 ° counterclockwise11 float alpha = Double.valueOf(((2f / number) * i - 0.5) * Math.PI).floatValue();12 float nextX = mX + Double.valueOf(r * Math.cos(alpha)).floatValue();13 float nextY = my + Double.valueOf(r * Math.sin(alpha)).floatValue();14 if (i == 0) {15 path.moveTo(nextX, nextY);16 } else {17 path.lineTo(nextX, nextY);18 }19 }20 canvas.drawPath(path, paintByLevel);21}
怎样绘制带有圆角的正 N 边形
这个问题我一开始的思路是根据圆形的半径,然后计算出各个点的坐标,接着使用 path 中的 addArc() 方法来绘制。但是在计算各个点的坐标的时候,遇到很多难度,最后无法得出。
后面查阅了 Android 官方的文档,发现了有这样一个方法
PathEffect setPathEffect (PathEffect effect)
从字面意思很容易理解,就是设置 PathEffect,可以对 Path 产生相应的影响。
那这个 PathEffect 又是什么东东呢?
public class PathEffect extends Object
Known Direct SubclassesComposePathEffect,CornerPathEffect,DashPathEffect,DiscretePathEffect,PathDashPathEffect,SumPathEffect
从官方文档可以了解到是继承于 Object 的,实现的子类有 ComposePathEffect, CornerPathEffect, DashPathEffect 等。
看到这里的时候你有没有突然有一种醍醐灌顶的感觉? 这个 CornerPathEffect 是不是就可以实现呢?没错,确实可以实现,而且贼简单。
核心代码只有这几句,就可以让我们绘制出的正 N 边形具有圆角
1CornerPathEffect cornerPathEffect = new CornerPathEffect(mCornerRadius);2mBitmapPaint.setPathEffect(cornerPathEffect);
代码实现细节注意事项
当空间的宽度和高度不一致的时候,半径怎样取值?
这里我们选择宽度和高度值较小的一个,然后除以2
1mDrawableRadius = Math.min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f);
当图片较大的时候,会不会发生 OOM
当图片较大的时候,我们会对其进行相应的缩放,采用的是矩阵的方法
1private void updateShaderMatrix() { 2 float scale; 3 float dx = 0; 4 float dy = 0; 5 6 mShaderMatrix.set(null); 7 8 if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) { 9 scale = mDrawableRect.height() / (float) mBitmapHeight;10 dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f;11 } else {12 scale = mDrawableRect.width() / (float) mBitmapWidth;13 dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f;14 }1516 mShaderMatrix.setScale(scale, scale);17 mShaderMatrix.postTranslate((int) (dx + 0.5f) + mDrawableRect.left, (int) (dy + 0.5f) + mDrawableRect.top);181920 mBitmapShader.setLocalMatrix(mShaderMatrix);21}
自定义控件怎样支持 padding 属性
在绘制图片的时候,我们对其进行相应的处理,确保我们的坐标是正确的。
1float left = getPaddingLeft() + (availableWidth - sideLength) / 2f;2float top = getPaddingTop() + (availableHeight - sideLength) / 2f;
1case CIRCLY: 2 ClipHelper.setCirclePath(path,width,height); 3 break; 4case RECTAHGE: 5 ClipHelper.setRectangle(path,calculateBounds(),cornerRadius); 6 break; 7case POLYGON: 8 ClipHelper.setPolygon(path,calculateBounds(),sides,mRotateAngles); 9 break;10111213private RectF calculateBounds() {14 int availableWidth = getWidth() - getPaddingLeft() - getPaddingRight();15 int availableHeight = getHeight() - getPaddingTop() - getPaddingBottom();1617 int sideLength = Math.min(availableWidth, availableHeight);1819 float left = getPaddingLeft() + (availableWidth - sideLength) / 2f;20 float top = getPaddingTop() + (availableHeight - sideLength) / 2f;2122 return new RectF(left, top, left + sideLength, top + sideLength);23}
正 N 边形的角度旋转是怎样实现的。
其实,这里,我们采用的是矩阵的方式进行旋转的,调用 path.transform 方法
1Matrix matrix = new Matrix();2matrix.postRotate(rotateAngle,mX,my);3path.transform(matrix);
题外话
在开发的时候,一刚开始说要实现圆角六边形的时候,查阅了相关的资料,知道有两种方法
-
第一种方法,让 UI 设计师直接给图, 使用 Paint 的 Xfermode 实现
-
第二种方法:直接绘制 Path;
那时候项目比较赶,采用的是第一种方式实现。不过作为一名程序猿,感觉采用第一种方法实现,总感觉有点 low。后面晚上下班的时候,查阅了相关的资料,最终终于实现了上述的效果。
这种正 N 边形圆角头像的效果,说难也不难,说容易也不容易。因为里面综合了很多知识点,需要一步步去处理。(比如怎样绘制正 N 边形,怎样支持圆角,怎样处理 Padding 等等)。
最后,给大家推荐 github 上面的一个开源库。ShapeOfView,里面实现了很多常见的图片(心形,五角星,六角形等)
https://github.com/florent37/ShapeOfView
欢迎大家关注我的微信公众号,扫一扫下方二维码或搜索微信号 Android 技术人,即可关注。 目前专注于 Android 开发,主要分享 Android开发相关知识和一些相关的优秀文章,包括个人总结,职场经验等。