效果图
思路
这个东西的整体思路如下
- 先画出后面的灰色背景圆
- 画出贝赛尔线(波浪线)这个要多画一些,从屏幕之外画出来以便后面做动画
- 画出来发现我们的波浪线有些不在灰色背景圆,我们这个时候用混合模式来删除掉不在背景圆里面的波浪线
- 动画通过修改 path 的起点位置来做动画
正餐
先画背景圆 (伪代码)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int centerX = getWidth() / 2;
int centerY = getHeight() / 2;
int radius = centerX;
/**
* 设置背景圆
*/
bgPaint.setStyle(Paint.Style.FILL);
//抗锯齿
bgPaint.setAntiAlias(true);
bgPaint.setFilterBitmap(true);
bgPaint.setColor(Color.parseColor("#224D4D4D"));
canvas.drawCircle(centerX, centerY, centerX,bgPaint);
}
效果图:
画波浪线
用贝赛尔曲线画出波浪的形状,大概就跟 PS 的钢笔工具那样
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//省略其他代码...
//设置波浪线画笔
ripplePaint.setStyle(Paint.Style.FILL);
//抗锯齿
ripplePaint.setAntiAlias(true);
ripplePaint.setFilterBitmap(true);
ripplePaint.setColor(Color.BLUE);
//设置画笔前要先清除画笔原来的路径 不然做动画后面很尴尬的
ripplePath.reset();
//将路径起点移动到这个点
ripplePath.moveTo(0, centerY);
//画两条相连的二阶赛尔线 waveValue 是波浪线的峰值 这个值是自己设置的
ripplePath.rQuadTo(radius / 2, waveValue, radius, 0);
ripplePath.rQuadTo(radius / 2, -waveValue, radius, 0);
//连接闭合路径 这两个顺序别整错了
ripplePath.lineTo(getWidth(), getHeight());
ripplePath.lineTo(0, getHeight());
//连接起点和终点 闭合路径
ripplePath.close();
canvas.drawPath(ripplePath, ripplePaint);
}
效果图大概是这样
这个时候会发现如下图所示下留下的四个角是我们所不愿意让波浪线画到的区域
这个时候我们就需要想办法把波浪线画到这四个角的区域去掉,这个时候我们就需要引出 Android 绘图的混合模式之 PorterDuffXfermode 。这个 PorterDuffXfermode 官方给的图有些坑, Android 提供的 PorterDuffXfermode 的模式中不存在只保留两个图形的交集。就是官方提供的图片(下图)红色圈住的这两种情况是不能通过 SrcIn 模式或者 DstIn 模式做到的。
而实际上他的这样的
你说气不气!人骗了我两三个钟,害我纳闷了这么久。所以我就采取了 Clear 来做图形的减法。
好了那么问题就变成了: 如何画上上上图说的那个四个角?
我的思路是:画背景圆上下两个半圆弧,然后分别连接我们这个自定义 View 的四个顶点。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//省略其他代码...
//设置画笔
deletePaint.setStyle(Paint.Style.FILL);
//抗锯齿
deletePaint.setAntiAlias(true);
deletePaint.setFilterBitmap(true);
deletePaint.setColor(Color.BLACK);
//要有一个清除路径的好习惯~
deleteTopAnglePath.reset();
deleteBottomAnglePath.reset();
//上半圆弧
deleteTopAnglePath.addArc(new RectF(0, 0, getWidth(), getHeight()), 0, -180);
deleteTopAnglePath.lineTo(0, 0);
deleteTopAnglePath.lineTo(getWidth(), 0);
deleteTopAnglePath.close();
//下半圆弧
deleteBottomAnglePath.addArc(new RectF(0, 0, getWidth(), getHeight()), 0, 180);
deleteBottomAnglePath.lineTo(0, getHeight());
deleteBottomAnglePath.lineTo(getWidth(), getHeight());
deleteBottomAnglePath.close();
//画出区域来
canvas.drawPath(deleteTopAnglePath,deletePaint);
canvas.drawPath(deleteBottomAnglePath,deletePaint);
}
可以想象到效果吧,大概是这样的
哇咔咔,那么我们可以来做 Clear 操作啦~ 稍微修改一下我们的代码就ok了
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//省略其他代码...
//存一下目前的画布 这个画布要在两个图形画出来之前保存
int layoutId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
//画波浪线
canvas.drawPath(ripplePath, ripplePaint);
//设置画笔
deletePaint.setStyle(Paint.Style.FILL);
//抗锯齿
deletePaint.setAntiAlias(true);
deletePaint.setFilterBitmap(true);
deletePaint.setColor(Color.BLACK);
//要有一个清除路径的好习惯~
deleteTopAnglePath.reset();
deleteBottomAnglePath.reset();
//上半圆弧
deleteTopAnglePath.addArc(new RectF(0, 0, getWidth(), getHeight()), 0, -180);
deleteTopAnglePath.lineTo(0, 0);
deleteTopAnglePath.lineTo(getWidth(), 0);
deleteTopAnglePath.close();
//下半圆弧
deleteBottomAnglePath.addArc(new RectF(0, 0, getWidth(), getHeight()), 0, 180);
deleteBottomAnglePath.lineTo(0, getHeight());
deleteBottomAnglePath.lineTo(getWidth(), getHeight());
deleteBottomAnglePath.close();
//设置清除模式
deletePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
//画出区域来
canvas.drawPath(deleteTopAnglePath, deletePaint);
canvas.drawPath(deleteBottomAnglePath, deletePaint);
//清除掉设置的混合模式
deletePaint.setXfermode(null);
//还原一下之前保存的画布
canvas.restoreToCount(layoutId);
}
大概是下图这个样子,有点雏形了吧
接下来,我们要做的是,如何让他动起来。我们把视线移动到他画波浪线那一块,那一块有一行代码(下图圈出来的代码)
我们画的两条相连的贝塞尔曲线的起点就是这行代码。想象一下,如果我们将这个起点的 x轴 坐标向右移动,我们的贝塞尔曲线就会跟着向右移动,这个时候我们要是多左边多画一点贝塞尔曲线呢?那好像就动起来了,这个时候还要保证起点和终点落在同一个点上,才不会别扭。
这个时候需要一些牛逼的工具来完成我的画图,但是我牛逼不来,所以就整了一个手动稿,大家将就一下,等什么时候我会用那些牛逼的工具再来补不上~ 为我劣质的画功以及丑陋的字在这里先跟大家道个歉理一下理工男的字
再补一下拙劣的PS画出来的拙劣的东西
好了,来发挥一下想象力,我们 A 点到 E 点为一个波长,然后我们去移动我们一大整条贝塞尔线,将 A 点移动到原来的 E 点的位置,整个过程就是波浪的动画了~ 然后这个时候问题就变成,如何移动我们的 A 点到 E 点。 靠的就是我上面说的 path.moveTo() 这个方法了,接下来就是代码了
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//省略其他代码...
//将路径起点移动到这个点
ripplePath.moveTo(moveX - getWidth(), centerY);
//画两次
for (int i = 0; i < 2; i++) {
//画两条相连的二阶贝赛尔线 waveValue 是波浪线的峰值 这个值是自己设置的
//如果你想波浪多一些的话可以再自己多画一些,在一个波长内
ripplePath.rQuadTo(radius / 2, waveValue, radius, 0);
ripplePath.rQuadTo(radius / 2, -waveValue, radius, 0);
}
}
//启动动画
public void startAnim() {
//值动画 从0-getWidth()
ValueAnimator moveXAnimator = ValueAnimator.ofInt(0, getWidth());
//动画间隔时间
moveXAnimator.setDuration(2000);
//时间插值器
moveXAnimator.setInterpolator(new LinearInterpolator());
//循环
moveXAnimator.setRepeatCount(ValueAnimator.INFINITE);
moveXAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//当前的值给 moveX
moveX = (int) animation.getAnimatedValue();
postInvalidate();
}
});
//启动动画
moveXAnimator.start();
}
然后再 Activity 点击 View 然后去启动动画
效果大概是这样
好了,这个就是我们最后的效果了,如果先做波浪上升动画我们改变 moveTo(x,y) 中的的 y 值就行了~
下面是整个自定义控件的代码
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.LinearInterpolator;
public class CircleRippleView extends View {
//背景圆画笔
private Paint bgPaint;
//波浪线画笔
private Paint ripplePaint;
//波浪线路径
private Path ripplePath;
//删除不在圆内的波浪线
private Path deleteTopAnglePath;
private Path deleteBottomAnglePath;
private Paint deletePaint;
//波浪线峰值常数
private int waveValue = 60;
private int moveX;
public CircleRippleView(Context context) {
super(context);
}
public CircleRippleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initPaint();
}
private void initPaint() {
//背景圆画笔
bgPaint = new Paint();
//波浪线画笔
ripplePaint = new Paint();
//波浪线路径
ripplePath = new Path();
//删除不在圆内的波浪线
deleteTopAnglePath = new Path();
deleteBottomAnglePath = new Path();
deletePaint = new Paint();
}
public CircleRippleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int centerX = getWidth() / 2;
int centerY = getHeight() / 2;
int radius = centerX;
/**
* 设置背景圆
*/
bgPaint.setStyle(Paint.Style.FILL);
//抗锯齿
bgPaint.setAntiAlias(true);
bgPaint.setFilterBitmap(true);
bgPaint.setColor(Color.parseColor("#224D4D4D"));
canvas.drawCircle(centerX, centerY, centerX, bgPaint);
//设置波浪线画笔
ripplePaint.setStyle(Paint.Style.FILL);
//抗锯齿
ripplePaint.setAntiAlias(true);
ripplePaint.setFilterBitmap(true);
ripplePaint.setColor(Color.BLUE);
//设置画笔前要先清除画笔原来的路径 不然做动画后面很尴尬的
ripplePath.reset();
//将路径起点移动到这个点
ripplePath.moveTo(moveX - getWidth(), centerY);
//画两次
for (int i = 0; i < 2; i++) {
//画两条相连的二阶贝赛尔线 waveValue 是波浪线的峰值 这个值是自己设置的
//如果你想波浪多一些的话可以再自己多画一些,在一个波长内
ripplePath.rQuadTo(radius / 2, waveValue, radius, 0);
ripplePath.rQuadTo(radius / 2, -waveValue, radius, 0);
}
//连接闭合路径 这两个顺序别整错了
ripplePath.lineTo(getWidth(), getHeight());
ripplePath.lineTo(0, getHeight());
//连接起点和终点 闭合路径
ripplePath.close();
//存一下目前的画布
int layoutId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
//画波浪线
canvas.drawPath(ripplePath, ripplePaint);
//设置画笔
deletePaint.setStyle(Paint.Style.FILL);
//抗锯齿
deletePaint.setAntiAlias(true);
deletePaint.setFilterBitmap(true);
deletePaint.setColor(Color.BLACK);
deleteTopAnglePath.reset();
deleteBottomAnglePath.reset();
//上半圆弧
deleteTopAnglePath.addArc(new RectF(0, 0, getWidth(), getHeight()), 0, -180);
deleteTopAnglePath.lineTo(0, 0);
deleteTopAnglePath.lineTo(getWidth(), 0);
deleteTopAnglePath.close();
//下半圆弧
deleteBottomAnglePath.addArc(new RectF(0, 0, getWidth(), getHeight()), 0, 180);
deleteBottomAnglePath.lineTo(0, getHeight());
deleteBottomAnglePath.lineTo(getWidth(), getHeight());
deleteBottomAnglePath.close();
//设置清除模式
deletePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
//画出区域来
canvas.drawPath(deleteTopAnglePath, deletePaint);
canvas.drawPath(deleteBottomAnglePath, deletePaint);
//清除掉设置的混合模式
deletePaint.setXfermode(null);
//还原一下画布
canvas.restoreToCount(layoutId);
}
public void startAnim() {
ValueAnimator moveXAnimator = ValueAnimator.ofInt(0, getWidth());
moveXAnimator.setDuration(2000);
moveXAnimator.setInterpolator(new LinearInterpolator());
moveXAnimator.setRepeatCount(ValueAnimator.INFINITE);
moveXAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
moveX = (int) animation.getAnimatedValue();
postInvalidate();
}
});
moveXAnimator.start();
}
}