最近做的AI项目里其中一part是用语音输入,设计说让参考kimi的动效,二话不说开搞
先看下效果
任务拆解
- 一、绘制17个圆角矩形,并带有一定间隔
- 二、根据输入的音量大小改变圆角矩形的高度
- 三、每个矩形高度需要按照一定的比例增加
- 四、每隔一定的时间间隔让比例值随机更新一下
- 五、绘制静音时的效果
- 六、让动画效果更平滑一些
任务实现
一、绘制17个圆角矩形,并带有一定间隔。这是整个控件的基础样式
private static final int WAVE_COUNT = 17;
int waveWidth = 12;
int waveSpace = 10;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int height = getHeight();
int baseWaveHeight = (int) ((currentVolume - minVolume) / (float) (maxVolume - minVolume) * height);
for (int i = 0; i < WAVE_COUNT; i++) {
int left = i * (this.waveWidth + waveSpace);
int top = (height - baseWaveHeight);
int right = left + this.waveWidth;
int bottom = top + baseWaveHeight;
//绘制圆角矩形,radius为60
canvas.drawRoundRect(left, top, right, bottom, 60, 60, paint);
}
}
二、根据输入的音量大小改变圆角矩形的高度, 通过改变音量属性,通知视图更新
public void setMaxVolume(int maxVolume) {
this.maxVolume = maxVolume;
}
public void setMinVolume(int minVolume) {
this.minVolume = minVolume;
}
private boolean isUpdating = true;
public void setCurrentVolume(int currentVolume) {
this.currentVolume = currentVolume;
invalidate();
}
三、每个矩形高度需要按照一定的比例增加, 如果都以同样的高度增加,那样看起来很傻,所以创建一个数组存放每个位置高度的变化比例,绘制的时候让音量的高度乘以这个比例,所以绘制代码改为。
int[] heightRatios1 = {4, 4, 6, 8, 6, 4, 4, 6, 8, 6, 4, 4, 6, 8, 6, 4, 4}; //音量条高度比例
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int height = getHeight();
int baseWaveHeight = (int) ((currentVolume - minVolume) / (float) (maxVolume - minVolume) * height);
//打印baseWaveHeight的值,方便调试
//System.out.println("baseWaveHeight:" + baseWaveHeight);
// 定义高度变化的比例,让波形有高低变化
int[] heightRatios = this.heightRatios1;
for (int i = 0; i < WAVE_COUNT; i++) {
int left = i * (this.waveWidth + waveSpace);
int top = (height - baseWaveHeight * heightRatios[i] / 8) / 2;
int right = left + this.waveWidth;
int bottom = top + baseWaveHeight * heightRatios[i] / 8;
//绘制圆角矩形,radius为60
canvas.drawRoundRect(left, top, right, bottom, 60, 60, paint);
}
}
}
四、每隔一定的时间间隔让比例值随机更新一下,如果每次都在相同位置更新音量的高度看起来还是很死板,所以把比例数组随机打乱一些,但是又不能每次都随机,这样的效果绘制出来会很跳跃,体感不好。随意绘制代码改为这样,这样说话的时候动画效果基本搞定了
int randomUpdateFrequency = 0; //随机动效更新频次
private static final int TIME_CLICK_SPACE_DEFAULT = 30;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int height = getHeight();
int baseWaveHeight = (int) ((currentVolume - minVolume) / (float) (maxVolume - minVolume) * height);
//打印baseWaveHeight的值,方便调试
//System.out.println("baseWaveHeight:" + baseWaveHeight);
// 定义高度变化的比例,让波形有高低变化
int[] heightRatios = this.heightRatios1;
//每30次随机高度变化
if (randomUpdateFrequency % TIME_CLICK_SPACE_DEFAULT == 0) {
shuffleArray(heightRatios);
}
if (randomUpdateFrequency >= Integer.MAX_VALUE - 1) {
randomUpdateFrequency = 0;
}
randomUpdateFrequency++;
currentSilentIndex = 0;
for (int i = 0; i < WAVE_COUNT; i++) {
int left = i * (this.waveWidth + waveSpace);
int top = (height - baseWaveHeight * heightRatios[i] / 8) / 2;
int right = left + this.waveWidth;
int bottom = top + baseWaveHeight * heightRatios[i] / 8;
//绘制圆角矩形,radius为60
canvas.drawRoundRect(left, top, right, bottom, 60, 60, paint);
}
}
void shuffleArray(int[] array) {
Random random = new Random();
for (int i = array.length - 1; i > 0; i--) {
int index = random.nextInt(i + 1);
// 交换 array[i] 和 array[index]
int temp = array[i];
array[i] = array[index];
array[index] = temp;
}
}
五、绘制静音时的效果,我们判断音量小于20的时候就算没有说话的状态,静音时的动效时从左到右依次增加高度,为了限制绘制的过快同时增加了一个频次的限制
private void defaultAnim2(Canvas canvas, int baseWaveHeight, int[] heightRatios, int height) {
//先每个绘制高度为12的矩形
for (int i = 0; i < WAVE_COUNT; i++) {
int left = i * (this.waveWidth + waveSpace);
int top = (height - baseWaveHeight * heightRatios[i] / 8) / 2;
int right = left + this.waveWidth;
int bottom = top + baseWaveHeight * heightRatios[i] / 8;
// canvas.drawRoundRect(left, top, right, bottom, paint);
//绘制圆角矩形,radius为60
canvas.drawRoundRect(left, top, right, bottom, 60, 60, paint);
}
//对currentIndex进行自增操作,并取余,实现循环
int index = (this.currentSilentIndex) % heightRatios.length;
//绘制他的左边一个
if (index > 0) {
int left = (index - 1) * (this.waveWidth + waveSpace);
int top = (height - baseWaveHeight * 12 / 8) / 2;
int right = left + this.waveWidth;
int bottom = top + baseWaveHeight * 12 / 8;
//绘制圆角矩形,radius为60
canvas.drawRoundRect(left, top, right, bottom, 60, 60, paint);
}
//绘制他的右边一个
if (index < heightRatios.length - 1) {
int left = (index + 1) * (this.waveWidth + waveSpace);
int top = (height - baseWaveHeight * 12 / 8) / 2;
int right = left + this.waveWidth;
int bottom = top + baseWaveHeight * 12 / 8;
canvas.drawRoundRect(left, top, right, bottom, 60, 60, paint);
}
//让第index矩形变高
int left = index * (this.waveWidth + waveSpace);
int top = (height - baseWaveHeight * 16 / 8) / 2;
int right = left + this.waveWidth;
int bottom = top + baseWaveHeight * 16 / 8;
// canvas.drawRoundRect(left, top, right, bottom, paint);
//绘制圆角矩形,radius为60
canvas.drawRoundRect(left, top, right, bottom, 60, 60, paint);
//每3次执行这个方法,让currentIndex自增1
if (silentUpdateFrequency % 3 == 0) {
if (currentSilentIndex >= heightRatios.length - 1) {
currentSilentIndex = 0;
}
currentSilentIndex++;
}
if(silentUpdateFrequency >= Integer.MAX_VALUE - 1)
silentUpdateFrequency = 0;
else if(silentUpdateFrequency < 0)
silentUpdateFrequency = 0;
else
//否则自增1(防止溢出
silentUpdateFrequency++;
}
六、让动画效果更平滑一些,ui反馈音量更新比较生硬,因为音量回调的很快,刷新频率很高,页面的绘制是根据输入音量的变化而变化的,为了解决这个问题,我决定加入一个动画,让页面的绘制更平滑一些。关键点,是下次的动画的开始要以上次的音量开始。
ValueAnimator animator;
int lastVolume = 0;
public void setVolume(float volume, float maxVolume, float minVolume) {
if(maxVolume <= VoiceWaveView.waveSilentVolume){
mBinding.voiceWaveView.setMaxVolume((int) maxVolume);
mBinding.voiceWaveView.setMinVolume((int) minVolume);
mBinding.voiceWaveView.setCurrentVolume((int) volume);
return;
}
animator = ValueAnimator.ofFloat(lastVolume, volume);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mBinding.voiceWaveView.setMaxVolume((int) maxVolume);
mBinding.voiceWaveView.setMinVolume((int) minVolume);
float value = (float) animation.getAnimatedValue();
mBinding.voiceWaveView.setCurrentVolume((int) value);
}
});
animator.setDuration(500);
animator.start();
lastVolume = (int) volume;
}