你们好,我是深红骑士,爱开玩笑,技术一渣渣,热爱钻研,这篇文章是今年的最后一篇了,首先祝你们在新的一年里心想事成,诸事顺利。今天来学习贝塞尔曲线,以前一直想学,惋惜没时间。什么是贝塞尔曲线呢?一开始我也是不懂的,当查了不少资料,如今仍是不够了解,其推导公式仍是不能深刻了解。对发布这曲线的法国工程师皮埃尔·贝塞尔
由衷敬佩,贝塞尔曲线,又称贝兹曲线或者贝济埃曲线,是应用于二维图形
应用程序的数学曲线.1962年,皮埃尔·贝塞尔
运用贝塞尔曲线为汽车的主体进行设计,贝塞尔曲线最初由Paul de Casteljau于1959年运用de Casteljau
算法开发,以稳定的述职方法求出贝塞尔曲线。其实贝塞尔曲线就在咱们平常生活中,如一些成熟的位图软件中:PhotoSHop,Flash5等。在前端开发中,贝塞尔曲线也是无处不在:前端2D或者3D图形图标库都会使用贝塞尔曲线;它能够用来绘制曲线,在svg和canvas中,原生提供的曲线绘制都是用贝塞尔曲线实现的;在css的transition-timing-function
属性,可使用贝塞尔曲线来描述过渡的缓动计算。css
贝塞尔曲线是用一系列点来控制曲线状态的,我将这一系列点分为三个点:起点
,终点
,控制点
。经过改变这些点,贝塞尔曲线就会发生变化。前端
一阶曲线就是一条直线,只有两个点,就是起点
和终点
,也就是最终效果就是一条线段。仍是直接上图比较直观:java
二阶曲线由两个数据点(起始点和终点),一个控制点来描述曲线状态,以下图,下面A点是起始点,C是终点,B是控制点。android
起点
和
终点
不断变化的一阶贝塞尔曲线。二阶公式以下:
三阶曲线其实就是由两个数据点(起始点和终点),两个控制点来描述曲线的状态,以下图,下面A是起始点,D是终点,B和C是控制点。git
在Android中,Path类中有四个方法与贝塞尔曲线相关的,也就是已经封装了关于贝塞尔曲线的函数,开发者直接调用便可:github
//二阶贝赛尔
public void quadTo(float x1, float y1, float x2, float y2);
public void rQuadTo(float dx1, float dy1, float dx2, float dy2);
//三阶贝赛尔
public void cubicTo(float x1, float y1, float x2, float y2,float x3, float y3);
public void rCubicTo(float x1, float y1, float x2, float y2,float x3, float y3);
复制代码
上面的四个函数中,quadTo、rQuadTo是二阶贝塞尔曲线,cubicTo、rCubicTo是三阶贝塞尔曲线。由于三阶贝塞尔曲线使用方法和二阶贝塞尔曲线类似,用处也不多,就不细说了。下面就针对二阶的贝塞尔曲线quadTo、rQuadTo为详细说明。算法
先看看quadTo函数的定义:canvas
/** * Add a quadratic bezier from the last point, approaching control point * (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for * this contour, the first point is automatically set to (0,0). * * @param x1 The x-coordinate of the control point on a quadratic curve * @param y1 The y-coordinate of the control point on a quadratic curve * @param x2 The x-coordinate of the end point on a quadratic curve * @param y2 The y-coordinate of the end point on a quadratic curve */
public void quadTo(float x1, float y1, float x2, float y2) {
isSimplePath = false;
nQuadTo(mNativePath, x1, y1, x2, y2);
}
复制代码
看上面的注释能够知道:(x1,y1)是控制点,(x2,y2)是终点坐标,怎么没有起点的坐标呢?做为Android开发者都知道,一条线段的起始点都是经过Path.move(x,y)来指定的。若是连续调用quadTo函数,那么前一个quadTo的终点就是下一个quadTo函数的起始点,若是初始化没有调用Path.moveTo(x,y)来指定起始点,那么控件视图就会以左上角(0,0)为起始点,仍是直接上例子描述。 下面实现绘制下面如下效果图:缓存
public class PathView extends View {
//画笔
private Paint paint;
//路径
private Path path;
public PathView(Context context) {
super(context);
}
public PathView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
//重写onDraw方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setStyle(Paint.Style.STROKE);
//线条宽度
paint.setStrokeWidth(10);
paint.setColor(Color.RED);
//设置起始点的位置为(200,400)
path.moveTo(200,400);
//线条p0-p2控制点(300,300) 终点位置(400,400)
path.quadTo(300,300,400,400);
//线条p2-p4控制点(500,500) 终点位置(600,400)
path.quadTo(500,500,600,400);
canvas.drawPath(path, paint);
}
private void init() {
paint = new Paint();
path = new Path();
}
}
复制代码
布局文件以下:app
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000">
<Button
android:id="@+id/btn_reset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginTop="10dp"
android:text="清空路径"
/>
<com.example.okhttpdemo.PathView
android:id="@+id/path_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/btn_reset"
android:background="#000000"/>
</android.support.constraint.ConstraintLayout>
复制代码
效果图以下图所示:
path.moveTo(200,400);
注释,再看看效果:
下面来看看Path.lineTo和Path.quadTo的区别,Path.lineTo是链接直线,是链接上一个点到当前点的之间的直线,下面来实现绘制手指在屏幕上所走的路径,也不难就在上面的基础上增长onTouchEvent
方法便可,代码以下:
public class PathView extends View {
//画笔
private Paint paint;
//路径
private Path path;
public PathView(Context context) {
super(context);
}
public PathView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
//重写onDraw方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setStyle(Paint.Style.STROKE);
//线条宽度
// paint.setStrokeWidth(10);
paint.setColor(Color.RED);
canvas.drawPath(path, paint);
}
private void init() {
paint = new Paint();
path = new Path();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
Log.d("ssd","触发按下");
path.moveTo(event.getX(), event.getY());
return true;
}
case MotionEvent.ACTION_MOVE:
Log.d("ssd","触发移动");
path.lineTo(event.getX(), event.getY());
invalidate();
break;
default:
break;
}
return super.onTouchEvent(event);
}
public void reset() {
path.reset();
invalidate();
}
}
复制代码
直接上效果图:
MotionEvent.DOWN
这个条件,而后调用
path.move(event.getX(),event.getY())
,当用户移动手指时,就用
path.lineTo(event.getX,event.getY())
将各个点链接起来,而后调用
invalidate
从新绘制。这里简单说一下在
MotionEvent.ACTION_DOWN
为何要返回
return true
。
return true
表示当前的控件已经消费了按下事件,剩下的
ACTION_UP
和
ACTION_MOVE
都会被执行;若是在
case MotionEvent.ACTION_DOWN
下返回
return false
,后续的
MOTION_MOVE
,
MMOTION_UP
都不会被接收到,由于没有消费
ACTION_DOWN
,系统就会认为
ACTION_DOWN
没有发生过,因此
ACTION_MOVE
和
ACTION_UP
就不能捕获,下面把图放大仔细看:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
Log.d("ssd","触发按下");
path.moveTo(event.getX(), event.getY());
//保存这个点的坐标
mBeforeX = event.getX();
mBeforeY = event.getY();
return true;
}
case MotionEvent.ACTION_MOVE:
Log.d("ssd","触发移动");
//移动的时候绘制二阶曲线
//终点是线段的中点
endX = (mBeforeX + event.getX()) / 2;
endY = (mBeforeY + event.getY()) / 2;
//绘制二阶曲线
path.quadTo(mBeforeX,mBeforeY,endX,endY);
//而后更新前一点的坐标
mBeforeX = event.getX();
mBeforeY = event.getY();
invalidate();
break;
default:
break;
}
return super.onTouchEvent(event);
}
复制代码
这里简单说明一下,在ACTION_DOWN
的时候,先调用path.moveTo(event.getX(), event.getY());
设置曲线的初始位置就是手指触屏的位置,上面也解释了若是不调用moveTo(event.getX(), event.getY())
的话,那么绘制点就会从控件的(0,0)开始。用mBeforeX
和mBeforeY
记录手指移动的前一个横纵坐标,而这个点是作控制点,最后返回return true
为了让ACTION_MOVE
和ACTION_UP
向本控件传递。下面说说在ACTION_MOVE
方法的逻辑处理,首先是肯定结束点,上面也说告终束点是线段的中间位置,因此用了两条公式来endX = (mBeforeX + event.getX()) / 2;
和endY = (mBeforeY + event.getY()) / 2;
求这个中间位置的横纵坐标,而控制点就是上个手指触摸屏幕的位置,后面就是更新前一个手指坐标。这里注意一下,上面也说了当连续调用quardTo
的时候,第一个起始点是Path.moveTo(x,y)
来设置的,其余部分,前面调用quadTo
的终点是下一个quard
的起点,这里所说的起始点就是上一个线段的中间点。上面的逻辑用一句话表示:把各个线段的中间点做为起始点和终点,把前一个手指位置做为控制点,最终效果以下:
quadT
实现的曲线会更顺滑。
直接看这个函数的说明:
/** * Add a quadratic bezier from the last point, approaching control point * (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for * this contour, the first point is automatically set to (0,0). * * @param x1 The x-coordinate of the control point on a quadratic curve * @param y1 The y-coordinate of the control point on a quadratic curve * @param x2 The x-coordinate of the end point on a quadratic curve * @param y2 The y-coordinate of the end point on a quadratic curve */
public void quadTo(float x1, float y1, float x2, float y2) {
isSimplePath = false;
nQuadTo(mNativePath, x1, y1, x2, y2);
}
复制代码
path.moveTo(100,200);
path.quadTo(200,100,300,400);
复制代码
path.moveTo(100,200);
path.rQuadTo(100,-100,200,200);
复制代码
在上面中,用quadTo
实现了一个波浪线,下图:
quadTo
实现的代码:
//重写onDraw方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setStyle(Paint.Style.STROKE);
//线条宽度
paint.setStrokeWidth(10);
paint.setColor(Color.RED);
//设置起始点的位置为(200,400)
path.moveTo(200,400);
//线条p0-p2控制点(300,300) 终点位置(400,400)
path.quadTo(300,300,400,400);
//线条p2-p4控制点(500,500) 终点位置(600,400)
path.quadTo(500,500,600,400);
canvas.drawPath(path, paint);
}
复制代码
下面就用rQuadTo
来实现这个波浪线,先上分析图:
//重写onDraw方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setStyle(Paint.Style.STROKE);
//线条宽度
paint.setStrokeWidth(10);
paint.setColor(Color.RED);
//设置起始点的位置为(200,400)
path.moveTo(200,400);
//线条p0-p2控制点(300,300) 终点坐标位置(400,400)
path.rQuadTo(100,-100,200,0);
//线条p2-p4控制点(500,500) 终点坐标位置(600,400)
path.rQuadTo(100,100,200,0);
canvas.drawPath(path, paint);
}
复制代码
第一行:path.rQuadTo(100,-100,200,0);这个一行代码是基于(200,400)这个点来计算曲线p0-p2的控制点和终点坐标。
那么第一条曲线就容易绘制出来了,而且第一条曲线的终点也知道了是(400,400),那么第二句path.rQuadTo(100,100,200,0)是基于这个终点(400,400)来计算第二条曲线的控制点和终点。
其实这句path.rQuadTo(100,100,200,0);
是和path.quadTo(500,500,600,400);
相等的,实际运行的效果图也和用quadTo
方法绘制的同样,经过这个例子,能够知道quadTo
这个方法的参数都是实际结果的坐标,而rQuadTo
这个方法的参数是以上一个终点位置为基准来作位移的。
下面要实现如下效果:
对应代码以下:
//重写onDraw方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
path.reset();
//设置填充绘制
paint.setStyle(Paint.Style.FILL_AND_STROKE);
//线条宽度
//paint.setStrokeWidth(10);
paint.setColor(Color.RED);
int control = waveLength / 2;
//首先肯定初始状态的起点(-400,1200)
path.moveTo(-waveLength,origY);
//由于这个整个波浪的的宽度是View宽度加上左右各一个波长
for(int i = -waveLength;i <= getWidth() + waveLength;i += waveLength){
path.rQuadTo(control / 2,-70,control,0);
path.rQuadTo(control / 2,70,control,0);
}
path.lineTo(getWidth(),getHeight());
path.lineTo(0,getHeight());
path.close();
canvas.drawPath(path, paint);
}
复制代码
下面一行一行分析:
//首先肯定初始状态的起点(-400,1200)
path.moveTo(-waveLength,origY);
复制代码
首先将Path
起始位置向左移一个波长,为了就是后面实现的位移动画,而后利用循环来画出屏幕所容下的全部波浪:
for(int i = -waveLength;i <= getWidth() + waveLength;i += waveLength){
path.rQuadTo(control / 2,-70,control,0);
path.rQuadTo(control / 2,70,control,0);
}
复制代码
这里我简单说一下,path.rQuadTo(control / 2,-70,control,0);
循环里的第一行画的是一个波长的前半部分,下面把数值放进去就很容易理解了,由于waveLength
是400,因此control = waveLength / 2
就是200,而path.rQuadTo(control / 2,-70,control,0)
就是path.rQuadTo(100,-70,200,0)
,而path.rQuadTo(control / 2,70,control,0)
就是path.rQuadTo(100,70,200,0)
,上面说过rQuadTo
的用法了,就再也不叙述,下面直接上分析图,下面只是分析最左边的第一个波浪起始点,控制点的坐标,其他波浪只是经过循环绘制,就不分析了:
rQuadTo
方法才能绘制出一个完整的波浪,因此上面分析须要肯定五个点的位置。这里注意,上面图左右有一条线段链接底部,造成封闭图形,由于要填充内部,因此要封闭绘制
paint.setStyle(Paint.Style.FILL_AND_STROKE);
。当波浪绘制完成时,
path
点会在A点,而后用
path.lineTo(getWidth(),getHeight());
链接A,B点,再调用
path.lineTo(0,getHeight());
链接B,C点,最后调用
path.close();
链接初始点就是链接C和起始点,这样满横屏的波浪就绘制完成了。
下面实现左右上下位移动画,这就会有一点点进度条的感受,个人作法很简单,由于一开始在View的左边多画了一个波浪,也就是说,将起始点向右边移动,而且要移动一个波浪的长度就可让波纹重合,而后不断循环便可,简单来说就是,动画移动的距离是一个波浪的长度,当移动到最大的距离时设置不断循环,就会从新绘制波浪的初始状态。
/** * 动画位移方法 */
public void startAnim(){
//建立动画实例
ValueAnimator moveAnimator = ValueAnimator.ofInt(0,waveLength);
//动画的时间
moveAnimator.setDuration(2500);
//设置动画次数 INFINITE表示无限循环
moveAnimator.setRepeatCount(ValueAnimator.INFINITE);
//设置动画插值
moveAnimator.setInterpolator(new LinearInterpolator());
//添加监听
moveAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
moveDistance = (int)animation.getAnimatedValue();
invalidate();
}
});
//启动动画
moveAnimator.start();
}
复制代码
动画的位移距离是一个波浪的长度,并将位移的距离保存到moveDistance
中,而后开始的时候,在moveTo
加上这个距离,就能够了,完整代码以下:
//重写onDraw方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//把路线清除 从新绘制 必定要加上 否则是矩形
path.reset();
//设置填充绘制
paint.setStyle(Paint.Style.FILL_AND_STROKE);
//线条宽度
//paint.setStrokeWidth(10);
paint.setColor(Color.RED);
int control = waveLength / 2;
//首先肯定初始状态的起点(-400,1200)
path.moveTo(-waveLength + moveDistance,origY);
for(int i = -waveLength;i <= getWidth() + waveLength;i += waveLength){
path.rQuadTo(control / 2,-70,control,0);
path.rQuadTo(control / 2,70,control,0);
}
path.lineTo(getWidth(),getHeight());
path.lineTo(0,getHeight());
path.close();
canvas.drawPath(path, paint);
}
复制代码
效果以下:
path.moveTo(-waveLength + moveDistance,origY - moveDistance);
,最后调用如下代码:
pathView = findViewById(R.id.path_view);
pathView.startAnim();
复制代码
效果如上上上图。通过上面,本身对贝塞尔曲线由初步的了解,下面就实现波浪形进度条。
学到了上面的基本知识,那下面就实现一个小例子,就是圆形波浪进度条,最终效果在文章最底部,惯例下面就一步一步来实现。
先绘制一段满屏的波浪线,绘制原理就不详细讲了,直接上代码:
/** * Describe : 实现圆形波浪进度条 * Created by Knight on 2019/2/1 * 点滴之行,看世界 **/
public class CircleWaveProgressView extends View {
//绘制波浪画笔
private Paint wavePaint;
//绘制波浪Path
private Path wavePath;
//波浪的宽度
private float waveLength;
//波浪的高度
private float waveHeight;
public CircleWaveProgressView(Context context) {
this(context,null);
}
public CircleWaveProgressView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public CircleWaveProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
/** * 初始化一些画笔路径配置 * @param context */
private void init(Context context){
//设置波浪宽度
waveLength = Density.dip2px(context,25);
//设置波浪高度
waveHeight = Density.dip2px(context,15);
wavePath = new Path();
wavePaint = new Paint();
wavePaint.setColor(Color.parseColor("#ff7c9e"));
//设置抗锯齿
wavePaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
//绘制波浪线
canvas.drawPath(paintWavePath(),wavePaint);
}
/** * 绘制波浪线 * * @return */
private Path paintWavePath(){
//要先清掉路线
wavePath.reset();
//起始点移至(0,waveHeight)
wavePath.moveTo(0,waveHeight);
for(int i = 0;i < getWidth() ;i += waveLength){
wavePath.rQuadTo(waveLength / 2,waveHeight,waveLength,0);
wavePath.rQuadTo(waveLength / 2,-waveHeight,waveLength,0);
}
return wavePath;
}
}
复制代码
xml布局文件:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<com.example.progressbar.CircleWaveProgressView
android:id="@+id/circle_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
/>
</android.support.constraint.ConstraintLayout>
复制代码
实际效果以下:
由于圆形进度框中的波浪是随着进度的增长而不断上升的,因此波浪是填充物,先绘制波浪,而后用path.lineTo
和path.close
来链接封闭起来,构成一个填充图形,分析以下图:
public class CircleWaveProgressView extends View {
//绘制波浪画笔
private Paint wavePaint;
//绘制波浪Path
private Path wavePath;
//波浪的宽度
private float waveLength;
//波浪的高度
private float waveHeight;
//波浪组的数量 一个波浪是一低一高
private int waveNumber;
//自定义View的波浪宽高
private int waveDefaultSize;
//自定义View的最大宽高 就是比波浪高一点
private int waveMaxHeight;
public CircleWaveProgressView(Context context) {
this(context,null);
}
public CircleWaveProgressView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public CircleWaveProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
/** * 初始化一些画笔路径配置 * @param context */
private void init(Context context){
//设置波浪宽度
waveLength = Density.dip2px(context,25);
//设置波浪高度
waveHeight = Density.dip2px(context,15);
//设置自定义View的宽高
waveDefaultSize = Density.dip2px(context,250);
//设置自定义View的最大宽高
waveMaxHeight = Density.dip2px(context,300);
//Math.ceil(a)返回求不小于a的最小整数
// 举个例子:
// Math.ceil(125.9)=126.0
// Math.ceil(0.4873)=1.0
// Math.ceil(-0.65)=-0.0
//这里是调整波浪数量 就是View中能容下几个波浪 用到ceil就是必定让View彻底能被波浪占满 为循环绘制作准备 分母越小就约精准
waveNumber = (int) Math.ceil(Double.parseDouble(String.valueOf(waveDefaultSize / waveLength / 2)));
wavePath = new Path();
wavePaint = new Paint();
//设置颜色
wavePaint.setColor(Color.parseColor("#ff7c9e"));
//设置抗锯齿
wavePaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
//绘制波浪线
canvas.drawPath(paintWavePath(),wavePaint);
Log.d("ssd",getWidth()+"");
}
/** * 绘制波浪线 * * @return */
private Path paintWavePath(){
//要先清掉路线
wavePath.reset();
//起始点移至(0,waveHeight)
wavePath.moveTo(0,waveMaxHeight - waveDefaultSize);
//最多能绘制多少个波浪
//其实也能够用 i < getWidth() ;i+=waveLength来判断 这个没那么完美
//绘制p0 - p1 绘制波浪线
for(int i = 0;i < waveNumber ;i ++){
wavePath.rQuadTo(waveLength / 2,waveHeight,waveLength,0);
wavePath.rQuadTo(waveLength / 2,-waveHeight,waveLength,0);
}
//链接p1 - p2
wavePath.lineTo(waveDefaultSize,waveDefaultSize);
//链接p2 - p3
wavePath.lineTo(0,waveDefaultSize);
//链接p3 - p0
wavePath.lineTo(0,waveMaxHeight - waveDefaultSize);
//封闭起来填充
wavePath.close();
return wavePath;
}
复制代码
在上面中,发现一个问题,就是宽和高都在初始化方法init
中定死了,通常来说视图View的宽高都是在xml
文件中定义或者类文件中定义的,那么就要重写View的onMeasure
方法:
@Override
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int height = measureSize(waveDefaultSize, heightMeasureSpec);
int width = measureSize(waveDefaultSize, widthMeasureSpec);
//获取View的最短边的长度
int minSize = Math.min(height,width);
//把View改成正方形
setMeasuredDimension(minSize,minSize);
//waveActualSize是实际的宽高
waveActualSize = minSize;
//Math.ceil(a)返回求不小于a的最小整数
// 举个例子:
// Math.ceil(125.9)=126.0
// Math.ceil(0.4873)=1.0
// Math.ceil(-0.65)=-0.0
//这里是调整波浪数量 就是View中能容下几个波浪 用到ceil就是必定让View彻底能被波浪占满 为循环绘制作准备 分母越小就约精准
waveNumber = (int) Math.ceil(Double.parseDouble(String.valueOf(waveActualSize / waveLength / 2)));
}
/** * 返回指定的值 * @param defaultSize 默认的值 * @param measureSpec 模式 * @return */
private int measureSize(int defaultSize,int measureSpec) {
int result = defaultSize;
int specMode = View.MeasureSpec.getMode(measureSpec);
int specSize = View.MeasureSpec.getSize(measureSpec);
//View.MeasureSpec.EXACTLY:若是是match_parent 或者设置定值就
//View.MeasureSpec.AT_MOST:wrap_content
if (specMode == View.MeasureSpec.EXACTLY) {
result = specSize;
} else if (specMode == View.MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
return result;
}
复制代码
上面就很简单了,就是增长了一个View的实际宽高变量waveActualSize
,让代码扩展性更强和精确到更高。
下面实现波浪高度随着进度变化而变化,当进度增长时,波浪高度增高,当进度减小时,波浪高度减小,其实很简单,也就是p0-p3,p1-p2的高度根据进度变化而变化,并增长动画,代码增长以下:
//当前进度值占总进度值的占比
private float currentPercent;
//当前进度值
private float currentProgress;
//进度的最大值
private float maxProgress;
//动画对象
private WaveProgressAnimat waveProgressAnimat;
/** * 初始化一些画笔路径配置 * @param context */
private void init(Context context){
//......
//占比一开始设置为0
currentPercent = 0;
//进度条进度开始设置为0
currentProgress = 0;
//进度条的最大值设置为100
maxProgress = 100;
//动画实例化
waveProgressAnimat = new WaveProgressAnimat();
}
/** * 绘制波浪线 * * @return */
private Path paintWavePath(){
//要先清掉路线
wavePath.reset();
//起始点移至(0,waveHeight) p0 -p1 的高度随着进度的变化而变化
wavePath.moveTo(0,(1 - currentPercent) * waveActualSize);
//最多能绘制多少个波浪
//其实也能够用 i < getWidth() ;i+=waveLength来判断 这个没那么完美
//绘制p0 - p1 绘制波浪线
for(int i = 0;i < waveNumber ;i ++){
wavePath.rQuadTo(waveLength / 2,waveHeight,waveLength,0);
wavePath.rQuadTo(waveLength / 2,-waveHeight,waveLength,0);
}
//链接p1 - p2
wavePath.lineTo(waveActualSize,waveActualSize);
//链接p2 - p3
wavePath.lineTo(0,waveActualSize);
//链接p3 - p0 p3-p0d的高度随着进度变化而变化
wavePath.lineTo(0,(1 - currentPercent) * waveActualSize);
//封闭起来填充
wavePath.close();
return wavePath;
}
//新建一个动画类
public class WaveProgressAnimat extends Animation{
//在绘制动画的过程当中会反复的调用applyTransformation函数,
// 每次调用参数interpolatedTime值都会变化,该参数从0渐 变为1,当该参数为1时代表动画结束
@Override
protected void applyTransformation(float interpolatedTime, Transformation t){
super.applyTransformation(interpolatedTime, t);
//更新占比
currentPercent = interpolatedTime * currentProgress / maxProgress;
//从新绘制
invalidate();
}
}
/** * 设置进度条数值 * @param currentProgress 当前进度 * @param time 动画持续时间 */
public void setProgress(float currentProgress,int time){
this.currentProgress = currentProgress;
//从0开始变化
currentPercent = 0;
//设置动画时间
waveProgressAnimat.setDuration(time);
//当前视图开启动画
this.startAnimation(waveProgressAnimat);
}
复制代码
最后在Activity调用一些代码:
//进度为50 时间是2500毫秒
circleWaveProgressView.setProgress(50,2500);
复制代码
最终效果以下图:
上面实现了波浪直线上升的动画,下面实现波浪平移的动画,添加左移的效果,这里想到前面也实现了平移的效果,可是下面实现方式和上面有点出入,简单来说就是移动p0坐标,可是若是移动p0坐标会出现波浪不铺满整个View的状况,这里运用到一种很常见的循环处理办法。在飞机大战的背景滚动图,是两张背景图拼接起来,当飞机从第一个背景图片最底端出发,向上移动了第一个背景图片高度的距离时,将角色从新放回到第一个背景图片的最底端,这样就能实现背景图片循环的效果。也就是一开始绘制两端p0-p1,而后随着进度变化,p0会左移,一开始不在View中的波浪会从右边往左边移动出现,当滑动最大距离时,又从新绘制最开始状态,这样就达到循环了。仍是先上分析图:
//波浪平移距离
private float moveDistance = 0;
/** * 绘制波浪线 * * @return */
private Path paintWavePath(){
//要先清掉路线
wavePath.reset();
//起始点移至(0,waveHeight) p0 -p1 的高度随着进度的变化而变化
wavePath.moveTo(-moveDistance,(1 - currentPercent) * waveActualSize);
//最多能绘制多少个波浪
//其实也能够用 i < getWidth() ;i+=waveLength来判断 这个没那么完美
//绘制p0 - p1 绘制波浪线 这里有一段是超出View的,在View右边距的右边 因此是* 2,为了水平位移
for(int i = 0; i < waveNumber * 2 ; i ++){
wavePath.rQuadTo(waveLength / 2,waveHeight,waveLength,0);
wavePath.rQuadTo(waveLength / 2,-waveHeight,waveLength,0);
}
//链接p1 - p2
wavePath.lineTo(waveActualSize,waveActualSize);
//链接p2 - p3
wavePath.lineTo(0,waveActualSize);
//链接p3 - p0 p3-p0d的高度随着进度变化而变化
wavePath.lineTo(0,(1 - currentPercent) * waveActualSize);
//封闭起来填充
wavePath.close();
return wavePath;
}
/** * 设置进度条数值 * @param currentProgress 当前进度 * @param time 动画持续时间 */
public void setProgress(final float currentProgress, int time){
this.currentProgress = currentProgress;
//从0开始变化
currentPercent = 0;
//设置动画时间
waveProgressAnimat.setDuration(time);
//设置循环播放
waveProgressAnimat.setRepeatCount(Animation.INFINITE);
//让动画匀速播放,避免出现波浪平移停顿的现象
waveProgressAnimat.setInterpolator(new LinearInterpolator());
waveProgressAnimat.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
}
@Override
public void onAnimationRepeat(Animation animation) {
//波浪到达最高处后平移的速度改变,给动画设置监听便可,当动画结束后以7000毫秒的时间运行,变慢了
if(currentPercent == currentProgress /maxProgress){
waveProgressAnimat.setDuration(7000);
}
}
});
//当前视图开启动画
this.startAnimation(waveProgressAnimat);
}
//新建一个动画类
public class WaveProgressAnimat extends Animation{
//在绘制动画的过程当中会反复的调用applyTransformation函数,
// 每次调用参数interpolatedTime值都会变化,该参数从0渐 变为1,当该参数为1时代表动画结束
@Override
protected void applyTransformation(float interpolatedTime, Transformation t){
super.applyTransformation(interpolatedTime, t);
//波浪高度达到最高就不用循环,只须要平移
if(currentPercent < currentProgress / maxProgress){
currentPercent = interpolatedTime * currentProgress / maxProgress;
}
//左移的距离根据动画进度而改变
moveDistance = interpolatedTime * waveNumber * waveLength * 2;
//从新绘制
invalidate();
}
}
复制代码
最后的效果以下图:
这里要用到PorterDuffXfermode
的知识,其实也不难,先上PorterDuff.Mode
各类模式的效果图:
PorterDuff.Mode.SRC_IN
,由于先绘制圆形背景,再绘制波浪线,而
PorterDuff.Mode.SRC_IN
模式在二者相交的地方绘制源图像,而且绘制的效果会受到目标图像对应地方透明度的影响,看上图就知道了,代码以下:
//圆形背景画笔
private Paint circlePaint;
//bitmap
private Bitmap circleBitmap;
//bitmap画布
private Canvas bitmapCanvas;
/** * 初始化一些画笔路径配置 * @param context */
private void init(Context context){
//.......
//绘制圆形背景开始
wavePaint = new Paint();
//设置画笔为取交集模式
wavePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
//圆形背景初始化
circlePaint = new Paint();
//颜色
circlePaint.setColor(Color.GRAY);
//设置抗锯齿
circlePaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
Log.d("ssd",getWidth()+"");
//这里用到了缓存 根据参数建立新位图
circleBitmap = Bitmap.createBitmap(waveActualSize, waveActualSize, Bitmap.Config.ARGB_8888);
//以该bitmap为低建立一块画布
bitmapCanvas = new Canvas(circleBitmap);
//绘制圆形 圆心 直径都是很简单得出
bitmapCanvas.drawCircle(waveActualSize/2, waveActualSize/2, waveActualSize/2, circlePaint);
//绘制波浪形
bitmapCanvas.drawPath(paintWavePath(),wavePaint);
//裁剪图片
canvas.drawBitmap(circleBitmap, 0, 0, null);
//绘制波浪线
// canvas.drawPath(paintWavePath(),wavePaint);
}
复制代码
实际效果以下图:
res\vaules
文件下添加
attrs.xml
文件,给
CircleWaveProgressView
添加自定义属性,以下:
<!--这里的名字要和自定义的View名称同样,否则在xml布局中没法引用-->
<declare-styleable name="CircleWaveProgressView">
<!--波浪的颜色-->
<attr name="wave_color" format="color"></attr>
<!--圆形背景颜色-->
<attr name="circlebg_color" format="color"></attr>
<!--波浪长度-->
<attr name="wave_length" format="dimension"></attr>
<!--波浪高度-->
<attr name="wave_height" format="dimension"></attr>
<!--当前进度-->
<attr name="currentProgress" format="float"></attr>
<!--最大进度-->
<attr name="maxProgress" format="float"></attr>
</declare-styleable>
复制代码
在自定义View为属性值赋值:
//波浪颜色
private int wave_color;
//圆形背景进度框颜色
private int circle_bgcolor;
public CircleWaveProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//获取attrs文件下配置属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleWaveProgressView);
//获取波浪宽度 第二个参数,若是xml设置这个属性,则会取设置的默认值 也就是说xml没有指定wave_length这个属性,就会取Density.dip2px(context,25)
waveLength = typedArray.getDimension(R.styleable.CircleWaveProgressView_wave_length,Density.dip2px(context,25));
//获取波浪高度
waveHeight = typedArray.getDimension(R.styleable.CircleWaveProgressView_wave_height,Density.dip2px(context,15));
//获取波浪颜色
wave_color = typedArray.getColor(R.styleable.CircleWaveProgressView_wave_color,Color.parseColor("#ff7c9e"));
//圆形背景颜色
circle_bgcolor = typedArray.getColor(R.styleable.CircleWaveProgressView_circlebg_color,Color.GRAY);
//当前进度
currentProgress = typedArray.getFloat(R.styleable.CircleWaveProgressView_currentProgress,50);
//最大进度
maxProgress = typedArray.getFloat(R.styleable.CircleWaveProgressView_maxProgress,100);
//记得把TypedArray回收
//程序在运行时维护了一个 TypedArray的池,程序调用时,会向该池中请求一个实例,用完以后,调用 recycle() 方法来释放该实例,从而使其可被其余模块复用。
//那为何要使用这种模式呢?答案也很简单,TypedArray的使用场景之一,就是上述的自定义View,会随着 Activity的每一次Create而Create,
//所以,须要系统频繁的建立array,对内存和性能是一个不小的开销,若是不使用池模式,每次都让GC来回收,极可能就会形成OutOfMemory。
//这就是使用池+单例模式的缘由,这也就是为何官方文档一再的强调:使用完以后必定 recycle,recycle,recycle
typedArray.recycle();
init(context);
}
/** * 初始化一些画笔路径配置 * @param context */
private void init(Context context){
//设置自定义View的宽高
waveDefaultSize = Density.dip2px(context,250);
//设置自定义View的最大宽高
waveMaxHeight = Density.dip2px(context,300);
wavePath = new Path();
wavePaint = new Paint();
//设置画笔为取交集模式
wavePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
//圆形背景初始化
circlePaint = new Paint();
//设置圆形背景颜色
circlePaint.setColor(circle_bgcolor);
//设置抗锯齿
circlePaint.setAntiAlias(true);
//设置波浪颜色
wavePaint.setColor(wave_color);
//设置抗锯齿
wavePaint.setAntiAlias(true);
//占比一开始设置为0
currentPercent = 0;
//进度条进度开始设置为0
currentProgress = 0;
//进度条的最大值设置为100
maxProgress = 100;
//动画实例化
waveProgressAnimat = new WaveProgressAnimat();
复制代码
下面就能够在布局文件自定义设置波浪颜色,高度,宽度以及圆形背景颜色:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
tools:context=".MainActivity">
<com.android.quard.CircleWaveProgressView
android:id="@+id/circle_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:wave_color="@color/colorPrimaryDark"
app:circlebg_color="@android:color/black"
/>
</android.support.constraint.ConstraintLayout>
复制代码
效果图就不贴出来了。
下面要实现文字显示进度,进度条确定缺不了具体数值的显示,最简单就是直接在View
中实现绘制文字的操做,这种是很简单的,我以前实现自定义View都是将逻辑放在里面,这样就显得View很臃肿和扩展性不高,由于你想,假如我如今要改变字体位置和样式,那就须要在这个View去改去大动干戈。假如这个View能开放出处理文字接口的话,也就是后面修改文字样式只经过这个接口就能够了,这样就实现了文字和进度条这个View的解耦。
//进度显示 TextView
private TextView tv_progress;
//进度条显示值监听接口
private UpdateTextListener updateTextListener;
//新建一个动画类
public class WaveProgressAnimat extends Animation{
//在绘制动画的过程当中会反复的调用applyTransformation函数,
// 每次调用参数interpolatedTime值都会变化,该参数从0渐 变为1,当该参数为1时代表动画结束
@Override
protected void applyTransformation(float interpolatedTime, Transformation t){
super.applyTransformation(interpolatedTime, t);
//波浪高度达到最高就不用循环,只须要平移
if(currentPercent < currentProgress / maxProgress){
currentPercent = interpolatedTime * currentProgress / maxProgress;
//这里直接根据进度值显示
tv_progress.setText(updateTextListener.updateText(interpolatedTime,currentProgress,maxProgress));
}
//左边的距离
moveDistance = interpolatedTime * waveNumber * waveLength * 2;
//从新绘制
invalidate();
}
}
//定义数值监听
public interface UpdateTextListener{
/** * 提供接口 给外部修改数值样式 等 * @param interpolatedTime 这个值是动画的 从0变成1 * @param currentProgress 进度条的数值 * @param maxProgress 进度条的最大数值 * @return */
String updateText(float interpolatedTime,float currentProgress,float maxProgress);
}
//设置监听
public void setUpdateTextListener(UpdateTextListener updateTextListener){
this.updateTextListener = updateTextListener;
}
/** * * 设置显示内容 * @param tv_progress 内容 数值什么均可以 * */
public void setTextViewVaule(TextView tv_progress){
this.tv_progress = tv_progress;
}
复制代码
而后在Activity
文件实现CircleWaveProgressView.UpdateTextListener
接口,进行逻辑处理:
public class MainActivity extends AppCompatActivity {
private CircleWaveProgressView circleWaveProgressView;
private TextView tv_value;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//TextView控件
tv_value = findViewById(R.id.tv_value);
//进度条控件
circleWaveProgressView = findViewById(R.id.circle_progress);
//将TextView设置进度条里
circleWaveProgressView.setTextViewVaule(tv_value);
//设置字体数值显示监听
circleWaveProgressView.setUpdateTextListener(new CircleWaveProgressView.UpdateTextListener() {
@Override
public String updateText(float interpolatedTime, float currentProgress, float maxProgress) {
//取一位整数和而且保留两位小数
DecimalFormat decimalFormat=new DecimalFormat("0.00");
String text_value = decimalFormat.format(interpolatedTime * currentProgress / maxProgress * 100)+"%";
//最终把格式好的内容(数值带进进度条)
return text_value ;
}
});
//设置进度和动画时间
circleWaveProgressView.setProgress(50,2500);
}
}
复制代码
布局文件增长一个TextView
:
<android.support.constraint.ConstraintLayout 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"
tools:context=".MainActivity">
<com.android.quard.CircleWaveProgressView
android:id="@+id/circle_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
/>
<TextView
android:id="@+id/tv_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:textColor="#ffffff"
android:textSize="24dp"
/>
</android.support.constraint.ConstraintLayout>
复制代码
最终效果以下图:
要实现第二层波浪平移方向和第一层波浪平移方向相反,要改一下绘制顺序,。下图:
//是否绘制双波浪线
private boolean isCanvasSecond_Wave;
//第二层波浪的颜色
private int second_WaveColor;
//第二层波浪的画笔
private Paint secondWavePaint;
复制代码
attrs文件增长第二层波浪的颜色:
<!--这里的名字要和自定义的View名称同样,否则在xml布局中没法引用-->
<declare-styleable name="CircleWaveProgressView">
<!--波浪的颜色-->
<attr name="wave_color" format="color"></attr>
<!--圆形背景颜色-->
<attr name="circlebg_color" format="color"></attr>
<!--波浪长度-->
<attr name="wave_length" format="dimension"></attr>
<!--波浪高度-->
<attr name="wave_height" format="dimension"></attr>
<!--当前进度-->
<attr name="currentProgress" format="float"></attr>
<!--最大进度-->
<attr name="maxProgress" format="float"></attr>
<!--第二层波浪的颜色-->
<attr name="second_color" format="color"></attr>
</declare-styleable>
复制代码
类文件:
//第二层波浪的颜色
second_WaveColor = typedArray.getColor(R.styleable.CircleWaveProgressView_second_color,Color.RED);
复制代码
在init
方法增长:
//初始化第二层波浪画笔
secondWavePaint = new Paint();
secondWavePaint.setColor(second_WaveColor);
secondWavePaint.setAntiAlias(true);
//要覆盖在第一层波浪上,因此选SRC_ATOP模式,第二层波浪彻底显示,而且第一层非交集部分显示。这个模式看上面的图像合成图文章就能够了解
secondWavePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
//初始状态不绘制第二层波浪
isCanvasSecond_Wave = false;
复制代码
在onDraw
方法增长:
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
Log.d("ssd",getWidth()+"");
//这里用到了缓存 根据参数建立新位图
circleBitmap = Bitmap.createBitmap(waveActualSize, waveActualSize, Bitmap.Config.ARGB_8888);
//以该bitmap为低建立一块画布
bitmapCanvas = new Canvas(circleBitmap);
//绘制圆形 半径设小了一点,就是为了能让波浪填充完整个圆形背景
bitmapCanvas.drawCircle(waveActualSize/2, waveActualSize/2, waveActualSize/2 - Density.dip2px(getContext(),8), circlePaint);
//绘制波浪形
bitmapCanvas.drawPath(paintWavePath(),wavePaint);
//是否绘制第二层波浪
if(isCanvasSecond_Wave){
bitmapCanvas.drawPath(cavasSecondPath(),secondWavePaint);
}
//裁剪图片
canvas.drawBitmap(circleBitmap, 0, 0, null);
//绘制波浪线
// canvas.drawPath(paintWavePath(),wavePaint);
}
//是否绘制第二层波浪
public void isSetCanvasSecondWave(boolean isCanvasSecond_Wave){
this.isCanvasSecond_Wave = isCanvasSecond_Wave;
}
/** * 绘制第二层波浪方法 * @return */
private Path cavasSecondPath(){
float secondWaveHeight = waveHeight;
wavePath.reset();
//移动到右上方,也就是p1点
wavePath.moveTo(waveActualSize + moveDistance, (1 - currentPercent) * waveActualSize);
//p1 - p0
for(int i = 0; i < waveNumber * 2 ; i ++){
wavePath.rQuadTo(-waveLength / 2,secondWaveHeight,-waveLength,0);
wavePath.rQuadTo(-waveLength / 2,-secondWaveHeight,-waveLength,0);
}
//p0-p3 p3-p0d的高度随着进度变化而变化
wavePath.lineTo(0, waveActualSize);
//链接p3 - p2
wavePath.lineTo(waveActualSize,waveActualSize);
//链接p2 - p1
wavePath.lineTo(waveActualSize,(1 - currentPercent) * waveActualSize);
//封闭起来填充
wavePath.close();
return wavePath;
}
复制代码
最后在Activty文件设置:
//是否绘制第二层波浪
circleWaveProgressView.isSetCanvasSecondWave(true);
复制代码
最终效果以下图:
通过贝塞尔公式的推导和小例子实现,有了更深入的印象。有不少东西看起来并非那么触达,就好像当本身拿到一个开发需求时,技术评估发现会用到本身没有以前没有用到的技术,这时候就要多去参照别人实现的思路和方法,或者厚着脸皮问技术牛的人,学到了就是本身的,多付出就努力变得容易。