Android自定义控件水波加速球

当筵意气临九霄,星离雨散不终朝。这篇文章主要讲述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>

【Android自定义控件水波加速球】
Android自定义控件水波加速球

文章图片

ok, 我们设置的长度和宽度并不一样, 但是他显示的是一个正方形, 并且, 根据上一篇博客的介绍, 它是有自己的坐标系的, 我们绘制的所有东西都在这个坐标系内, 并且依靠它去确定位置。
回忆正余弦
大家通过查资料和联想心电图等可以知道, 水波其实就是在绘制一条正弦或者余弦波, 如果让这条波移动就是
Android自定义控件水波加速球

文章图片

这里我们使用正弦实现需要如下公式:
y = Asin(wx+ b)+ h , 这个公式里: w影响周期, A影响振幅, h影响y位置, b为初相;
画图就少不了要确定不同的点, 通过这个公式, 我们可以得到Y轴坐标点的值, 那么X轴坐标点的值该如何得到呢?
通过观察图我们可以发现, 这些点连起来就是一条曲线, 也就是说每个点之间的距离是非常小的, 是不是可以用, 这些所有的点都在X轴上有值, 刚好是i(i从0加到len的长度( View的长度也就是圆的直径) )
Android自定义控件水波加速球

文章图片

比如图中的中间点的坐标(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之间的值太小, 显示在屏幕上像一条直线。
Android自定义控件水波加速球

文章图片

上图是什么意思呢? 我们的水波效果是下面有填充色的, 那么这些填充色其实就是波上的每个点, 往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()方法中添加了两个画直线的方法, 我们要对每个点都要绘制所以使用了循环。
Android自定义控件水波加速球

文章图片

哎呀, 这不是咱们想看到的效果呀, 这是因为坐标的原点依旧在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(); ; }

我们需要先保存画布的状态, 再去移动坐标系, 之后在恢复合并。
Android自定义控件水波加速球

文章图片

现在是我们想看到的样子了, 但是还有一点, 我们希望他是一个圆形的, 这时候就需要另外一个功能, 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
Android自定义控件水波加速球

文章图片

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

在时间任务中, 这里没用去关闭时间任务, 它会一直动, ,动态去改变, 并在构造方法中去调用
Android自定义控件水波加速球

文章图片

效果已经很不错了, 如何让它去增加和减少呢, 让它从下往上增加, 只要不断去影响Y的值就好了,
Android自定义控件水波加速球

文章图片

如果坐标系不改变, 绘制水波的时候还要判断是增加还是减少, 为了方便计算, 只需要将坐标系移动到底部就好了, 为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); } }); } }

设置点击事件, 调用动的方法
Android自定义控件水波加速球

文章图片

终于大功告成了
目已经上传到github
github点击下载
最后的最后, 个人淘宝店( 抱歉, 请见谅) 。。霓裳雅阁
有喜欢的商品可以和我说下哦,,QQ:1070379530
谢谢

    推荐阅读