波浪图
先上一张效果图
感觉还是挺炫酷的。其中用到的技术点就是贝塞尔曲线,说到贝塞尔曲线,它能做的东西就太多了,qq未读消息气泡拖拽,波浪效果,轨迹变化的动画都可以依赖贝塞尔曲线实现。
而我这里也不是自己造轮子,而是站在巨人的肩膀上。Android已经封装好了一个方法,就是path类的quadTo方法来绘制二阶贝塞尔曲线。更多阶的咱们暂且不谈。
1、构造贝塞尔曲线
二阶贝塞尔曲线介绍
先来描述一下各个点和线的含义,上图这条红线就是我们的贝塞尔曲线,P0是起始点,P2是结束点,P1是插入进来的控制点,我们在P0P1上找一点Q0,在P1P2上找一点Q1,使得P0Q0 / P0P1 = P1Q1 / P1P2 = t,然后连接Q0Q1,在Q0Q1上找一点B,使得Q0B / Q0Q1 = t,这样在Q0从P0到P1移动的过程中,Q1,B也在不断移动,而B的移动轨迹,就是我们要的贝塞尔曲线。
贝塞尔曲线在Android中的代码实现
在绘制贝塞尔曲线的时候,有三个重要的点,即起点P0,终点P2,以及控制点P1。实现思路如下:
- 首先在自定义View的构造方法中初始化好Paint的基本设置(其中有三个patin,贝塞尔线,控制线,文字)
- 实现onSizeChanged方法,用她来确定自定义View的大小。我们可以在此方法中确定起点,终点,控制点的坐标。(这里的第一二个参数应该是控件的宽和高)
- 然后在onDraw方法中使用Canvas绘制贝塞尔曲线,绘制曲线就要指定Path的移动路径。首先调用reset()方法,然后利用moveTo方法设置起点P0,然后利用quadTo方法设置控制点和终点。最后调用canvas.drawPath(),传入Path和Paint
- 以上过程已经将贝塞尔曲线绘制完毕,为了更好的观感体验,在onDraw方法中添加绘制起点、终点、控制点的圆圈Point和文字提示,再分别绘制两条起、终点到控制点的直线。整体效果更加清晰
- 最后实现boolean onTouchEvent(MotionEvent event)方法,监听MotionEvent.ACTION_MOVE事件,将手指一动坐标赋值给控制点,调用invalidate();重新渲染。这样便可动态绘制二阶贝塞尔曲线。
来贴一下代码:
public class SecondBezierView extends View {
//起点
private float mStartPointX;
private float mStartPointY;
//终点
private float mEndPointX;
private float mEndPointY;
//控制点
private float mFlagPointX;
private float mFlagPointY;
private Path mPath;
private Paint mPaintBezier;
private Paint mPaintFlag;
private Paint mPaintFlagText;
public SecondBezierView(Context context) {
super(context);
}
public SecondBezierView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaintBezier = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintBezier.setStrokeWidth(8);
mPaintBezier.setStyle(Paint.Style.STROKE);
mPaintFlag = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintFlag.setStrokeWidth(3);
mPaintFlag.setStyle(Paint.Style.STROKE);
mPaintFlagText = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintFlagText.setStyle(Paint.Style.STROKE);
mPaintFlagText.setTextSize(20);
}
public SecondBezierView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//初始时确定起点、终点和控制点的坐标
mStartPointX = w / 4;
mStartPointY = h / 2 - 200;
mEndPointX = w * 3 / 4;
mEndPointY = h / 2 - 200;
mFlagPointX = w / 2;
mFlagPointY = h / 2 - 300;
mPath = new Path();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.reset();
mPath.moveTo(mStartPointX, mStartPointY);
mPath.quadTo(mFlagPointX, mFlagPointY, mEndPointX, mEndPointY);
canvas.drawPoint(mStartPointX, mStartPointY, mPaintFlag);
canvas.drawText("起点", mStartPointX, mStartPointY, mPaintFlagText);
canvas.drawPoint(mEndPointX, mEndPointY, mPaintFlag);
canvas.drawText("终点", mEndPointX, mEndPointY, mPaintFlagText);
canvas.drawPoint(mFlagPointX, mFlagPointY, mPaintFlag);
canvas.drawText("控制点", mFlagPointX, mFlagPointY, mPaintFlagText);
canvas.drawLine(mStartPointX, mStartPointY, mFlagPointX, mFlagPointY, mPaintFlag);
canvas.drawLine(mEndPointX, mEndPointY, mFlagPointX, mFlagPointY, mPaintFlag);
canvas.drawPath(mPath, mPaintBezier);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
mFlagPointX = event.getX();
mFlagPointY = event.getY();
invalidate();
break;
}
return true;
}
}
效果图如下:
2、波浪效果
波浪效果的实现关键就是借助波浪的周期性规律和ValueAnimator位移偏量0到波长间重复的变化,通过不断的位移变化,达到波浪流动的效果
(1)实现一个完整的波浪
一个完整的波浪需要两个贝塞尔曲线,这里设定半波长为400,连续调用两次quadTo方法:
mPath.moveTo(0, mStartPointY);
mPath.quadTo(200, mStartPointY-300, 400, mStartPointY);
mPath.quadTo(600, mStartPointY+300, 800, mStartPointY);
接下来的任务是让波浪填满屏幕宽度,且流动起来。因此需要弄清两个问题:屏幕宽度可以容纳几个周期波浪,填满屏幕后每次波浪位移量多少?通过改变波浪的横坐标来达到让波浪流动的效果。
- 前者获取屏幕宽度计算即可。
- 后者只需增加二阶贝塞尔曲线的起点、终点、控制点横坐标位移量即可,使其呈现出流动的动画效果。但需要注意当波浪向右侧流动时,屏幕左侧之外应当也有波形,使得波形准备向右侧移动一个弦长的距离时,屏幕左侧之外也有一个弦长的波形准备移动进来,不然波浪之间则会断开
(2)将波浪填满屏幕
- 首先在自定义View的构造方法中定义一个完整的波浪长度为800,即包括上圆拱和下圆拱部分。
- 在初始定义View大小的void onSizeChanged(int w, int h, int oldw, int oldh)方法中获取屏幕宽度计算填满屏幕需要几个完整波长。注意此处在计算时不可直接简单为mScreenWidth / mWaveLength,首先考虑到除法操作后结果为Double类型,再赋值给int类型count,因此避免舍位带来的误差,需要在除法过后加上0.5,而且之前一直在强调,屏幕左侧之外也应当有一个弦长的波形准备移动进来,因此再加上1,最后公式为mWaveCount = (int) Math.round(mScreenWidth / mWaveLength + 1.5);
- 因此绘制时第一个波形的起点是屏幕外侧的 -波形长坐标,调用mPath。move确定好起点后,循环mWaveCount数量绘制波形,每次循环绘制一个波形,即之前讲过的调用两次quadTo方法。
(3)让波浪动起来
这里我们利用ValueAnimator让波浪动起来,并设置一个插值器来控制坐标点的偏移。完整代码如下:
public class WaveBezierView extends View implements View.OnClickListener {
private Path mPath;
private Paint mPaintBezier;
private int mWaveLength;
private int mScreenHeight;
private int mScreenWidth;
private int mCenterY;
private int mWaveCount;
private ValueAnimator mValueAnimator;
//波浪流动X轴偏移量
private int mOffsetX;
//波浪升起Y轴偏移量
private int mOffsetY;
private int count = 0;
public WaveBezierView(Context context) {
super(context);
}
public WaveBezierView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaintBezier = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintBezier.setColor(Color.LTGRAY);
mPaintBezier.setStrokeWidth(8);
mPaintBezier.setStyle(Paint.Style.FILL_AND_STROKE);
mWaveLength = 800;
}
public WaveBezierView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mPath = new Path();
setOnClickListener(this);
mScreenHeight = h;
mScreenWidth = w;
mCenterY = h / 2;//设定波浪在屏幕中央处显示
//此处多加1,是为了预先加载屏幕外的一个波浪,持续报廊移动时的连续性
mWaveCount = (int) Math.round(mScreenWidth / mWaveLength + 1.5);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.reset();
//Y坐标每次绘制时减去偏移量,即波浪升高
mPath.moveTo(-mWaveLength + mOffsetX, mCenterY);
//每次循环绘制两个二阶贝塞尔曲线形成一个完整波形(含有一个上拱圆,一个下拱圆)
for (int i = 0; i < mWaveCount; i++) {
//此处的60是指波浪起伏的偏移量,自定义为60
/*
mPath.quadTo(-mWaveLength * 3 / 4 + i * mWaveLength + mOffsetX, mCenterY + 60, -mWaveLength / 2 + i * mWaveLength + mOffset, mCenterY);
mPath.quadTo(-mWaveLength / 4 + i * mWaveLength + mOffsetX, mCenterY - 60, i * mWaveLength + mOffset, mCenterY);
*/
//第二种写法:相对位移
mPath.rQuadTo(mWaveLength / 4, -60, mWaveLength / 2, 0);
mPath.rQuadTo(mWaveLength / 4, +60, mWaveLength / 2, 0);
}
mPath.lineTo(mScreenWidth, mScreenHeight);
mPath.lineTo(0, mScreenHeight);
mPath.close();
canvas.drawPath(mPath, mPaintBezier);
}
@Override
public void onClick(View view) {
//设置动画运动距离
mValueAnimator = ValueAnimator.ofInt(0, mWaveLength);
mValueAnimator.setDuration(1000);
//设置播放数量无限循环
mValueAnimator.setRepeatCount(ValueAnimator.INFINITE);
// mValueAnimator.setRepeatCount(1);
//设置线性运动的插值器
mValueAnimator.setInterpolator(new LinearInterpolator());
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
//获取偏移量,绘制波浪曲线的X横坐标加上此偏移量,产生移动效果
mOffsetX = (int) valueAnimator.getAnimatedValue();
count++;
invalidate();
}
});
mValueAnimator.start();
}