zoukankan      html  css  js  c++  java
  • Android自定义控件实战——水流波动效果的实现WaveView

    水流波动的波形都是三角波,曲线是正余弦曲线,但是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. }  

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

    源码下载

  • 相关阅读:
    [POI2007]山峰和山谷Grz
    [POI2007]驾驶考试egz
    [POI2007]立方体大作战tet
    BZOJ1085 [SCOI2005]骑士精神
    BZOJ1975 [Sdoi2010]魔法猪学院
    codeforces754D Fedor and coupons
    UOJ79 一般图最大匹配
    BZOJ3944 Sum
    BZOJ3434 [Wc2014]时空穿梭
    UOJ58 【WC2013】糖果公园
  • 原文地址:https://www.cnblogs.com/Free-Thinker/p/4749870.html
Copyright © 2011-2022 走看看