Android · 2015年3月4日 0

Android自定义控件实战——水流波动效果的实现WaveView

转载请声明出处http://blog.csdn.net/zhongkejingwang/article/details/38556891

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

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

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

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

是不是很动感?

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

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

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

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

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

然后再合并成这样:

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

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

红色点的运动轨迹就是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坐标又恢复到平移前的值,这样就可以一个波形一个波形地往外传输。用草图表示如下:

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

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

WaveView.java:

  1. package com.jingchen.waveview;
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. import java.util.Timer;
  5. import java.util.TimerTask;
  6. import android.content.Context;
  7. import android.graphics.Canvas;
  8. import android.graphics.Color;
  9. import android.graphics.Paint;
  10. import android.graphics.Paint.Align;
  11. import android.graphics.Paint.Style;
  12. import android.graphics.Region.Op;
  13. import android.graphics.Path;
  14. import android.graphics.RectF;
  15. import android.os.Handler;
  16. import android.os.Message;
  17. import android.util.AttributeSet;
  18. import android.view.View;
  19. /**
  20.  * 水流波动控件
  21.  * 
  22.  * @author chenjing
  23.  * 
  24.  */
  25. public class WaveView extends View
  26. {
  27.     private int mViewWidth;
  28.     private int mViewHeight;
  29.     /**
  30.      * 水位线
  31.      */
  32.     private float mLevelLine;
  33.     /**
  34.      * 波浪起伏幅度
  35.      */
  36.     private float mWaveHeight = 80;
  37.     /**
  38.      * 波长
  39.      */
  40.     private float mWaveWidth = 200;
  41.     /**
  42.      * 被隐藏的最左边的波形
  43.      */
  44.     private float mLeftSide;
  45.     private float mMoveLen;
  46.     /**
  47.      * 水波平移速度
  48.      */
  49.     public static final float SPEED = 1.7f;
  50.     private List<Point> mPointsList;
  51.     private Paint mPaint;
  52.     private Paint mTextPaint;
  53.     private Path mWavePath;
  54.     private boolean isMeasured = false;
  55.     private Timer timer;
  56.     private MyTimerTask mTask;
  57.     Handler updateHandler = new Handler()
  58.     {
  59.         @Override
  60.         public void handleMessage(Message msg)
  61.         {
  62.             // 记录平移总位移
  63.             mMoveLen += SPEED;
  64.             // 水位上升
  65.             mLevelLine -= 0.1f;
  66.             if (mLevelLine < 0)
  67.                 mLevelLine = 0;
  68.             mLeftSide += SPEED;
  69.             // 波形平移
  70.             for (int i = 0; i < mPointsList.size(); i++)
  71.             {
  72.                 mPointsList.get(i).setX(mPointsList.get(i).getX() + SPEED);
  73.                 switch (i % 4)
  74.                 {
  75.                 case 0:
  76.                 case 2:
  77.                     mPointsList.get(i).setY(mLevelLine);
  78.                     break;
  79.                 case 1:
  80.                     mPointsList.get(i).setY(mLevelLine + mWaveHeight);
  81.                     break;
  82.                 case 3:
  83.                     mPointsList.get(i).setY(mLevelLine – mWaveHeight);
  84.                     break;
  85.                 }
  86.             }
  87.             if (mMoveLen >= mWaveWidth)
  88.             {
  89.                 // 波形平移超过一个完整波形后复位
  90.                 mMoveLen = 0;
  91.                 resetPoints();
  92.             }
  93.             invalidate();
  94.         }
  95.     };
  96.     /**
  97.      * 所有点的x坐标都还原到初始状态,也就是一个周期前的状态
  98.      */
  99.     private void resetPoints()
  100.     {
  101.         mLeftSide = -mWaveWidth;
  102.         for (int i = 0; i < mPointsList.size(); i++)
  103.         {
  104.             mPointsList.get(i).setX(i * mWaveWidth / 4 – mWaveWidth);
  105.         }
  106.     }
  107.     public WaveView(Context context)
  108.     {
  109.         super(context);
  110.         init();
  111.     }
  112.     public WaveView(Context context, AttributeSet attrs)
  113.     {
  114.         super(context, attrs);
  115.         init();
  116.     }
  117.     public WaveView(Context context, AttributeSet attrs, int defStyle)
  118.     {
  119.         super(context, attrs, defStyle);
  120.         init();
  121.     }
  122.     private void init()
  123.     {
  124.         mPointsList = new ArrayList<Point>();
  125.         timer = new Timer();
  126.         mPaint = new Paint();
  127.         mPaint.setAntiAlias(true);
  128.         mPaint.setStyle(Style.FILL);
  129.         mPaint.setColor(Color.BLUE);
  130.         mTextPaint = new Paint();
  131.         mTextPaint.setColor(Color.WHITE);
  132.         mTextPaint.setTextAlign(Align.CENTER);
  133.         mTextPaint.setTextSize(30);
  134.         mWavePath = new Path();
  135.     }
  136.     @Override
  137.     public void onWindowFocusChanged(boolean hasWindowFocus)
  138.     {
  139.         super.onWindowFocusChanged(hasWindowFocus);
  140.         // 开始波动
  141.         start();
  142.     }
  143.     private void start()
  144.     {
  145.         if (mTask != null)
  146.         {
  147.             mTask.cancel();
  148.             mTask = null;
  149.         }
  150.         mTask = new MyTimerTask(updateHandler);
  151.         timer.schedule(mTask, 010);
  152.     }
  153.     @Override
  154.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
  155.     {
  156.         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  157.         if (!isMeasured)
  158.         {
  159.             isMeasured = true;
  160.             mViewHeight = getMeasuredHeight();
  161.             mViewWidth = getMeasuredWidth();
  162.             // 水位线从最底下开始上升
  163.             mLevelLine = mViewHeight;
  164.             // 根据View宽度计算波形峰值
  165.             mWaveHeight = mViewWidth / 2.5f;
  166.             // 波长等于四倍View宽度也就是View中只能看到四分之一个波形,这样可以使起伏更明显
  167.             mWaveWidth = mViewWidth * 4;
  168.             // 左边隐藏的距离预留一个波形
  169.             mLeftSide = -mWaveWidth;
  170.             // 这里计算在可见的View宽度中能容纳几个波形,注意n上取整
  171.             int n = (int) Math.round(mViewWidth / mWaveWidth + 0.5);
  172.             // n个波形需要4n+1个点,但是我们要预留一个波形在左边隐藏区域,所以需要4n+5个点
  173.             for (int i = 0; i < (4 * n + 5); i++)
  174.             {
  175.                 // 从P0开始初始化到P4n+4,总共4n+5个点
  176.                 float x = i * mWaveWidth / 4 – mWaveWidth;
  177.                 float y = 0;
  178.                 switch (i % 4)
  179.                 {
  180.                 case 0:
  181.                 case 2:
  182.                     // 零点位于水位线上
  183.                     y = mLevelLine;
  184.                     break;
  185.                 case 1:
  186.                     // 往下波动的控制点
  187.                     y = mLevelLine + mWaveHeight;
  188.                     break;
  189.                 case 3:
  190.                     // 往上波动的控制点
  191.                     y = mLevelLine – mWaveHeight;
  192.                     break;
  193.                 }
  194.                 mPointsList.add(new Point(x, y));
  195.             }
  196.         }
  197.     }
  198.     @Override
  199.     protected void onDraw(Canvas canvas)
  200.     {
  201.         mWavePath.reset();
  202.         int i = 0;
  203.         mWavePath.moveTo(mPointsList.get(0).getX(), mPointsList.get(0).getY());
  204.         for (; i < mPointsList.size() – 2; i = i + 2)
  205.         {
  206.             mWavePath.quadTo(mPointsList.get(i + 1).getX(),
  207.                     mPointsList.get(i + 1).getY(), mPointsList.get(i + 2)
  208.                             .getX(), mPointsList.get(i + 2).getY());
  209.         }
  210.         mWavePath.lineTo(mPointsList.get(i).getX(), mViewHeight);
  211.         mWavePath.lineTo(mLeftSide, mViewHeight);
  212.         mWavePath.close();
  213.         // mPaint的Style是FILL,会填充整个Path区域
  214.         canvas.drawPath(mWavePath, mPaint);
  215.         // 绘制百分比
  216.         canvas.drawText(“” + ((int) ((1 – mLevelLine / mViewHeight) * 100))
  217.                 + “%”, mViewWidth / 2, mLevelLine + mWaveHeight
  218.                 + (mViewHeight – mLevelLine – mWaveHeight) / 2, mTextPaint);
  219.     }
  220.     class MyTimerTask extends TimerTask
  221.     {
  222.         Handler handler;
  223.         public MyTimerTask(Handler handler)
  224.         {
  225.             this.handler = handler;
  226.         }
  227.         @Override
  228.         public void run()
  229.         {
  230.             handler.sendMessage(handler.obtainMessage());
  231.         }
  232.     }
  233.     class Point
  234.     {
  235.         private float x;
  236.         private float y;
  237.         public float getX()
  238.         {
  239.             return x;
  240.         }
  241.         public void setX(float x)
  242.         {
  243.             this.x = x;
  244.         }
  245.         public float getY()
  246.         {
  247.             return y;
  248.         }
  249.         public void setY(float y)
  250.         {
  251.             this.y = y;
  252.         }
  253.         public Point(float x, float y)
  254.         {
  255.             this.x = x;
  256.             this.y = y;
  257.         }
  258.     }
  259. }

代码中注释写的很多,不难看懂。

Demo的布局:

  1. <RelativeLayout xmlns:android=“http://schemas.android.com/apk/res/android”
  2.     android:layout_width=“match_parent”
  3.     android:layout_height=“match_parent”
  4.     android:background=“#000000” >
  5.     <com.jingchen.waveview.WaveView
  6.         android:layout_width=“100dp”
  7.         android:background=“#ffffff”
  8.         android:layout_height=“match_parent”
  9.         android:layout_centerInParent=“true” />
  10. </RelativeLayout>

MainActivity的代码:

  1. package com.jingchen.waveview;
  2. import android.os.Bundle;
  3. import android.app.Activity;
  4. import android.view.Menu;
  5. public class MainActivity extends Activity
  6. {
  7.     @Override
  8.     protected void onCreate(Bundle savedInstanceState)
  9.     {
  10.         super.onCreate(savedInstanceState);
  11.         setContentView(R.layout.activity_main);
  12.     }
  13.     @Override
  14.     public boolean onCreateOptionsMenu(Menu menu)
  15.     {
  16.         getMenuInflater().inflate(R.menu.main, menu);
  17.         return true;
  18.     }
  19. }

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

源码下载

Share this: