Android仿kimi语音识别动效

216 阅读4分钟

最近做的AI项目里其中一part是用语音输入,设计说让参考kimi的动效,二话不说开搞

先看下效果

2025-03-21T08_51_33.272Z-874960.gif

任务拆解

  • 一、绘制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;

}

源码:github.com/mm46468648/…

以上就是这个控件的设计思路以及核心代码,谢谢观看