转载请声明出处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. 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, 0, 10);
  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的布局:

[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. <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的代码:

[java] view plaincopy

 
  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. }

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

源码下载

Android自定义控件实战——水流波动效果的实现WaveView的更多相关文章

  1. Android自定义控件简单实现ratingbar效果

    先上图: 一开始让我自定义控件我是拒绝的,因为android很早以前就有一个控件ratingbar,但是设置样式的时候我发现把图片设置小一点就显示不全,一直找不到解办法!(可以设置系统的自带的小样式) ...

  2. android自定义控件(5)-实现ViewPager效果

    对于系统的ViewGroup我们已经是十分熟悉了,最常用的LinearLayout和RelativeLayout几乎是天天要打交道,下面我们就来看看,如何一步一步将其实现: 一.首先当然也是最通常的新 ...

  3. Android自定义控件实战——滚动选择器PickerView

    转载请声明出处http://blog.csdn.net/zhongkejingwang/article/details/38513301 手机里设置闹钟需要选择时间,那个选择时间的控件就是滚动选择器, ...

  4. Android自定义控件实战——仿淘宝商品浏览界面

    转载请声明出处http://blog.csdn.net/zhongkejingwang/article/details/38656929 用手机淘宝浏览商品详情时,商品图片是放在后面的,在第一个Scr ...

  5. Android自定义控件练手——波浪效果

    这一次要绘制出波浪效果,也是小白的我第一次还望轻喷.首先当然是展示效果图啦: 一.首先来说说实现思路. 想到波浪效果,当然我第一反应是用正余弦波来设计啦(也能通过贝塞尔曲线,这里我不提及这个方法但是在 ...

  6. Android SurfaceView实战 打造抽奖转盘

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/41722441 ,本文出自:[张鸿洋的博客] 1.概述 今天给大家带来Surfac ...

  7. android自定义控件(4)-自定义水波纹效果

    一.实现单击出现水波纹单圈效果: 照例来说,还是一个自定义控件,观察这个效果,发现应该需要重写onTouchEvent和onDraw方法,通过在onTouchEvent中获取触摸的坐标,然后以这个坐标 ...

  8. Android 自定义控件实现刮刮卡效果 真的就只是刮刮卡么

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/40162163 , 本文出自:[张鸿洋的博客] 很久以前也过一个html5的刮刮卡 ...

  9. (转载)Android项目实战(二十八):使用Zxing实现二维码及优化实例

    Android项目实战(二十八):使用Zxing实现二维码及优化实例 作者:听着music睡 字体:[增加 减小] 类型:转载 时间:2016-11-21我要评论 这篇文章主要介绍了Android项目 ...

随机推荐

  1. 委托与Lambda表达式

    ~,先不急说委托和Lambda表达式,先看两个例子再说: 1. 通过委托,为一个数字加10,如下代码: class Program { private delegate int JiSuan(int ...

  2. 【转】20个令人敬畏的jQuery插件

    为网页设计师和开发推荐20个令人敬畏的jQuery插件.例如滑块,图像画廊,幻灯片插件,jQuery的导航菜单,jQuery文件上传,图像旋转器,标签的插件,用户界面​​元素,网络接触形式,模态窗口, ...

  3. Linux下 保存 git账号密码

    一.通过文件方式 1.在~/下, touch创建文件 .git-credentials, 用vim编辑此文件,输入内容格式: touch .git-credentials vim .git-crede ...

  4. Dokcer 组成原理简介

    首先来张图了解Docker的组成 重要 Docker在启动容器的时候,需要创建文件系统,为rootfs提供挂载点.最初Docker仅能在支持Aufs文件系统的Linux发行版上运行,但是由于Aufs未 ...

  5. 怎么理解angularjs中的服务?

    AngularJS中的服务其实就是提供一种方式抽取共用类库 比如说一些工具类方法,我们传统的做法就是自己写个 utility 类,把相关的工具方法填充到utility里面去,最后把utility类放到 ...

  6. 软件包管理_rpm命令管理_yum工具管理_文件归档压缩_源码包管理

    rpm命令管理软件 对于挂载的像U盘那种都会在midea目录下,但是会显示在桌面上 安装软件(i:install,v:verbose冗长的,h:human):rpm  -ivh  xxxx.rpm 安 ...

  7. Jmeter接口測试

    一.创建project.引包 1.创建JAVAproject 2.引入Jmeter中lib\ext基础包:ApacheJMeter_java.jar.ApacheJMeter_core.jar 3.引 ...

  8. POJ 3228Gold Transportation(二分+最大流)

    题目地址:POJ3288 这个题跟之前的一道题混了,感觉是一样的,所以连想都没怎么想就拆点然后求最短路然后二分求最大流了.结果连例子都只是,还一直以为又是哪里手残了..结果看了看例子,手算也确实不正确 ...

  9. PE框架学习之道:PE框架——style的配置

    1.在style.xml中定义style     <style id="NumberStyle"> <setting> <param name=&qu ...

  10. YUV / RGB 格式及快速转换算法

    1 前言 自然界的颜色千变万化,为了给颜色一个量化的衡量标准,就需要建立色彩空间模型来描述各种各样的颜色,由于人对色彩的感知是一个复杂的生理和心理联合作用 的过程,所以在不同的应用领域中为了更好更准确 ...