前言

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

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

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

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

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

实现

首先创建类SoundWaveView继承自View

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

private int viewWid = 1000;     // px
private int viewHeight = 100; // px @Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
viewWid = w;
viewHeight = h;
// ..
}

基本属性

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

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

设计监听器

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

public interface OnEvent {
void onMoveEnd(); // 停止拖动了 void onDragTouchEvent(MotionEvent event);
} private OnEvent onEventListener; private void tellOnMoveEnd() {
if (onEventListener != null) {
onEventListener.onMoveEnd();
}
}

绘制图形

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

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

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

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

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (dataList == null || dataList.isEmpty()) {
// draw nothing
drawMiddleLine(canvas);
return;
}
float x0 = viewWid / 2.0f; if (midIndex > 0) {
x0 = x0 - (barGapPx + barWidPx) * midIndex; // 可能是负数
}
for (int i = 0; i < dataList.size(); i++) {
float d = dataList.get(i);
float x = x0 + (barWidPx + barGapPx) * i;
if (x < 0) {
continue;
}
if (x > viewWid) {
break;
}
if (i <= midIndex) {
paint.setColor(leftColor);
} else {
paint.setColor(rightColor);
}
paint.setStrokeWidth(barWidPx);
float bh = (d / showMaxData) * viewHeight;
bh = Math.max(bh, 4); // 最小也要一点高度 (1)
float bhGap = (viewHeight - bh) / 2f;
canvas.drawLine(x, bhGap, x, viewHeight - bhGap, paint);
} drawMiddleLine(canvas);
} private void drawMiddleLine(Canvas canvas) {
paint.setColor(middleLineColor);
canvas.drawLine(viewWid / 2f, 0, viewWid / 2f, viewHeight, paint);
}
  1. 如果数据太小,为了更美观,也要显示一点东西

左右拖动

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

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

/**
* 单纯播放 展示 无交互
*/
public static final int MODE_PLAY = 1; /**
* 允许左右拖动
*/
public static final int MODE_CAN_DRAG = 2;

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

@Override
public boolean onTouchEvent(MotionEvent event) {
if (mode == MODE_CAN_DRAG) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
float dx = (downX - event.getX()); // 不要那么灵敏
float movePercent = dx / viewWid;
int dIndex = (int) (movePercent * barCount);
int targetMidIndex = downOldMidIndex + dIndex;
targetMidIndex = Math.max(0, targetMidIndex);
targetMidIndex = Math.min(targetMidIndex, dataList.size() - 1);
setMidIndex(targetMidIndex);
Log.d(TAG, "onTouchEvent-MOVE; dx: " + dx + ", dIndex: " + dIndex + "; targetMidIndex: " + targetMidIndex);
break;
case MotionEvent.ACTION_DOWN:
downX = event.getX();
downOldMidIndex = midIndex;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
downOldMidIndex = midIndex;
tellOnMoveEnd();
break;
}
if (onEventListener != null) {
onEventListener.onDragTouchEvent(event);
}
return true;
}
return super.onTouchEvent(event);
}

完整代码

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

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View; import androidx.annotation.Nullable; import java.util.ArrayList;
import java.util.List; /**
* @author an.rustfisher.com
*/
public class SoundWaveView extends View {
private static final String TAG = "rustAppSoundWaveView"; /**
* 单纯播放 展示 无交互
*/
public static final int MODE_PLAY = 1; /**
* 允许左右拖动
*/
public static final int MODE_CAN_DRAG = 2; private int mode = MODE_PLAY; // 1 播放
private List<Float> dataList = new ArrayList<>(100);
private float showMaxData = 40f; // 能显示的最大数据
private int midIndex = 0; // 在中间显示的数据的下标
private float barWidDp = 1.5f;
private float barWidPx = 3f;
private float barGapPx = barWidPx / 2;
private int barCount = 1; // 当前宽度能绘制多少个柱子
private int viewWid = 1000; // px
private int viewHeight = 100; // px private final Paint paint = new Paint();
private int leftColor = Color.GREEN;
private int rightColor = Color.LTGRAY;
private int middleLineColor = Color.parseColor("#55000000"); private float downX = 0; // getX
private int downOldMidIndex = 0; public interface OnEvent {
void onMoveEnd(); // 停止拖动了 void onDragTouchEvent(MotionEvent event);
} private OnEvent onEventListener; public SoundWaveView(Context context) {
this(context, null);
} public SoundWaveView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
} public SoundWaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
paint.setColor(Color.BLUE);
} @Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
viewWid = w;
viewHeight = h;
calBarPara();
Log.d(TAG, "onSizeChanged: " + w + ", " + h);
Log.d(TAG, "onSizeChanged: barWidPx: " + barWidPx);
} @Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (dataList == null || dataList.isEmpty()) {
// draw nothing
drawMiddleLine(canvas);
return;
}
float x0 = viewWid / 2.0f; // 绘制数据
if (midIndex > 0) {
x0 = x0 - (barGapPx + barWidPx) * midIndex; // 可能是负数
}
for (int i = 0; i < dataList.size(); i++) {
float d = dataList.get(i);
float x = x0 + (barWidPx + barGapPx) * i;
if (x < 0) {
continue;
}
if (x > viewWid) {
break;
}
if (i <= midIndex) {
paint.setColor(leftColor);
} else {
paint.setColor(rightColor);
}
paint.setStrokeWidth(barWidPx);
float bh = (d / showMaxData) * viewHeight;
bh = Math.max(bh, 4); // 最小也要一点高度
float bhGap = (viewHeight - bh) / 2f;
canvas.drawLine(x, bhGap, x, viewHeight - bhGap, paint);
}
drawMiddleLine(canvas);
} private void drawMiddleLine(Canvas canvas) {
paint.setColor(middleLineColor);
canvas.drawLine(viewWid / 2f, 0, viewWid / 2f, viewHeight, paint);
} public float getMidByPercent() {
return midIndex / (float) (dataList.size() - 1);
} @Override
public boolean onTouchEvent(MotionEvent event) {
if (mode == MODE_CAN_DRAG) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
float dx = (downX - event.getX()); // 不要那么灵敏
float movePercent = dx / viewWid;
int dIndex = (int) (movePercent * barCount);
int targetMidIndex = downOldMidIndex + dIndex;
targetMidIndex = Math.max(0, targetMidIndex);
targetMidIndex = Math.min(targetMidIndex, dataList.size() - 1);
setMidIndex(targetMidIndex);
Log.d(TAG, "onTouchEvent-MOVE; dx: " + dx + ", dIndex: " + dIndex + "; targetMidIndex: " + targetMidIndex);
break;
case MotionEvent.ACTION_DOWN:
downX = event.getX();
downOldMidIndex = midIndex;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
downOldMidIndex = midIndex;
tellOnMoveEnd();
break;
}
if (onEventListener != null) {
onEventListener.onDragTouchEvent(event);
}
return true;
}
return super.onTouchEvent(event);
} public void setMode(int mode) {
this.mode = mode;
} public int getMode() {
return mode;
} public int getMidIndex() {
return midIndex;
} public List<Float> getDataList() {
return dataList;
} public void setOnEventListener(OnEvent onEventListener) {
this.onEventListener = onEventListener;
} public void clear() {
dataList = new ArrayList<>();
midIndex = 0;
invalidate();
} private void calBarPara() {
barWidPx = dp2Px(barWidDp);
barGapPx = barWidPx;
barCount = (int) ((viewWid - barGapPx) / (barWidPx + barGapPx));
paint.setStrokeWidth(barWidPx);
Log.d(TAG, "calBarPara: barCount: " + barCount);
} public void setDataList(List<Float> input) {
dataList = new ArrayList<>(input);
midIndex = 0;
invalidate();
} public void setMidIndex(int midIndex) {
this.midIndex = midIndex;
invalidate();
} public void setMidEnd() {
setMidIndex(dataList.size() - 1);
} // 设置当前播放进度
public void setPlayPercent(float percent) {
midIndex = (int) (percent * (dataList.size() - 1));
if (percent >= 1) {
midIndex = dataList.size() - 1;
}
invalidate();
} public void setShowMaxData(float showMaxData) {
this.showMaxData = showMaxData;
} public float getShowMaxData() {
return showMaxData;
} // 不停地插入数据
public void addDataEnd(float f) {
dataList.add(f);
midIndex = dataList.size() - 1;
invalidate();
} public void setLeftColor(int leftColor) {
this.leftColor = leftColor;
} public void setRightColor(int rightColor) {
this.rightColor = rightColor;
} private float dp2Px(float dp) {
float density = getContext().getResources().getDisplayMetrics().density;
int mark = dp > 0 ? 1 : -1;
return dp * density * mark;
} private void tellOnMoveEnd() {
if (onEventListener != null) {
onEventListener.onMoveEnd();
}
}
}

layout中使用

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

activity中使用模拟数据

private void setData1() {
List<Float> dataList = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
dataList.add((float) (Math.random() * soundWaveView.getShowMaxData()));
}
soundWaveView.setDataList(dataList);
soundWaveView.setMidIndex(0); soundWaveView.setOnEventListener(new SoundWaveView.OnEvent() {
@Override
public void onMoveEnd() {
Log.d(TAG, "onMoveEnd: " + soundWaveView.getMidIndex());
} @Override
public void onDragTouchEvent(MotionEvent event) {
// 在这里可以收到触摸事件
}
});
}

运行示例:

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

相关代码在: AndroidTutorial - gitee

扩展阅读

Android 自定义View - 柱状波形图 wave view的更多相关文章

  1. echart使用自定义单个柱状颜色实现

    项目实践中遇到一个根据需要,当X轴等于某个值是,柱状变成特殊颜色的需求,大致有两个方案实现: 1.在前台遍历数据对象,判断设置: 2.在后台拼装数据是,按照格式要求拼装好: 手拉手,用Vue开发动态刷 ...

  2. 【朝花夕拾】Android自定义View篇之(一)View绘制流程

    前言 转载请申明转自[https://www.cnblogs.com/andy-songwei/p/10955062.html]谢谢! 自定义View.多线程.网络,被认为是Android开发者必须牢 ...

  3. 【Android - 自定义View】之自定义View浅析

    1.概述 Android自定义View / ViewGroup的步骤大致如下: 1) 自定义属性: 2) 选择和设置构造方法: 3) 重写onMeasure()方法: 4) 重写onDraw()方法: ...

  4. android自定义View&自定义ViewGroup(上)

    一般自定义view需要重写的方法 void onMeasure(int widthMeasureSpec, int heightMeasureSpec) void onSizeChanged(int ...

  5. Android 自定义View及其在布局文件中的使用示例

    前言: 尽管Android已经为我们提供了一套丰富的控件,如:Button,ImageView,TextView,EditText等众多控件,但是,有时候在项目开发过程中,还是需要开发者自定义一些需要 ...

  6. Android自定义View 画弧形,文字,并增加动画效果

    一个简单的Android自定义View的demo,画弧形,文字,开启一个多线程更新ui界面,在子线程更新ui是不允许的,但是View提供了方法,让我们来了解下吧. 1.封装一个抽象的View类   B ...

  7. Android自定义View4——统计图View

    1.介绍 周末在逛慕课网的时候,看到了一张学习计划报告图,详细记录了自己一周的学习情况,天天都是0节课啊!正好在学习Android自定义View,于是就想着自己去写了一个,这里先给出一张慕课网的图,和 ...

  8. (转)[原] Android 自定义View 密码框 例子

    遵从准则 暴露您view中所有影响可见外观的属性或者行为. 通过XML添加和设置样式 通过元素的属性来控制其外观和行为,支持和重要事件交流的事件监听器 详细步骤见:Android 自定义View步骤 ...

  9. Android 自定义View合集

    自定义控件学习 https://github.com/GcsSloop/AndroidNote/tree/master/CustomView 小良自定义控件合集 https://github.com/ ...

随机推荐

  1. pandas:聚合统计、数据分箱、分组可视化

    1.聚合统计 1.1描述统计 #df.describe(),对数据的总体特征进行描述 df.groupby('team').describe() df.groupby('team').describe ...

  2. 20212115朱时鸿-关于python技能树以及markdown编辑器的测评

    csdn的链接:https://blog.csdn.net/m0_68116569/article/details/124049366 计算机连接:https://gitee.com/zhu-shih ...

  3. Mac 睡眠唤醒 不睡眠 问题

    问题 之前一直有夜晚睡觉前电脑关机的习惯,主要是想着电脑也跟人一样️要休息.然后最近想着自己 Mac 干脆每天睡眠算了,省得每天开关机麻烦,所以就最近这段时间每次夜晚睡觉前主动去点了电脑的 「Slee ...

  4. 直接将A类库复制到vs中的B类库,但是解决方案菜单中不显示

    1.将要复制的文件夹复制粘贴到你要用的vs项目中 2.右键 添加   现有项目  选中xxxxx.csproj文件   点击  打开   就可以了

  5. 前端5jQuery

    内容概要 jQuery简介 查找标签 jQuery操作标签 jQuery事件操作 jQuery动画效果(了解) 前端第三方框架(基础) 内容详情 jQuery简介

  6. ExtJS 布局-Absolute布局(Absolute layout)

    更新记录: 2022年5月31日 发布本篇 1.说明 使用xy配置项设置子组件在父容器中绝对位置,本质是将子组件的CSS的position设置为absolute,然后使用x和y配置项映射到CSS的to ...

  7. 2.1 动为进程,静为程序 -进程概论 -《zobolの操作系统学习札记》

    2.1 动为进程,静为程序 -进程概论 目录 2.1 动为进程,静为程序 -进程概论 问1:发明进程的原因? 问2:现在计算机中的进程的定义是什么? 问3:为什么进程跟处理器的联系更密切? 问4:进程 ...

  8. JS:表达式

    js代码的形式: 1.直接量 2.表达式 3.语句 1; "a"; true; null; var a; function fn(){}; b; var c = 20; var f ...

  9. node环境下怎样优化引入多文件(实现自动化)

    const mocks = [];function getJsonFiles(jsonPath) {function findJsonFile(path) {let files = fs.readdi ...

  10. NC20861 兔子的逆序对

    NC20861 兔子的逆序对 题目 题目描述 兔子最近喜欢上了逆序对.一个逆序对 \((i,j)\) 需要满足 \(i < j\) 且 \(a_i > a_j\) .兔子觉得只是求一个序列 ...