当筵意气临九霄,星离雨散不终朝。这篇文章主要讲述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自定义控件水波加速球】
文章图片
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
谢谢
推荐阅读
- Android应用层View绘制流程之measure,layout,draw三步曲
- Eclipse导入Android工程报错 Invalid project description(转载)
- JAVA Eclipse如何开发Android的多页面程序
- 安卓改变窗体的大小
- JAVA Eclipse开发Android如何让超出界面的部分自动显示滚动条
- 解决Android下元素滑动问题
- 黎活明8天快速掌握android视频教程--24_网络通信之网页源码查看器
- 编译器设计教程目录
- 云计算开发教程目录