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

能量球效果(贝塞尔曲线)

程序员文章站 2022-07-12 23:16:02
...

在项目中需要用能量球来展示活动余额的剩余量,如下图的效果:
能量球效果(贝塞尔曲线)
看到效果图,首先就会想怎么实现这个波浪的效果,其实这就是两个波浪线一直在左右移动,然后给你一个一直在那浪啊浪的错觉~~~说到曲线,脑阔中第一个蹦出来的就是贝塞尔曲线。要用贝塞尔曲线,首先还是要确定控制点,先看一张图(没有好的作图工具,凑合着看了- -!):
能量球效果(贝塞尔曲线)
如上图,一个完整的波要这5个点来确定,其中B和D点就是控制点,像我这个是将view的宽度均分成4段,这些点的坐标很容易算出来了。
下面贴出画波的主要代码:

    private float waveHeightPercent = 0.7f;   //波的高度百分比
    private float waveTranslateValue = 0f;    //波水平移动的比例
    private float waveAmplitude;              //波的振幅
    private int waveCount = 4;                //波的数量 

    //这边是为了保证view为正方形,方便后面圆形好弄
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int minSize = Math.min(widthSize, heightSize);
        setMeasuredDimension(minSize, minSize);
    }

    protected void onDraw(Canvas canvas) {
        float levelHeight = (1 - waveHeightPercent) * height;  //波实际高度
        float specWidth = width/waveCount;                     //四分之一波长
        float translateX = width * waveTranslateValue;         //水平移动的实际距离

        //这两个方法是计算波形路径
        setFrontWavePath(levelHeight, specWidth, translateX);
        setBehindWavePath(levelHeight, specWidth, translateX);
        //画后面的波
        wavePaint.setColor(ContextCompat.getColor(context, R.color.behind_color));
        canvas.drawPath(wavePathBehind, wavePaint);
        //画前面的波
        wavePaint.setColor(ContextCompat.getColor(context, R.color.front_color));
        canvas.drawPath(wavePathFront, wavePaint);
    }

下面只贴出setFrontWavePath()这个方法,setBehindWavePath()和它类似,只不过波的方向是反过来的:

private void setFrontWavePath(float levelHeight, float specWidth, float translateX){
        wavePathFront.reset();

        wavePathFront.moveTo(0 - translateX, height);
        wavePathFront.lineTo(0 - translateX, levelHeight);
        for(int i=1;i<=waveCount;i++){
            float controlX = specWidth * (i*2 - 1) - translateX;
            float controlY = i%2 != 0 ? levelHeight - waveAmplitude : levelHeight + waveAmplitude;
            float toX = specWidth * (2 * i) - translateX;
            wavePathFront.quadTo(controlX, controlY, toX, levelHeight);
        }
        wavePathFront.lineTo(specWidth * waveCount + translateX, height);
        wavePathFront.close();
    }

这边画波浪,用的二阶贝塞尔曲线,最后路径闭合,是为了填充颜色。
现在运行一下,基础波形就出来了,入下图:
能量球效果(贝塞尔曲线)
接下来就是加入动画效果,让波来回移动,代码如下:

   public void waveAnimate(){
        ObjectAnimator transAnim = ObjectAnimator.ofFloat(this, "waveTranslateValue", 0, 1);
        transAnim.setInterpolator(new LinearInterpolator());
        transAnim.setDuration(1000);
        transAnim.setRepeatCount(ValueAnimator.INFINITE);

        ObjectAnimator upAnim = ObjectAnimator.ofFloat(this, "waveHeightPercent", 0, waveHeightPercent);
        upAnim.setDuration(3000);

        animatorSet.playTogether(transAnim, upAnim);
        animatorSet.start();
    }

这边用属性动画来完成波的移动,waveTranslateValue这个属性,控制波的水平移动;waveHeightPercent属性,控制波的高度,现在来看下效果:
能量球效果(贝塞尔曲线)
到这里,剩下最后一个就是把矩形变成圆形,这边首先想到的就是裁剪画布,那就直接写上代码如下:

clipPath.addCircle(circleX, circleY, circleRadius, Path.Direction.CW);
canvas.clipPath(clipPath);

圆的坐标和半径根据view的宽高很容易得到,然后在onDraw中调用canvas.clipPath(clipPath),实际效果如下:
能量球效果(贝塞尔曲线)
从图中看到,圆有锯齿,裁剪没有用到Paint类,不好设置抗锯齿,所以效果不是很好。
下面用Paint 的Xfermode模式来修改一下,代码如下:

    //初始化Xfermode
    xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);

    protected void onDraw(Canvas canvas) {
        float levelHeight = (1 - waveHeightPercent) * height;
        float specWidth = width/waveCount;
        float translateX = width * waveTranslateValue;

        canvas.drawCircle(circleX, circleY, circleRadius, bgPaint);
        //创建新的图层,进行图像合成
        int saveCount = canvas.saveLayer(0, 0, width, height, wavePaint, Canvas.ALL_SAVE_FLAG);

        setFrontWavePath(levelHeight, specWidth, translateX);
        setBehindWavePath(levelHeight, specWidth, translateX);

        wavePaint.setColor(ContextCompat.getColor(context, R.color.behind_color));
        canvas.drawPath(wavePathBehind, wavePaint);

        wavePaint.setColor(ContextCompat.getColor(context, R.color.front_color));
        canvas.drawPath(wavePathFront, wavePaint);

        //设置Paint的Xfermode
        circlePaint.setXfermode(xfermode);
        if(circleBitmap == null){
            circleBitmap = createCircleBitmap();
        }
        canvas.drawBitmap(circleBitmap,circleX - circleRadius, 0, circlePaint);
        circlePaint.setXfermode(null);
        //释放图层
        canvas.restoreToCount(saveCount);
    }

    //创建圆形Bitmap
    private Bitmap createCircleBitmap(){
        Bitmap bitmap = Bitmap.createBitmap((int)circleDiameter, (int)circleDiameter, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(Color.WHITE);
        paint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(circleRadius, circleRadius,circleRadius, paint);
        return bitmap;
    }

上面onDraw中新增的代码都给出了注释,主要就是画圆,然后进行图像合成,改之后的效果如下:
能量球效果(贝塞尔曲线)
这下就没有锯齿了,到这就基本实现能量球效果。