经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 移动开发 » Android » 查看文章
Android自定义View实现柱状波形图的绘制
来源:jb51  时间:2022/8/16 9:55:00  对本文有异议

前言

柱状波形图是一种常见的图形。一个个柱子按顺序排列,构成一个波形图。

柱子的高度由输入数据决定。如果输入的是音频的音量,则可得到一个声波图。

在一些音频软件中,我们也可以左右拖动声波,来改变音频的播放进度

本文举例的自定View,实现如下功能:

  • 以柱状形式展示数据的大小
  • 标明图形当前最中间的数据
  • 可以横向拖动进度,进度就是让某个特定的数据居中展示
  • 可以改变左右两边的柱子颜色
  • 可以调整柱子的宽度
  • 拖动完毕后监听当前进度

实现

首先创建类SoundWaveView继承自View

我们可以先记录给定的宽高,方便后面找到View的中间点

  1. private int viewWid = 1000; // px
  2. private int viewHeight = 100; // px
  3.  
  4. @Override
  5. protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  6. super.onSizeChanged(w, h, oldw, oldh);
  7. viewWid = w;
  8. viewHeight = h;
  9. // ..
  10. }

基本属性

例如柱子的颜色,宽度。可以设置个属性来记录,并开放出去可由外部来设置。

  1. private float barWidDp = 1.5f;
  2. private float barWidPx = 3f;
  3. private float barGapPx = barWidPx / 2;
  4. private int barCount = 1; // 当前宽度能绘制多少个柱子
  5.  
  6. private final Paint paint = new Paint();
  7. private int leftColor = Color.GREEN;
  8. private int rightColor = Color.LTGRAY;
  9. private int middleLineColor = Color.parseColor("#55000000");

设计监听器

拖动完毕后,可以将当前进度通知出去。也可以直接把触摸事件传出去。

  1. public interface OnEvent {
  2. void onMoveEnd(); // 停止拖动了
  3.  
  4. void onDragTouchEvent(MotionEvent event);
  5. }
  6.  
  7. private OnEvent onEventListener;
  8.  
  9. private void tellOnMoveEnd() {
  10. if (onEventListener != null) {
  11. onEventListener.onMoveEnd();
  12. }
  13. }

绘制图形

onDraw方法中根据数据绘制图形

本例没有设计背景,直接绘制数据。

图形需求之一是要求某个数据能居中显示,我们用midIndex来标记这个数据的下标。

比较简单粗暴的实现方法,遍历整个数据列表,计算出每个数据的x坐标。超出范围的不绘制,范围内的逐一绘制。

  1. @Override
  2. protected void onDraw(Canvas canvas) {
  3. super.onDraw(canvas);
  4. if (dataList == null || dataList.isEmpty()) {
  5. // draw nothing
  6. drawMiddleLine(canvas);
  7. return;
  8. }
  9. float x0 = viewWid / 2.0f;
  10.  
  11. if (midIndex > 0) {
  12. x0 = x0 - (barGapPx + barWidPx) * midIndex; // 可能是负数
  13. }
  14. for (int i = 0; i < dataList.size(); i++) {
  15. float d = dataList.get(i);
  16. float x = x0 + (barWidPx + barGapPx) * i;
  17. if (x < 0) {
  18. continue;
  19. }
  20. if (x > viewWid) {
  21. break;
  22. }
  23. if (i <= midIndex) {
  24. paint.setColor(leftColor);
  25. } else {
  26. paint.setColor(rightColor);
  27. }
  28. paint.setStrokeWidth(barWidPx);
  29. float bh = (d / showMaxData) * viewHeight;
  30. bh = Math.max(bh, 4); // 最小也要一点高度 (1)
  31. float bhGap = (viewHeight - bh) / 2f;
  32. canvas.drawLine(x, bhGap, x, viewHeight - bhGap, paint);
  33. }
  34.  
  35. drawMiddleLine(canvas);
  36. }
  37.  
  38. private void drawMiddleLine(Canvas canvas) {
  39. paint.setColor(middleLineColor);
  40. canvas.drawLine(viewWid / 2f, 0, viewWid / 2f, viewHeight, paint);
  41. }

如果数据太小,为了更美观,也要显示一点东西

左右拖动

本例给出的思路是在SoundWaveView中直接获取触摸事件并进行处理。

简单区分一下模式,分为纯展示和可拖动模式

  1. /**
  2. * 单纯播放 展示 无交互
  3. */
  4. public static final int MODE_PLAY = 1;
  5.  
  6. /**
  7. * 允许左右拖动
  8. */
  9. public static final int MODE_CAN_DRAG = 2;

复写onTouchEvent方法,如果是MODE_CAN_DRAG模式,则拦截触摸事件。判断拖动的横向(x)距离。

  1. @Override
  2. public boolean onTouchEvent(MotionEvent event) {
  3. if (mode == MODE_CAN_DRAG) {
  4. switch (event.getAction()) {
  5. case MotionEvent.ACTION_MOVE:
  6. float dx = (downX - event.getX()); // 不要那么灵敏
  7. float movePercent = dx / viewWid;
  8. int dIndex = (int) (movePercent * barCount);
  9. int targetMidIndex = downOldMidIndex + dIndex;
  10. targetMidIndex = Math.max(0, targetMidIndex);
  11. targetMidIndex = Math.min(targetMidIndex, dataList.size() - 1);
  12. setMidIndex(targetMidIndex);
  13. Log.d(TAG, "onTouchEvent-MOVE; dx: " + dx + ", dIndex: " + dIndex + "; targetMidIndex: " + targetMidIndex);
  14. break;
  15. case MotionEvent.ACTION_DOWN:
  16. downX = event.getX();
  17. downOldMidIndex = midIndex;
  18. break;
  19. case MotionEvent.ACTION_CANCEL:
  20. case MotionEvent.ACTION_UP:
  21. downOldMidIndex = midIndex;
  22. tellOnMoveEnd();
  23. break;
  24. }
  25. if (onEventListener != null) {
  26. onEventListener.onDragTouchEvent(event);
  27. }
  28. return true;
  29. }
  30. return super.onTouchEvent(event);
  31. }

完整代码

文件SoundWaveView.java,这个view主要目的是展现声波,取名为「SoundWave」

  1. import android.content.Context;
  2. import android.graphics.Canvas;
  3. import android.graphics.Color;
  4. import android.graphics.Paint;
  5. import android.util.AttributeSet;
  6. import android.util.Log;
  7. import android.view.MotionEvent;
  8. import android.view.View;
  9.  
  10. import androidx.annotation.Nullable;
  11.  
  12. import java.util.ArrayList;
  13. import java.util.List;
  14.  
  15. /**
  16. * @author an.rustfisher.com
  17. */
  18. public class SoundWaveView extends View {
  19. private static final String TAG = "rustAppSoundWaveView";
  20.  
  21. /**
  22. * 单纯播放 展示 无交互
  23. */
  24. public static final int MODE_PLAY = 1;
  25.  
  26. /**
  27. * 允许左右拖动
  28. */
  29. public static final int MODE_CAN_DRAG = 2;
  30.  
  31. private int mode = MODE_PLAY; // 1 播放
  32. private List<Float> dataList = new ArrayList<>(100);
  33. private float showMaxData = 40f; // 能显示的最大数据
  34. private int midIndex = 0; // 在中间显示的数据的下标
  35. private float barWidDp = 1.5f;
  36. private float barWidPx = 3f;
  37. private float barGapPx = barWidPx / 2;
  38. private int barCount = 1; // 当前宽度能绘制多少个柱子
  39. private int viewWid = 1000; // px
  40. private int viewHeight = 100; // px
  41.  
  42. private final Paint paint = new Paint();
  43. private int leftColor = Color.GREEN;
  44. private int rightColor = Color.LTGRAY;
  45. private int middleLineColor = Color.parseColor("#55000000");
  46.  
  47. private float downX = 0; // getX
  48. private int downOldMidIndex = 0;
  49.  
  50. public interface OnEvent {
  51. void onMoveEnd(); // 停止拖动了
  52.  
  53. void onDragTouchEvent(MotionEvent event);
  54. }
  55.  
  56. private OnEvent onEventListener;
  57.  
  58. public SoundWaveView(Context context) {
  59. this(context, null);
  60. }
  61.  
  62. public SoundWaveView(Context context, @Nullable AttributeSet attrs) {
  63. this(context, attrs, 0);
  64. }
  65.  
  66. public SoundWaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
  67. super(context, attrs, defStyleAttr);
  68. paint.setColor(Color.BLUE);
  69. }
  70.  
  71. @Override
  72. protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  73. super.onSizeChanged(w, h, oldw, oldh);
  74. viewWid = w;
  75. viewHeight = h;
  76. calBarPara();
  77. Log.d(TAG, "onSizeChanged: " + w + ", " + h);
  78. Log.d(TAG, "onSizeChanged: barWidPx: " + barWidPx);
  79. }
  80.  
  81. @Override
  82. protected void onDraw(Canvas canvas) {
  83. super.onDraw(canvas);
  84. if (dataList == null || dataList.isEmpty()) {
  85. // draw nothing
  86. drawMiddleLine(canvas);
  87. return;
  88. }
  89. float x0 = viewWid / 2.0f;
  90.  
  91. // 绘制数据
  92. if (midIndex > 0) {
  93. x0 = x0 - (barGapPx + barWidPx) * midIndex; // 可能是负数
  94. }
  95. for (int i = 0; i < dataList.size(); i++) {
  96. float d = dataList.get(i);
  97. float x = x0 + (barWidPx + barGapPx) * i;
  98. if (x < 0) {
  99. continue;
  100. }
  101. if (x > viewWid) {
  102. break;
  103. }
  104. if (i <= midIndex) {
  105. paint.setColor(leftColor);
  106. } else {
  107. paint.setColor(rightColor);
  108. }
  109. paint.setStrokeWidth(barWidPx);
  110. float bh = (d / showMaxData) * viewHeight;
  111. bh = Math.max(bh, 4); // 最小也要一点高度
  112. float bhGap = (viewHeight - bh) / 2f;
  113. canvas.drawLine(x, bhGap, x, viewHeight - bhGap, paint);
  114. }
  115. drawMiddleLine(canvas);
  116. }
  117.  
  118. private void drawMiddleLine(Canvas canvas) {
  119. paint.setColor(middleLineColor);
  120. canvas.drawLine(viewWid / 2f, 0, viewWid / 2f, viewHeight, paint);
  121. }
  122.  
  123. public float getMidByPercent() {
  124. return midIndex / (float) (dataList.size() - 1);
  125. }
  126.  
  127. @Override
  128. public boolean onTouchEvent(MotionEvent event) {
  129. if (mode == MODE_CAN_DRAG) {
  130. switch (event.getAction()) {
  131. case MotionEvent.ACTION_MOVE:
  132. float dx = (downX - event.getX()); // 不要那么灵敏
  133. float movePercent = dx / viewWid;
  134. int dIndex = (int) (movePercent * barCount);
  135. int targetMidIndex = downOldMidIndex + dIndex;
  136. targetMidIndex = Math.max(0, targetMidIndex);
  137. targetMidIndex = Math.min(targetMidIndex, dataList.size() - 1);
  138. setMidIndex(targetMidIndex);
  139. Log.d(TAG, "onTouchEvent-MOVE; dx: " + dx + ", dIndex: " + dIndex + "; targetMidIndex: " + targetMidIndex);
  140. break;
  141. case MotionEvent.ACTION_DOWN:
  142. downX = event.getX();
  143. downOldMidIndex = midIndex;
  144. break;
  145. case MotionEvent.ACTION_CANCEL:
  146. case MotionEvent.ACTION_UP:
  147. downOldMidIndex = midIndex;
  148. tellOnMoveEnd();
  149. break;
  150. }
  151. if (onEventListener != null) {
  152. onEventListener.onDragTouchEvent(event);
  153. }
  154. return true;
  155. }
  156. return super.onTouchEvent(event);
  157. }
  158.  
  159. public void setMode(int mode) {
  160. this.mode = mode;
  161. }
  162.  
  163. public int getMode() {
  164. return mode;
  165. }
  166.  
  167. public int getMidIndex() {
  168. return midIndex;
  169. }
  170.  
  171. public List<Float> getDataList() {
  172. return dataList;
  173. }
  174.  
  175. public void setOnEventListener(OnEvent onEventListener) {
  176. this.onEventListener = onEventListener;
  177. }
  178.  
  179. public void clear() {
  180. dataList = new ArrayList<>();
  181. midIndex = 0;
  182. invalidate();
  183. }
  184.  
  185. private void calBarPara() {
  186. barWidPx = dp2Px(barWidDp);
  187. barGapPx = barWidPx;
  188. barCount = (int) ((viewWid - barGapPx) / (barWidPx + barGapPx));
  189. paint.setStrokeWidth(barWidPx);
  190. Log.d(TAG, "calBarPara: barCount: " + barCount);
  191. }
  192.  
  193. public void setDataList(List<Float> input) {
  194. dataList = new ArrayList<>(input);
  195. midIndex = 0;
  196. invalidate();
  197. }
  198.  
  199. public void setMidIndex(int midIndex) {
  200. this.midIndex = midIndex;
  201. invalidate();
  202. }
  203.  
  204. public void setMidEnd() {
  205. setMidIndex(dataList.size() - 1);
  206. }
  207.  
  208. // 设置当前播放进度
  209. public void setPlayPercent(float percent) {
  210. midIndex = (int) (percent * (dataList.size() - 1));
  211. if (percent >= 1) {
  212. midIndex = dataList.size() - 1;
  213. }
  214. invalidate();
  215. }
  216.  
  217. public void setShowMaxData(float showMaxData) {
  218. this.showMaxData = showMaxData;
  219. }
  220.  
  221. public float getShowMaxData() {
  222. return showMaxData;
  223. }
  224.  
  225. // 不停地插入数据
  226. public void addDataEnd(float f) {
  227. dataList.add(f);
  228. midIndex = dataList.size() - 1;
  229. invalidate();
  230. }
  231.  
  232. public void setLeftColor(int leftColor) {
  233. this.leftColor = leftColor;
  234. }
  235.  
  236. public void setRightColor(int rightColor) {
  237. this.rightColor = rightColor;
  238. }
  239.  
  240. private float dp2Px(float dp) {
  241. float density = getContext().getResources().getDisplayMetrics().density;
  242. int mark = dp > 0 ? 1 : -1;
  243. return dp * density * mark;
  244. }
  245.  
  246. private void tellOnMoveEnd() {
  247. if (onEventListener != null) {
  248. onEventListener.onMoveEnd();
  249. }
  250. }
  251. }

layout中使用

  1. <com.rustfisher.tutorial2020.customview.soundwave.SoundWaveView
  2. android:id="@+id/sound_wave_view"
  3. android:layout_width="match_parent"
  4. android:layout_height="100dp"
  5. android:layout_marginTop="4dp"
  6. android:background="@android:color/white"
  7. app:layout_constraintTop_toTopOf="parent" />

activity中使用模拟数据

  1. private void setData1() {
  2. List<Float> dataList = new ArrayList<>();
  3. for (int i = 0; i < 1000; i++) {
  4. dataList.add((float) (Math.random() * soundWaveView.getShowMaxData()));
  5. }
  6. soundWaveView.setDataList(dataList);
  7. soundWaveView.setMidIndex(0);
  8.  
  9. soundWaveView.setOnEventListener(new SoundWaveView.OnEvent() {
  10. @Override
  11. public void onMoveEnd() {
  12. Log.d(TAG, "onMoveEnd: " + soundWaveView.getMidIndex());
  13. }
  14.  
  15. @Override
  16. public void onDragTouchEvent(MotionEvent event) {
  17. // 在这里可以收到触摸事件
  18. }
  19. });
  20. }

运行示例:

我们也可以扩展一下,假设不使用柱子,也可以把相邻点连接起来,形成折线图的样子。

相关代码在: AndroidTutorial - gitee

以上就是Android自定义View实现柱状波形图的绘制的详细内容,更多关于Android柱状波形图的资料请关注w3xue其它相关文章!

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号