声浪效果是基于第三方实现的。
https://github.com/xfans/VoiceWaveView
将三方的 Kotlin 代码转 java 使用(按照他的readme 进行依赖,好像少了点东西,至少本项目跑不起来)
声浪效果在android 8 以上都是比较好的,不会出现断点的情况。但是在 android 8下,就会出现如下图所示的断点情况。
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.os.Handler;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* 音浪线
*/
public class VoiceWaveView extends View {
private static final String TAG = "VoiceWaveView";
private List<Integer> bodyWaveList = new ArrayList<>();
private List<Integer> headerWaveList = new ArrayList<>();
private List<Integer> footerWaveList = new ArrayList<>();
private List<Integer> waveList = new ArrayList<>();
private float lineSpace = 10f;
private float lineWidth = 20f;
private long duration = 200;
private int lineColor = Color.BLUE;
private Paint paintLine;
private Paint paintPathLine;
private ValueAnimator valueAnimator = ValueAnimator.ofFloat(0f, 1f);
private float valueAnimatorOffset = 1f;
private Handler valHandler = new Handler();
private Path linePath = new Path();
private boolean isStart = false;
private WaveMode waveMode = WaveMode.UP_DOWN;
private LineType lineType = LineType.BAR_CHART;
private int showGravity = Gravity.LEFT | Gravity.BOTTOM;
private Runnable runnable;
public VoiceWaveView(Context context) {
this(context, null);
}
public VoiceWaveView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public VoiceWaveView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(attrs);
}
private void init(@Nullable AttributeSet attrs) {
if (attrs != null) {
// Read and initialize attributes here
}
paintLine = new Paint();
paintLine.setAntiAlias(true);
paintLine.setStrokeCap(Paint.Cap.ROUND);
paintPathLine = new Paint();
paintPathLine.setAntiAlias(true);
paintPathLine.setStyle(Paint.Style.STROKE);
valueAnimator.addUpdateListener(animation -> {
valueAnimatorOffset = (float) animation.getAnimatedValue();
invalidate();
});
}
public void setLineSpace(float lineSpace) {
this.lineSpace = lineSpace;
}
public void setLineWidth(float lineWidth) {
this.lineWidth = lineWidth;
}
public void setDuration(long duration) {
this.duration = duration;
}
public void setLineColor(int lineColor) {
this.lineColor = lineColor;
}
public void setWaveMode(WaveMode waveMode) {
this.waveMode = waveMode;
}
public void setLineType(LineType lineType) {
this.lineType = lineType;
}
public void setShowGravity(int showGravity) {
this.showGravity = showGravity;
}
public VoiceWaveView addBody(int soundLevel) {
checkNum(soundLevel);
bodyWaveList.add(soundLevel);
return this;
}
public VoiceWaveView initBody(int length, int soundLevel) {
bodyWaveList.clear();
for (int i = 0; i < length; i++) {
addBody(soundLevel);
}
return this;
}
// TODO: 2023/11/1 中间弹的的逻辑
public VoiceWaveView refreshBody(int soundLevel) {
// 添加 soundLevel 到头部
bodyWaveList.add(0, soundLevel);
// 递减相邻元素的值
for (int i = 1; i < bodyWaveList.size() - 1; i++) {
int previousValue = bodyWaveList.get(i - 1);
int currentValue = bodyWaveList.get(i);
int nextValue = bodyWaveList.get(i + 1);
int updatedValue = Math.max(currentValue - 1, Math.max(previousValue, nextValue) - 2);
bodyWaveList.set(i, updatedValue);
}
return this;
}
/**
* 刷新最后一个
*
* @param soundLevel
*/
public void updateBody(int soundLevel) {
bodyWaveList.remove(bodyWaveList.size() - 1);
addBody(soundLevel);
}
public VoiceWaveView addHeader(int soundLevel) {
checkNum(soundLevel);
headerWaveList.add(soundLevel);
return this;
}
public VoiceWaveView addFooter(int soundLevel) {
checkNum(soundLevel);
footerWaveList.add(soundLevel);
return this;
}
private void checkNum(int soundLevel) {
if (soundLevel < 0 || soundLevel > 100) {
throw new IllegalArgumentException("num must be between 0 and 100");
}
}
public void start() {
if (isStart) {
return;
}
L.i(TAG, "start ");
isStart = true;
if (waveMode == WaveMode.UP_DOWN) {
valueAnimator.setDuration(duration);
valueAnimator.setRepeatMode(ValueAnimator.REVERSE);
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.start();
} else if (waveMode == WaveMode.LEFT_RIGHT) {
runnable = new Runnable() {
@Override
public void run() {
//日志类,自己构建即可
L.i(TAG, bodyWaveList.toString());
Integer last = bodyWaveList.remove(bodyWaveList.size() - 1);
bodyWaveList.add(0, last);
invalidate();
valHandler.postDelayed(this, duration);
}
};
valHandler.post(runnable);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
L.i(TAG, "onDraw ");
waveList.clear();
waveList.addAll(headerWaveList);
waveList.addAll(bodyWaveList);
waveList.addAll(footerWaveList);
linePath.reset();
paintPathLine.setStrokeWidth(lineWidth);
paintPathLine.setColor(lineColor);
paintLine.setStrokeWidth(lineWidth);
paintLine.setColor(lineColor);
float measuredWidth = getMeasuredWidth();
float measuredHeight = getMeasuredHeight();
float startX = 0f;
float startY = 0f;
float endX = 0f;
float endY = 0f;
for (int i = 0; i < waveList.size(); i++) {
float offset = 1f;
if (i >= headerWaveList.size() && i < (waveList.size() - footerWaveList.size())) {
offset = valueAnimatorOffset;
}
float lineHeight = (waveList.get(i) / 100.0f) * measuredHeight * offset;
int absoluteGravity = Gravity.getAbsoluteGravity(showGravity, getLayoutDirection());
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
int lineSize = waveList.size();
float allLineWidth = lineSize * (lineSpace + lineWidth);
if (allLineWidth < measuredWidth) {
startX = (i * (lineSpace + lineWidth) + lineWidth / 2) + ((measuredWidth - allLineWidth) / 2);
} else {
startX = i * (lineSpace + lineWidth) + lineWidth / 2;
}
endX = startX;
break;
case Gravity.RIGHT:
lineSize = waveList.size();
allLineWidth = lineSize * (lineSpace + lineWidth);
if (allLineWidth < measuredWidth) {
startX = (i * (lineSpace + lineWidth) + lineWidth / 2) + (measuredWidth - allLineWidth);
} else {
startX = i * (lineSpace + lineWidth) + lineWidth / 2;
}
endX = startX;
break;
case Gravity.LEFT:
startX = i * (lineSpace + lineWidth) + lineWidth / 2;
endX = startX;
break;
}
switch (showGravity & Gravity.VERTICAL_GRAVITY_MASK) {
case Gravity.TOP:
startY = 0f;
endY = lineHeight;
break;
case Gravity.CENTER_VERTICAL:
startY = (measuredHeight / 2 - lineHeight / 2);
endY = (measuredHeight / 2 + lineHeight / 2);
break;
case Gravity.BOTTOM:
startY = (measuredHeight - lineHeight);
endY = measuredHeight;
break;
}
if (lineType == LineType.BAR_CHART) {
canvas.drawLine(startX, startY, endX, endY, paintLine);
}
if (lineType == LineType.LINE_GRAPH) {
if (i == 0) {
linePath.moveTo(startX, startY);
float pathEndX = endX + (lineWidth / 2) + (lineSpace / 2);
linePath.lineTo(pathEndX, endY);
} else {
linePath.lineTo(startX, startY);
float pathEndX = endX + (lineWidth / 2) + (lineSpace / 2);
linePath.lineTo(pathEndX, endY);
}
}
}
if (lineType == LineType.LINE_GRAPH) {
canvas.drawPath(linePath, paintPathLine);
}
}
public void stop() {
L.i(TAG, "stop ");
isStart = false;
if (runnable != null) {
valHandler.removeCallbacks(runnable);
}
valueAnimator.cancel();
}
@Override
protected Parcelable onSaveInstanceState() {
// TODO onSaveInstanceState
return super.onSaveInstanceState();
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
// TODO onRestoreInstanceState
super.onRestoreInstanceState(state);
}
}
public enum LineType {
LINE_GRAPH(0),
BAR_CHART(1);
private int value;
private LineType(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
public enum WaveMode {
UP_DOWN,
LEFT_RIGHT
}