Android WaveView实现水流波动效果

作者:陈靖_ 时间:2021-11-09 16:50:59 

   水流波动的波形都是三角波,曲线是正余弦曲线,但是Android中没有提供绘制正余弦曲线的API,好在Path类有个绘制贝塞尔曲线的方法quadTo,绘制出来的是2阶的贝塞尔曲线,要想实现波动效果,只能用它来绘制Path曲线。待会儿再讲解2阶的贝塞尔曲线是怎么回事,先来看实现的效果:

Android WaveView实现水流波动效果

这个波长比较短,还看不到起伏,只是荡漾,把波长拉长再看一下:

Android WaveView实现水流波动效果

已经可以看到起伏很明显了,再拉长看一下:

Android WaveView实现水流波动效果

这个的起伏感就比较强了。利用这个波动效果,可以用在绘制水位线的时候使用到,还可以做一个波动的进度条WaveUpProgress,比如这样:

Android WaveView实现水流波动效果

是不是很动感?

那这样的波动效果是怎么做的呢?前面讲到的贝塞尔曲线到底是什么呢?下面一一讲解。想要用好贝塞尔曲线就得先理解它的表达式,为了形象描述,我从网上盗了些动图。

首先看1阶贝塞尔曲线的表达式:

                             Android WaveView实现水流波动效果

随着t的变化,它实际是一条P0到P1的直线段:

                                Android WaveView实现水流波动效果

Android中Path的quadTo是3点的2阶贝塞尔曲线,那么2阶的表达式是这样的:

    Android WaveView实现水流波动效果

看起来很复杂,我把它拆分开来看:

        Android WaveView实现水流波动效果

然后再合并成这样:

      Android WaveView实现水流波动效果

看到什么了吧?如果看不出来再替换成这样:

     Android WaveView实现水流波动效果

      Android WaveView实现水流波动效果

     Android WaveView实现水流波动效果

B0和B1分别是P0到P1和P1到P2的1阶贝塞尔曲线。而2阶贝塞尔曲线B就是B0到B1的1阶贝塞尔曲线。显然,它的动态图表示出来就不难理解了:

                                          Android WaveView实现水流波动效果

红色点的运动轨迹就是B的轨迹,这就是2阶贝塞尔曲线了。当P1位于P0和P2的垂直平分线上时,B就是开口向上或向下的抛物线了。而在WaveView中就是用的开口向上和向下的抛物线模拟水波。在Android里用Path的方法,首先path.moveTo(P0),然后path.quadTo(P1, P2),canvas.drawPath(path, paint)曲线就出来了,如果想要绘制多个贝塞尔曲线就不断的quadTo吧。

    讲完贝塞尔曲线后就要开始讲水波动的效果是怎么来的了,首先要理解,机械波的传输就是通过介质的震动把波形往传输方向平移,每震动一个周期波形刚好平移一个波长,所有介质点又回到一个周期前的状态。所以要实现水波动效果只需要把波形平移就可以了。

那么WaveView的实现原理是这样的:

    首先在View上根据View宽计算可以容纳几个完整波形,不够一个的算一个,然后在View的不可见处预留一个完整的波形;然后波动开始的时候将所有点同时在x方向上移动相同的距离,这样隐藏的波形就会被平移出来,当平移距离达到一个波长时,这时候将所有点的x坐标又恢复到平移前的值,这样就可以一个波形一个波形地往外传输。用草图表示如下:

Android WaveView实现水流波动效果

WaveView的原理在上图很直观的看出来了,P[2n+1],n>=0都是贝塞尔曲线的控制点,红线为水位线。

知道原理以后可以看代码了:

WaveView.java:


package com.jingchen.waveview;

import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.Style;
import android.graphics.Region.Op;
import android.graphics.Path;
import android.graphics.RectF;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.View;

/**
* 水流波动控件
*
* @author chenjing
*
*/
public class WaveView extends View
{

private int mViewWidth;
private int mViewHeight;

/**
 * 水位线
 */
private float mLevelLine;

/**
 * 波浪起伏幅度
 */
private float mWaveHeight = 80;
/**
 * 波长
 */
private float mWaveWidth = 200;
/**
 * 被隐藏的最左边的波形
 */
private float mLeftSide;

private float mMoveLen;
/**
 * 水波平移速度
 */
public static final float SPEED = 1.7f;

private List<Point> mPointsList;
private Paint mPaint;
private Paint mTextPaint;
private Path mWavePath;
private boolean isMeasured = false;

private Timer timer;
private MyTimerTask mTask;
Handler updateHandler = new Handler()
{

@Override
 public void handleMessage(Message msg)
 {
  // 记录平移总位移
  mMoveLen += SPEED;
  // 水位上升
  mLevelLine -= 0.1f;
  if (mLevelLine < 0)
   mLevelLine = 0;
  mLeftSide += SPEED;
  // 波形平移
  for (int i = 0; i < mPointsList.size(); i++)
  {
   mPointsList.get(i).setX(mPointsList.get(i).getX() + SPEED);
   switch (i % 4)
   {
   case 0:
   case 2:
    mPointsList.get(i).setY(mLevelLine);
    break;
   case 1:
    mPointsList.get(i).setY(mLevelLine + mWaveHeight);
    break;
   case 3:
    mPointsList.get(i).setY(mLevelLine - mWaveHeight);
    break;
   }
  }
  if (mMoveLen >= mWaveWidth)
  {
   // 波形平移超过一个完整波形后复位
   mMoveLen = 0;
   resetPoints();
  }
  invalidate();
 }

};

/**
 * 所有点的x坐标都还原到初始状态,也就是一个周期前的状态
 */
private void resetPoints()
{
 mLeftSide = -mWaveWidth;
 for (int i = 0; i < mPointsList.size(); i++)
 {
  mPointsList.get(i).setX(i * mWaveWidth / 4 - mWaveWidth);
 }
}

public WaveView(Context context)
{
 super(context);
 init();
}

public WaveView(Context context, AttributeSet attrs)
{
 super(context, attrs);
 init();
}

public WaveView(Context context, AttributeSet attrs, int defStyle)
{
 super(context, attrs, defStyle);
 init();
}

private void init()
{
 mPointsList = new ArrayList<Point>();
 timer = new Timer();

mPaint = new Paint();
 mPaint.setAntiAlias(true);
 mPaint.setStyle(Style.FILL);
 mPaint.setColor(Color.BLUE);

mTextPaint = new Paint();
 mTextPaint.setColor(Color.WHITE);
 mTextPaint.setTextAlign(Align.CENTER);
 mTextPaint.setTextSize(30);

mWavePath = new Path();
}

@Override
public void onWindowFocusChanged(boolean hasWindowFocus)
{
 super.onWindowFocusChanged(hasWindowFocus);
 // 开始波动
 start();
}

private void start()
{
 if (mTask != null)
 {
  mTask.cancel();
  mTask = null;
 }
 mTask = new MyTimerTask(updateHandler);
 timer.schedule(mTask, 0, 10);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
 if (!isMeasured)
 {
  isMeasured = true;
  mViewHeight = getMeasuredHeight();
  mViewWidth = getMeasuredWidth();
  // 水位线从最底下开始上升
  mLevelLine = mViewHeight;
  // 根据View宽度计算波形峰值
  mWaveHeight = mViewWidth / 2.5f;
  // 波长等于四倍View宽度也就是View中只能看到四分之一个波形,这样可以使起伏更明显
  mWaveWidth = mViewWidth * 4;
  // 左边隐藏的距离预留一个波形
  mLeftSide = -mWaveWidth;
  // 这里计算在可见的View宽度中能容纳几个波形,注意n上取整
  int n = (int) Math.round(mViewWidth / mWaveWidth + 0.5);
  // n个波形需要4n+1个点,但是我们要预留一个波形在左边隐藏区域,所以需要4n+5个点
  for (int i = 0; i < (4 * n + 5); i++)
  {
   // 从P0开始初始化到P4n+4,总共4n+5个点
   float x = i * mWaveWidth / 4 - mWaveWidth;
   float y = 0;
   switch (i % 4)
   {
   case 0:
   case 2:
    // 零点位于水位线上
    y = mLevelLine;
    break;
   case 1:
    // 往下波动的控制点
    y = mLevelLine + mWaveHeight;
    break;
   case 3:
    // 往上波动的控制点
    y = mLevelLine - mWaveHeight;
    break;
   }
   mPointsList.add(new Point(x, y));
  }
 }
}

@Override
protected void onDraw(Canvas canvas)
{

mWavePath.reset();
 int i = 0;
 mWavePath.moveTo(mPointsList.get(0).getX(), mPointsList.get(0).getY());
 for (; i < mPointsList.size() - 2; i = i + 2)
 {
  mWavePath.quadTo(mPointsList.get(i + 1).getX(),
    mPointsList.get(i + 1).getY(), mPointsList.get(i + 2)
      .getX(), mPointsList.get(i + 2).getY());
 }
 mWavePath.lineTo(mPointsList.get(i).getX(), mViewHeight);
 mWavePath.lineTo(mLeftSide, mViewHeight);
 mWavePath.close();

// mPaint的Style是FILL,会填充整个Path区域
 canvas.drawPath(mWavePath, mPaint);
 // 绘制百分比
 canvas.drawText("" + ((int) ((1 - mLevelLine / mViewHeight) * 100))
   + "%", mViewWidth / 2, mLevelLine + mWaveHeight
   + (mViewHeight - mLevelLine - mWaveHeight) / 2, mTextPaint);
}

class MyTimerTask extends TimerTask
{
 Handler handler;

public MyTimerTask(Handler handler)
 {
  this.handler = handler;
 }

@Override
 public void run()
 {
  handler.sendMessage(handler.obtainMessage());
 }

}

class Point
{
 private float x;
 private float y;

public float getX()
 {
  return x;
 }

public void setX(float x)
 {
  this.x = x;
 }

public float getY()
 {
  return y;
 }

public void setY(float y)
 {
  this.y = y;
 }

public Point(float x, float y)
 {
  this.x = x;
  this.y = y;
 }

}

}

代码中注释写的很多,不难看懂。
Demo的布局:


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000" >

<com.jingchen.waveview.WaveView
 android:layout_width="100dp"
 android:background="#ffffff"
 android:layout_height="match_parent"
 android:layout_centerInParent="true" />

</RelativeLayout>

MainActivity的代码:


package com.jingchen.waveview;

import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;

public class MainActivity extends Activity
{

@Override
protected void onCreate(Bundle savedInstanceState)
{
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
}

@Override
public boolean onCreateOptionsMenu(Menu menu)
{
 getMenuInflater().inflate(R.menu.main, menu);
 return true;
}

}

代码量很少,这样就可以很简单的做出水波效果啦。

源码下载: 《Android实现水流波动效果》

标签:Android,WaveView,水波
0
投稿

猜你喜欢

  • SpringBoot打成war包在tomcat或wildfly下运行的方法

    2023-11-23 08:20:56
  • Java用递归方法解决汉诺塔问题详解

    2022-11-23 03:11:40
  • Spring Boot + Mybatis多数据源和动态数据源配置方法

    2023-02-16 17:15:31
  • java实现银行管理系统

    2023-12-07 23:50:28
  • 浅析MMAP零拷贝在RocketMQ中的运用

    2021-11-21 01:59:47
  • Android控件之Spinner用法实例分析

    2022-08-06 08:36:33
  • Java中工具Jstack的使用实例

    2023-07-26 15:18:02
  • C# winform跨线程操作控件的实现

    2023-06-15 16:32:41
  • Spring Boot2.3 新特性分层JAR的使用

    2021-08-03 12:55:50
  • JDK13的新特性之AppCDS详解

    2023-03-19 10:32:38
  • C# 使用SharpZipLib生成压缩包的实例代码

    2021-08-29 20:32:57
  • android工程下不能运行java main程序的解决方法

    2023-06-23 21:54:08
  • 如何在Spring Boot中使用MQTT

    2023-10-08 20:39:13
  • Java实现堆排序和图解

    2023-11-11 12:13:37
  • unity绘制一条流动的弧线(贝塞尔线)

    2022-09-03 18:15:00
  • Android中WebView常见问题及解决方案汇总

    2021-06-26 07:23:02
  • Android带进度条的文件上传示例(使用AsyncTask异步任务)

    2023-06-24 09:43:11
  • 详解hbase与hive数据同步

    2023-02-09 00:20:36
  • 关于Java中修饰符的总结(fina除外)

    2023-11-22 23:15:57
  • Android横竖屏幕切换生命周期详解

    2023-11-02 03:29:45
  • asp之家 软件编程 m.aspxhome.com