https://blog.csdn.net/YX_BB/article/details/104561359
在上篇文章中,我们实现了圆形图片圆角图片,基本上已经满足了日常开发中的需要。那如果想要更多的图形效果该怎么办呢?与我们在现实中的绘图方式一致,用笔(Paint)在纸(Canvas)上按照一定的路径(Path),即可形成特定的图画。一个图形的Path是固定,所以,我们可以通过改变Paint和Canvas来实现想要的效果。如下

这里以心形图片为例,展示几种不同的实现方法,首先先实现心形图片的Path。
公式:t 表示点(x,y)在坐标系中的角度
x = 16 sin^3 t
y = 13 cos t - 5 cos 2t - 2 cos 3t - cos 4t
/**
* 心形曲线
*/
private Path getHeartPath() {
int n = 100;
// 计算缩放比例
float scale = getWidth() / 17f / 2f;
// 将360度平均分为 100份,每个弧度对应一个点
float interval = (float) (2 * Math.PI / 100);
// 定义初始弧度degree
float degree = 0;
Point[] points = new Point[n];
for (int i = 0; i < n; i++) {
// 根据心形曲线公式,计算出每个弧度对应的点的坐标
// 当degree = 90度的时候,x取最大值16。当degree = 180度的时候,y取最小值 -17。
// 即保证y * scale * 17 * 2 = height的时候,曲线的与控件相切,以此计算出scale的值
float x = (float) ((16 * Math.pow(Math.sin(degree), 3)) * scale);
float y = (float) ((13 * Math.cos(degree) - 5 * Math.cos(2 * degree) - 2 * Math.cos(3 * degree) - Math.cos(4 * degree)) * scale);
points[i] = new Point(x + getWidth() / 2f, -y + getHeight() / 2f);
degree = degree + interval;
}
// 连线
Path path = new Path();
path.moveTo(points[0].x, points[0].y);
for (int i = 1; i < n; i++) {
path.lineTo(points[i].x, points[i].y);
}
path.close();
return path;
}
思路1:使用Canvas的clipPath方法直接剪裁Canvas。
优点简单粗暴,可以适应于各种控件且不需要考虑绘制中的各种情形。缺点是剪裁之后,Canvas的绘制区域将被Path限制,任何的内容及后续扩展都局限在了这个Path之内。且clipPath方法不支持硬件加速,当应用开启了硬件加速时,设备在4.0.4与4.0.3这样的版本上使用图片剪裁功能的时候会crash。
重写onDraw()方法
@Override
protected void onDraw(Canvas canvas) {
// 调用父类的绘制方法之前直接根据模式裁剪Canvas
canvas.clipPath(getHeartPath());
super.onDraw(canvas);
}
控件继承自ImageView,注意要在super.onDraw()之前剪裁Canvas。否则Canvas已经将图片绘制到了View中,这个时候再剪裁Canvas毫无意义
思路2:使用Paint.setShader方法,只给指定区域上色。
Shader在三维软件中称之为着色器,就是用来给空白图形上色用的。
关于Shader的详细介绍可以参考
https://blog.csdn.net/harvic880925/article/details/52039081
首先获取ImageView上的Drawable,将Drawable转化成Bitmap,然后给Paint设置BitmapShader着色器,Bitmap在指定Path中的区域将被上色,Path之外的区域会被忽略。这种方式不会改变Canvas,也不会对后续的绘制工作造成影响
重写onDraw()方法
@Override
protected void onDraw(Canvas canvas) {
if (mBitmap == null) {
mBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
Canvas drawCanvas = new Canvas(mBitmap);
Drawable drawable = getDrawable();
if (drawable != null) {
drawable.draw(drawCanvas);
}
mPaint.setShader(new BitmapShader(mBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT));
canvas.drawPath(getHeartPath(), mPaint);
}
}
注意:使用这种方式,不需要再使用super.onDraw(canvas)
思路3:使用PorterDuffXfermode混合图层
分别绘制图片图层,与路径图层。然后将两个图层混合。
@Override
protected void onDraw(Canvas canvas) {
// 图层混合绘制方式
srcBmp = makeSrc();
dstBmp = makeDst();
int layerID = canvas.saveLayer(0, 0, getWidth(), getHeight(), mPaint, Canvas.ALL_SAVE_FLAG);
canvas.drawBitmap(srcBmp, 0, 0, mPaint);
mPaint.setXfermode(xfermode);
canvas.drawBitmap(dstBmp, 0, 0, mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(layerID);
}
/**
* 源图层
*/
private Bitmap makeSrc() {
Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Drawable drawable = getDrawable();
if (drawable != null) {
drawable.draw(canvas);
}
return bitmap;
}
/**
* 目标图层
*/
private Bitmap makeDst() {
Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
canvas.drawPath(getHeartPath(), paint);
return bitmap;
}
注意:
int layerID = canvas.saveLayer(0, 0, getWidth(), getHeight(), mPaint, Canvas.ALL_SAVE_FLAG);
canvas.restoreToCount(layerID);
如果不加如这段代码,在进行图层混合的时候,源图层将会被直接绘制在原始Canvas上。然后再绘制目标图层的时候,会与整个原始Canvas取交集,这时会将非交集区域的颜色全部清空,从而漏出Activity的底色。

在加入这段代码之后,Canvas.saveLayer(),会生成新的全透明的Bitmap,后续的绘制都是在这个新图层上完成的。混合之后再绘制在原始Canvas上。这时边缘区域显示的颜色为ImageView父布局的背景色。简单理解两者的区别如下
未使用saveLayer时,3 - 2 - 1 = 0
而使用saveLayer时,3 - (2 - 1) = 2
显然两者的计算结果不一样了,而后者才是我们真正需要的绘制流程。
关于saveLayer()的用法可以参考
https://blog.csdn.net/harvic880925/article/details/51317746
完整代码
public class ShapeImageView extends AppCompatImageView {
private Paint mPaint;
private Bitmap srcBmp;
private Bitmap dstBmp;
private Xfermode xfermode;
public ShapeImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initData();
}
private void initData() {
mPaint = new Paint();
xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
}
@Override
protected void onDraw(Canvas canvas) {
// 图层混合绘制方式
srcBmp = makeSrc();
dstBmp = makeDst();
int layerID = canvas.saveLayer(0, 0, getWidth(), getHeight(), mPaint, Canvas.ALL_SAVE_FLAG);
canvas.drawBitmap(srcBmp, 0, 0, mPaint);
mPaint.setXfermode(xfermode);
canvas.drawBitmap(dstBmp, 0, 0, mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(layerID);
// BitmapShader绘制方式
// if (srcBmp == null) {
// srcBmp = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
// Canvas drawCanvas = new Canvas(srcBmp);
// Drawable drawable = getDrawable();
// if (drawable != null) {
// drawable.draw(drawCanvas);
// }
//
// mPaint.setShader(new BitmapShader(srcBmp, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT));
// canvas.drawPath(getHeartPath(), mPaint);
// }
// clipPath绘制方式
// canvas.clipPath(getHeartPath());
// super.onDraw(canvas);
}
/**
* 源图层
*/
private Bitmap makeSrc() {
Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Drawable drawable = getDrawable();
if (drawable != null) {
drawable.draw(canvas);
}
return bitmap;
}
/**
* 目标图层
*/
private Bitmap makeDst() {
Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
canvas.drawPath(getHeartPath(), paint);
return bitmap;
}
/**
* 画心形曲线
*/
private Path getHeartPath() {
int n = 100;
// 计算缩放比例
float scale = getWidth() / 17f / 2f;
// 将360度平均分为 100份,每个弧度对应一个点
float interval = (float) (2 * Math.PI / 100);
// 定义初始弧度degree
float degree = 0;
Point[] points = new Point[n];
for (int i = 0; i < n; i++) {
// 根据心形曲线公式,计算出每个弧度对应的点的坐标
// 当degree = 90度的时候,x取最大值16。当degree = 180度的时候,y取最小值 -17。
// 即保证y * scale * 17 * 2 = height的时候,曲线的与控件相切,以此计算出scale的值
float x = (float) ((16 * Math.pow(Math.sin(degree), 3)) * scale);
float y = (float) ((13 * Math.cos(degree) - 5 * Math.cos(2 * degree) - 2 * Math.cos(3 * degree) - Math.cos(4 * degree)) * scale);
points[i] = new Point(x + getWidth() / 2f, -y + getHeight() / 2f);
degree = degree + interval;
}
// 连线
Path path = new Path();
path.moveTo(points[0].x, points[0].y);
for (int i = 1; i < n; i++) {
path.lineTo(points[i].x, points[i].y);
}
path.close();
return path;
}
public static class Point {
float x;
float y;
public Point(float x, float y) {
this.x = x;
this.y = y;
}
}
}
这篇博客详细介绍了在Android中如何实现心形图片,包括三种不同的实现方法:使用Canvas的clipPath方法剪裁Canvas,通过Paint.setShader只给指定区域上色,以及利用PorterDuffXfermode混合图层。每种方法都有其优缺点,并提供了相应的代码实现。

6905

被折叠的 条评论
为什么被折叠?



