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:

 

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


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

 

Demo的布局:

 

[html] view plaincopy
 
  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.  
  6.     <com.jingchen.waveview.WaveView 
  7.         android:layout_width="100dp" 
  8.         android:background="#ffffff" 
  9.         android:layout_height="match_parent" 
  10.         android:layout_centerInParent="true" /> 
  11.  
  12. </RelativeLayout> 


MainActivity的代码:

 

 

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

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

源码下载

posted @ 2015-05-18 01:03  brave-sailor  阅读(818)  评论(0)    收藏  举报