自定义圆形水波纹控件

效果图

在这里插入图片描述

思路

这个东西的整体思路如下

  1. 先画出后面的灰色背景圆
  2. 画出贝赛尔线(波浪线)这个要多画一些,从屏幕之外画出来以便后面做动画
  3. 画出来发现我们的波浪线有些不在灰色背景圆,我们这个时候用混合模式来删除掉不在背景圆里面的波浪线
  4. 动画通过修改 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 的钢笔工具那样,其他关于贝塞尔曲线还是看别人的解释吧~ https://blog.csdn.net/u013831257/article/details/51281136

@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 官方给的图有些坑,这里推荐大家看一下这篇文章了解一下~ https://blog.csdn.net/u013085697/article/details/52096703

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();
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
总共分为三层:一层为圆形边线,一层为进度边线,一层用来显示标识进度节点。 public class CircleProgressBar extends View { private int maxProgress = 100; private int progress = 15; private int progressStrokeWidth = 2; private int marxArcStorkeWidth = 16; // 画圆所在的距形区域 RectF oval; Paint paint; public CircleProgressBar(Context context, AttributeSet attrs) { super(context, attrs); // TODO Auto-generated constructor stub oval = new RectF(); paint = new Paint(); } @Override protected void onDraw(Canvas canvas) { // TODO 自动生成的方法存根 super.onDraw(canvas); int width = this.getWidth(); int height = this.getHeight(); width = (width > height) ? height : width; height = (width > height) ? height : width; paint.setAntiAlias(true); // 设置画笔为抗锯齿 paint.setColor(Color.WHITE); // 设置画笔颜色 canvas.drawColor(Color.TRANSPARENT); // 白色背景 paint.setStrokeWidth(progressStrokeWidth); // 线宽 paint.setStyle(Style.STROKE); oval.left = marxArcStorkeWidth / 2; // 左上角x oval.top = marxArcStorkeWidth / 2; // 左上角y oval.right = width - marxArcStorkeWidth / 2; // 左下角x oval.bottom = height - marxArcStorkeWidth / 2; // 右下角y canvas.drawArc(oval, -90, 360, false, paint); // 绘制白色圆圈,即进度条背景 paint.setColor(Color.rgb(0x57, 0x87, 0xb6)); paint.setStrokeWidth(marxArcStorkeWidth); canvas.drawArc(oval, -90, ((float) progress / maxProgress) * 360, false, paint); // 绘制进度圆弧,这里是蓝色 paint.setStrokeWidth(1); String text = progress + "%"; int textHeight = height / 4; paint.setTextSize(textHeight); int textWidth = (int) paint.measureText(text, 0, text.length()); paint.setStyle(Style.FILL); canvas.drawText(text, width / 2 - textWidth / 2, height / 2 + textHeight / 2, paint); } public int getMaxProgress() { return maxProgress; } public void setMaxProgress(int maxProgress) { this.maxProgress = maxProgress; } /** * 设置进度 * * @param progress * 进度百分比 * @param view * 标识进度的节点视图 */ public void setProgress(int progress, View view) { this.progress = progress; view.setAnimation(pointRotationAnima(0, (int) (((float) 360 / maxProgress) * progress))); this.invalidate(); } /** * 非UI线程调用 */ public void setProgressNotInUiThread(int progress, View view) { this.progress = progress; view.setAnimation(pointRotationAnima(0, (int) (((float) 360 / maxProgress) * progress))); this.postInvalidate(); } /** * 进度标注点的动画 * * @param fromDegrees * @param toDegrees * @return */ private Animation pointRotationAnima(float fromDegrees, float toDegrees) { int initDegress = 306;// 进度点起始位置(图片偏移约54度) RotateAnimation animation = new RotateAnimation(fromDegrees, initDegress + toDegrees, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); animation.setDuration(1);// 设置动画执行时间 animation.setRepeatCount(1);// 设置重复执行次数 animation.setFillAfter(true);// 设置动画结束后是否停留在结束位置 return animation; } }

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值