首页 > 代码库 > Android自定义控件水波加速球

Android自定义控件水波加速球

技术分享
通过上一篇的博客,相信你对Android中的坐标系和绘制刻度的实现原理有了一个认识(所以这一篇可能没有那么详细。。。),接下来就是另外一部分内容,如何去绘制水波加速球。

自定义View确定一个正方形

public class WaterView extends View {
    private int len;

    public WaterView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        //以最小值为正方形的长
        len = Math.min(width, height);
        //设置测量高度和宽度(必须要调用,不然无效果)
        setMeasuredDimension(len, len);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }
}

同样这里集成了View,并通过设置测量值,限定空间为正方形。
布局中使用:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/ll_parent"
    android:orientation="vertical"
    android:background="@color/colorPrimary"
    android:padding="20dp"
    tools:context="com.example.huaweiview.MainActivity">

    <com.example.huaweiview.WaterView
        android:layout_gravity="center"
        android:background="@color/colorAccent"
        android:layout_width="200dp"
        android:layout_height="300dp" />

</LinearLayout>

技术分享
ok,我们设置的长度和宽度并不一样,但是他显示的是一个正方形,并且,根据上一篇博客的介绍,它是有自己的坐标系的,我们绘制的所有东西都在这个坐标系内,并且依靠它去确定位置。

回忆正余弦

大家通过查资料和联想心电图等可以知道,水波其实就是在绘制一条正弦或者余弦波,如果让这条波移动就是
技术分享
这里我们使用正弦实现需要如下公式:
y = Asin(wx+b)+h ,这个公式里:w影响周期,A影响振幅,h影响y位置,b为初相;
画图就少不了要确定不同的点,通过这个公式,我们可以得到Y轴坐标点的值,那么X轴坐标点的值该如何得到呢?
通过观察图我们可以发现,这些点连起来就是一条曲线,也就是说每个点之间的距离是非常小的,是不是可以用,这些所有的点都在X轴上有值,刚好是i(i从0加到len的长度(View的长度也就是圆的直径))
技术分享
比如图中的中间点的坐标(y=0,x=i=len/2)
当然Y值是通过公式得到的,既然有很多点,我们就需要用数组来保存这些点,水波效果最好是有两条效果会好些,所以需要个数组:

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        //以最小值为正方形的长
        len = Math.min(width, height);
        //定义两个数组,保存Y值
        firstWaterLine = new float[len];
        secondWaterLine = new float[len];
        //设置测量高度和宽度(必须要调用,不然无效果)

        setMeasuredDimension(len, len);
    }

这里定义了两个全局数组,用来保存Y轴值,个数和View长度相等(直径相等)
然后就是利用公式获取每个点的Y轴坐标值

 @Override
    protected void onDraw(Canvas canvas) {
        // y = Asin(wx+b)+h ,这个公式里:w影响周期,A影响振幅,h影响y位置,b为初相;
        // 将周期定为view总宽度
        float mCycleFactorW = (float) (2 * Math.PI / len);

        // 得到第一条波的y值
        for (int i = 0; i < len; i++) {
            firstWaterLine[i] = (float) (10 * Math
                    .sin(mCycleFactorW * i));
        }
        // 得到第二条波的y值(第二条波的初相偏左)
        for (int i = 0; i < len; i++) {
            secondWaterLine[i] = (float) (15 * Math.sin(mCycleFactorW * i + 10));
        }
    }

在onDraw()方法中分别得到了两个数组中Y轴的值,并且将他一个周期的长度定位len(和直径相等)每个值都对应着一个X轴的值,也就是说我们得到两个正弦波上的所有点的坐标值了。并且第二天波偏左一点
而且还要增加一下他们的振幅,不然0-1之间的值太小,显示在屏幕上像一条直线。
技术分享
上图是什么意思呢?我们的水波效果是下面有填充色的,那么这些填充色其实就是波上的每个点,往View的底边画的一条一条的直线(数学中的细分法或者微积分吧)然后线就组成了面。
ok,划线需要知道起点坐标,和终点坐标,如图中的两个绿色的坐标点。(i,y)到(i,len);接下来开始画直线

public WaterView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

        waterPaint = new Paint();
        //抗锯齿
        waterPaint.setAntiAlias(true);
        waterPaint.setColor(Color.GREEN);

    }
 @Override
    protected void onDraw(Canvas canvas) {
        // y = Asin(wx+b)+h ,这个公式里:w影响周期,A影响振幅,h影响y位置,b为初相;
        // 将周期定为view总宽度
        float mCycleFactorW = (float) (2 * Math.PI / len);
        // 得到第一条波的y值
        for (int i = 0; i < len; i++) {
            firstWaterLine[i] = (float) (10 * Math
                    .sin(mCycleFactorW * i));
        }
        // 得到第二条波的y值
        for (int i = 0; i < len; i++) {
            secondWaterLine[i] = (float) (15 * Math.sin(mCycleFactorW * i + 10));

        }

        //第一条波的所有直线
        for (int i = 0; i < len; i++) {
            canvas.drawLine(i, firstWaterLine[i], i, len, waterPaint);
        }
        //第二条波的所有直线
        for (int i = 0; i < len; i++) {
            canvas.drawLine(i, secondWaterLine[i], i, len, waterPaint);
        }

    }

构造方法中实例化出了一个画笔对象,颜色为绿色,抗锯齿。在onDraw()方法中添加了两个画直线的方法,我们要对每个点都要绘制所以使用了循环。
技术分享
哎呀,这不是咱们想看到的效果呀,这是因为坐标的原点依旧在View的左上角,振幅小,都往下方画直线就覆盖了整个View,接下来我们把坐标系往下放移动len/2的距离,再去绘制:

 @Override
    protected void onDraw(Canvas canvas) {
        // y = Asin(wx+b)+h ,这个公式里:w影响周期,A影响振幅,h影响y位置,b为初相;
        // 将周期定为view总宽度
        float mCycleFactorW = (float) (2 * Math.PI / len);
        // 得到第一条波的y值
        for (int i = 0; i < len; i++) {
            firstWaterLine[i] = (float) (10 * Math
                    .sin(mCycleFactorW * i));
        }
        // 得到第二条波的y值
        for (int i = 0; i < len; i++) {
            secondWaterLine[i] = (float) (15 * Math.sin(mCycleFactorW * i + 10));

        }
        //保存原来的内容
       canvas.save();
       canvas.translate(0,len/2);
        //第一条波的所有直线
        for (int i = 0; i < len; i++) {
            canvas.drawLine(i, firstWaterLine[i], i, len, waterPaint);
        }
        //第二条波的所有直线
        for (int i = 0; i < len; i++) {
            canvas.drawLine(i, secondWaterLine[i], i, len, waterPaint);
        }
        //恢复到原来的状态(会自动结合绘制的内容)
        canvas.restore();;
    }

我们需要先保存画布的状态,再去移动坐标系,之后在恢复合并。
技术分享
现在是我们想看到的样子了,但是还有一点,我们希望他是一个圆形的,这时候就需要另外一个功能,cavans的剪切功能

 @Override
    protected void onDraw(Canvas canvas) {
        // y = Asin(wx+b)+h ,这个公式里:w影响周期,A影响振幅,h影响y位置,b为初相;
        // 将周期定为view总宽度
        float mCycleFactorW = (float) (2 * Math.PI / len);
        // 得到第一条波的y值
        for (int i = 0; i < len; i++) {
            firstWaterLine[i] = (float) (10 * Math
                    .sin(mCycleFactorW * i));
        }
        // 得到第二条波的y值
        for (int i = 0; i < len; i++) {
            secondWaterLine[i] = (float) (15 * Math.sin(mCycleFactorW * i + 10));

        }
        // 裁剪成圆形区域
        Path path = new Path();
        path.reset();
        float clipRadius=len/2;
        //添加圆形路径
        //Path.Direction.CCW逆时针
        //Path.Direction.CW顺时针
        path.addCircle(len / 2, len / 2, clipRadius, Path.Direction.CCW);
        // (剪裁路径)裁剪成圆形区域
        //(REPLACE用当前要剪切的区域代替画布中的内容的区域)
        canvas.clipPath(path, android.graphics.Region.Op.REPLACE);

       canvas.save();
       canvas.translate(0,len/2);

        //第一条波的所有直线
        for (int i = 0; i < len; i++) {
            canvas.drawLine(i, firstWaterLine[i], i, len, waterPaint);
        }
        //第二条波的所有直线
        for (int i = 0; i < len; i++) {
            canvas.drawLine(i, secondWaterLine[i], i, len, waterPaint);
        }
        canvas.restore();;
    }

在我们要移动画布之前,将View剪切成了圆形
点击了解
Region.Op

技术分享
在布局中我去除了,控件的背景,效果如图所示,接下来就是去控制让他动起来了,水平方向移动也就是每次初相都不相同,开启时间任务,让它的初相值不断变化(从右往左移动就加上一个数)

@Override
    protected void onDraw(Canvas canvas) {
        // y = Asin(wx+b)+h ,这个公式里:w影响周期,A影响振幅,h影响y位置,b为初相;
        // 将周期定为view总宽度
        float mCycleFactorW = (float) (2 * Math.PI / len);
        // 得到第一条波的y值
        for (int i = 0; i < len; i++) {
            //添加一个可变的初相值
            firstWaterLine[i] = (float) (10 * Math
                    .sin(mCycleFactorW * i+move));
        }
        // 得到第二条波的y值
        for (int i = 0; i < len; i++) {
            //添加一个可变的初相值
            secondWaterLine[i] = (float) (15 * Math.sin(mCycleFactorW * i + move+10));

        }
        // 裁剪成圆形区域
        Path path = new Path();
        path.reset();
        float clipRadius=len/2;
        //添加圆形路径
        //Path.Direction.CCW逆时针
        //Path.Direction.CW顺时针
        path.addCircle(len / 2, len / 2, clipRadius, Path.Direction.CCW);
        // (剪裁路径)裁剪成圆形区域
        //(REPLACE用当前要剪切的区域代替画布中的内容的区域)
        canvas.clipPath(path, android.graphics.Region.Op.REPLACE);

       canvas.save();
       canvas.translate(0,len/2);

        //第一条波的所有直线
        for (int i = 0; i < len; i++) {
            canvas.drawLine(i, firstWaterLine[i], i, len, waterPaint);
        }
        //第二条波的所有直线
        for (int i = 0; i < len; i++) {
            canvas.drawLine(i, secondWaterLine[i], i, len, waterPaint);
        }
        canvas.restore();;
    }

在onDraw()的方法中,获取坐标Y值的时候,添加一条可变的全局变量,动态改变初相值。开启时间任务:

 public void moveWaterLine() {
        final Timer timer = new Timer();
        timer.schedule(new TimerTask() {

            @Override
            public void run() {
                //不断改变初相
                move += 1;
                //重新绘制(子线程中调用)
                postInvalidate();
            }
        }, 500, 200);
    }

在时间任务中,这里没用去关闭时间任务,它会一直动,,动态去改变,并在构造方法中去调用
技术分享

效果已经很不错了,如何让它去增加和减少呢,让它从下往上增加,只要不断去影响Y的值就好了,
技术分享
如果坐标系不改变,绘制水波的时候还要判断是增加还是减少,为了方便计算,只需要将坐标系移动到底部就好了,为0的时候代表什么都没用,有的时候让Y值不断的减去一个值就实现了网上增加。

@Override
    protected void onDraw(Canvas canvas) {
        // y = Asin(wx+b)+h ,这个公式里:w影响周期,A影响振幅,h影响y位置,b为初相;
        // 将周期定为view总宽度
        float mCycleFactorW = (float) (2 * Math.PI / len);
        // 得到第一条波的y值
        for (int i = 0; i < len; i++) {
            //添加一个可变的初相值
            firstWaterLine[i] = (float) (10 * Math
                    .sin(mCycleFactorW * i + move) - up);
        }
        // 得到第二条波的y值
        for (int i = 0; i < len; i++) {
            //添加一个可变的初相值
            secondWaterLine[i] = (float) (15 * Math.sin(mCycleFactorW * i + move + 10) - up);

        }
        // 裁剪成圆形区域
        Path path = new Path();
        path.reset();
        float clipRadius = len / 2;
        //添加圆形路径
        //Path.Direction.CCW逆时针
        //Path.Direction.CW顺时针
        path.addCircle(len / 2, len / 2, clipRadius, Path.Direction.CCW);
        // (剪裁路径)裁剪成圆形区域
        //(REPLACE用当前要剪切的区域代替画布中的内容的区域)
        canvas.clipPath(path, android.graphics.Region.Op.REPLACE);

        canvas.save();
        canvas.translate(0, len);

        //第一条波的所有直线
        for (int i = 0; i < len; i++) {
            canvas.drawLine(i, firstWaterLine[i], i, len, waterPaint);
        }
        //第二条波的所有直线
        for (int i = 0; i < len; i++) {
            canvas.drawLine(i, secondWaterLine[i], i, len, waterPaint);
        }
        canvas.restore();
        ;
    }

在onDraw()方法中将坐标系移动到底部,并且声明一个全局变量up来动态改变Y值,因为是从下往上运动,所以是减去,开启时间任务:

//如果在运行,就不会执行下次动画
    private boolean isRunning;
    //判断是上升还是下降
    public int state = 1;

    public void change(final int trueAngle) {
        if (isRunning) {
            return;
        }
        final Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                switch (state) {
                    case 1:
                        isRunning = true;
                        up -= 10;
                        if (up <= 0) {
                            up = 0;
                            state = 2;
                        }
                        break;
                    case 2:
                        up += 10;
                        if (up >= trueAngle) {
                            up = trueAngle;
                            state = 1;
                            isRunning = false;
                            timer.cancel();
                        }
                        break;
                    default:
                        break;
                }

                postInvalidate();
            }
        }, 500, 30);

    }

声明一个boolean值来判断是否在运动,如果在动,就不进行下次运动,声明一个state变量来判断是上还是下
up值动态增加或减小,再重复绘制
activity调用

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
       final WaterView wv= (WaterView) findViewById(R.id.wv);
        wv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                wv.change(200);
            }
        });

    }
}

设置点击事件,调用动的方法
技术分享
终于大功告成了
目已经上传到github
github点击下载

最后的最后,个人淘宝店(抱歉,请见谅)。。霓裳雅阁
有喜欢的商品可以和我说下哦,,QQ:1070379530
谢谢

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    Android自定义控件水波加速球