Android 录音监听动画

一、前言

在很多app种内置了语音助手,也存在各种动画,主要原因是处理2个阶段问题,第一个是监听声音的等待效果,第二个是语意解析存在一定耗时的等待效果,前者要求有声音输入时有视觉反馈,后者让用户知道在处理某些事情,同时呢,这个效果还能互相切换,这是一般语音监听动画的设计逻辑。本文提供一种,希望对大家有所帮助。

效果图

(gif 有些卡,可能是压缩的原因)

二、实现方法

2.1 过渡动画

必须等待上一个动画结束后再切换为制定状态

2.2 声音抖动计算

本文没有明确计算线性音量,取出音量数据,进行了简单的计算

ini 复制代码
    public void updateShakeValue(int volume) {

        if (this.getVisibility() != View.VISIBLE || !isAttachedToWindow()) return;
        if (!isPlaying) return;

        float ratio = volume * 1.0f / this.mMaxShakeRange;

        if (ratio < 1f / 4) {
            ratio = 0;
        }
        if (ratio >= 1f / 4 && ratio < 2f / 4) {
            ratio = 1f / 4;
        }
        if (ratio >= 2f / 4 && ratio < 3f / 4) {
            ratio = 2f / 4;
        }

        if (ratio >= 3f / 4) {
            ratio = 1f;
        }
        updateShakeRatio(ratio);
    }

2.3 模式切换

需要LISTENING和LOADING 模式之间互相切换

ini 复制代码
   public void startPlay(final int state) {
        post(new Runnable() {
            @Override
            public void run() {
                setState(state);
                if (!isPlaying) {
                    mCurrentState = mNextState;
                }
                isPlaying = true;
                if (mNextState == mCurrentState) {
                    if (state == STATE_LISTENING) {
                        startListeningAnim();
                    } else if (state == STATE_LOADING) {
                        startLoadingAnim();
                    }
                } else {
                    startTransformAnim();
                }
            }
        });
    }

#loading 效果
radarView.startPlay(SpeechRadarView.STATE_LOADING);
#listening效果
radarView.startPlay(SpeechRadarView.STATE_LISTENING);

#停止播放
radarView.stopPlay();

2.3 抖动幅度范围,以适应不同类型的需求

ini 复制代码
#最大振幅
radarView.setMaxShakeRange(30);
#当前值
radarView.updateShakeValue(20);

三、全部代码

scss 复制代码
public class SpeechRadarView extends View {

    private static final long ANIMATION_CIRCLE_TIMEOUT = 1000;
    private static final long ANIMATION_LOADING_TIMEOUT = 800;
    private ValueAnimator mShakeAnimatorTimer;

    private int mFixedRadius = 0;
    private int mMaxRadius = 0;
    private TextPaint mPaint;

    private AnimationCircle[] mAnimationCircle = new AnimationCircle[2];
    private float mBullketStrokeWidthSize;

    private AnimatorSet mAnimatorTimerSet = null;
    private AnimatorSet mNextAnimatorTimerSet = null;
    private ValueAnimator mTransformAnimatorTimer;

    private int ANIMATION_MAIN_COLOR = 0x99FF8C14;
    private static final int MAIN_COLOR = 0xFFFF8C14;
    RectF arcBounds = new RectF();
    LinearInterpolator linearInterpolator = new LinearInterpolator();
    AccelerateDecelerateInterpolator accelerateDecelerateInterpolator = new AccelerateDecelerateInterpolator();
    public static final int STATE_LOADING = 0;
    public static final int STATE_LISTENING = 1;

    private int mCurrentState = STATE_LOADING;
    private int mNextState = STATE_LOADING; //过渡值

    private float LOADING_STOKE_WIDTH = 0;

    private int LOADING_START_ANGLE = 90;
    private int mCurrentAngle = LOADING_START_ANGLE;

    private int mTransformLoadingColor = Color.TRANSPARENT;
    private int mTransformListeningColor = Color.TRANSPARENT;

    private boolean isPlaying = false;

    private float mShakeRatio = 0;
    private float mNextShakeRatio = 0;
    private long mStartShakeTime = 0;
    private int mMaxShakeRange = 100;

    public SpeechRadarView(Context context) {
        this(context, null);
    }

    public SpeechRadarView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SpeechRadarView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPaint();
    }

    private void setState(int state) {
        if (this.mNextState == state) {
            return;
        }
        this.mNextState = state;

    }

    public int getState() {
        return mNextState;
    }

    private void initPaint() {
        // 实例化画笔并打开抗锯齿
        mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setAntiAlias(true);
        mPaint.setPathEffect(new CornerPathEffect(10)); //设置线条类型
        mPaint.setStrokeWidth(dip2px(1));
        mPaint.setTextSize(dip2px((12)));
        mPaint.setStyle(Paint.Style.STROKE);

        mBullketStrokeWidthSize =  dip2px(5);
        LOADING_STOKE_WIDTH =  dip2px(5);
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);

        if (widthMode != MeasureSpec.EXACTLY) {
            width = (int) dip2px(210);
        }
        if (heightMode != MeasureSpec.EXACTLY) {
            height = (int) dip2px(210);
        }
        setMeasuredDimension(width, height);

    }

    public float dip2px(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);


        int width = getWidth();
        int height = getHeight();

        if (width == 0 || height == 0) return;

        int centerX = width / 2;
        int centerY = height / 2;

        int diameter = Math.min(width, height) / 2;
        mFixedRadius = diameter / 3;
        mMaxRadius = diameter;
        initAnimationCircle();

        if (!isInEditMode() && !isPlaying) return;

        int layerId = saveLayer(canvas, centerX, centerY);

        if (mNextState == mCurrentState) {

            if (mCurrentState == STATE_LISTENING) {
                drawAnimationCircle(canvas);
                drawFixCircle(canvas, MAIN_COLOR);
                drawFlashBullket(canvas, Color.WHITE, mShakeRatio);
                mShakeRatio = 0;
            } else if (mCurrentState == STATE_LOADING) {
                drawLoadingArc(canvas, MAIN_COLOR);
                drawFlashBullket(canvas, MAIN_COLOR, 0);
            }
        } else {
            if (this.mNextState == STATE_LISTENING) {

                drawLoadingArc(canvas, mTransformLoadingColor);
                drawFixCircle(canvas, mTransformListeningColor);
                drawFlashBullket(canvas, Color.WHITE, 0);

            } else {
                drawFixCircle(canvas, mTransformListeningColor);
                drawLoadingArc(canvas, mTransformLoadingColor);
                drawFlashBullket(canvas, MAIN_COLOR, 0);
            }
        }

        restoreLayer(canvas, layerId);

    }


    private void drawLoadingArc(Canvas canvas, int color) {
        int oldColor = mPaint.getColor();
        Paint.Style style = mPaint.getStyle();
        float strokeWidth = mPaint.getStrokeWidth();

        mPaint.setStrokeWidth(LOADING_STOKE_WIDTH);
        float innerOffset = LOADING_STOKE_WIDTH / 2;
        mPaint.setColor(color);
        mPaint.setStyle(Paint.Style.STROKE);
        arcBounds.set(-mFixedRadius + innerOffset, -mFixedRadius + innerOffset, mFixedRadius - innerOffset, mFixedRadius - innerOffset);
        canvas.drawArc(arcBounds, mCurrentAngle, 270, false, mPaint);


        mPaint.setColor(oldColor);
        mPaint.setStyle(style);
        mPaint.setStrokeWidth(strokeWidth);
    }

    private void drawFlashBullket(Canvas canvas, int color, float fraction) {
        int bullketZoneWidth = mFixedRadius;
        int bullketZoneHeight = mFixedRadius * 2 / 3;
        int minHeight = (int) (bullketZoneHeight / 3f);
        int maxRangeHeight = (int) (bullketZoneHeight * 2 / 3f);
        drawFlashBullket(canvas, bullketZoneWidth, color, minHeight, (maxRangeHeight * fraction));
    }

    private void drawFlashBullket(Canvas canvas, int width, int color, int height, float delta) {


        int offset = (int) ((width - mBullketStrokeWidthSize * 4) / 3);
        int oldColor = mPaint.getColor();
        float strokeWidth = mPaint.getStrokeWidth();

        if (delta < 0f) {
            delta = 0f;
        }

        mPaint.setColor(color);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStrokeWidth(mBullketStrokeWidthSize);
        for (int i = 0; i < 4; i++) {
            int startX = (int) (i * (offset + mBullketStrokeWidthSize) - width / 2 + mBullketStrokeWidthSize / 2);
            if (i == 0 || i == 3) {
                canvas.drawLine(startX, -height / 2F + delta * 1 / 3, startX, height / 2F + delta * 1 / 3, mPaint);
            } else {
                canvas.drawLine(startX, -(height / 2F + delta * 2 / 3), startX, (height / 2F + delta * 2 / 3), mPaint);
            }
        }

        mPaint.setColor(oldColor);
        mPaint.setStrokeWidth(strokeWidth);
    }


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

    }

    private void drawAnimationCircle(Canvas canvas) {
        for (int i = 0; i < mAnimationCircle.length; i++) {
            AnimationCircle circle = mAnimationCircle[i];
            if (circle.radius > mFixedRadius) {
                drawCircle(canvas, circle.color, circle.radius);
                Log.e("AnimationCircle", "i=" + i + " , radius=" + circle.radius);
            } else {
                Log.d("AnimationCircle", "i=" + i + " , radius=" + circle.radius);
            }
        }
    }

    private void initAnimationCircle() {
        for (int i = 0; i < mAnimationCircle.length; i++) {
            if (mAnimationCircle[i] == null) {
                if (i == 0) {
                    mAnimationCircle[i] = new AnimationCircle(mMaxRadius, mFixedRadius, 0x88FF8C14);
                } else {
                    mAnimationCircle[i] = new AnimationCircle(mMaxRadius, mFixedRadius, 0x99FF8C14);
                }
            } else {
                if (mAnimationCircle[i].token != mMaxRadius) {
                    mAnimationCircle[i].radius = mFixedRadius;
                    mAnimationCircle[i].token = mMaxRadius;
                }
            }

        }
    }


    private void drawCircle(Canvas canvas, int color, float radius) {
        int oldColor = mPaint.getColor();
        Paint.Style style = mPaint.getStyle();
        float strokeWidth = mPaint.getStrokeWidth();


        mPaint.setStrokeWidth(0);
        mPaint.setColor(color);
        mPaint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(0, 0, radius, mPaint);


        mPaint.setColor(oldColor);
        mPaint.setStyle(style);
        mPaint.setStrokeWidth(strokeWidth);
    }

    private void restoreLayer(Canvas canvas, int save) {
        canvas.restoreToCount(save);
    }

    private int saveLayer(Canvas canvas, int centerX, int centerY) {
        int save = canvas.save();
        canvas.translate(centerX, centerY);
        return save;
    }

    private void drawFixCircle(Canvas canvas, int color) {
        drawCircle(canvas, color, mFixedRadius);
    }

    public void startPlay(final int state) {
        post(new Runnable() {
            @Override
            public void run() {
                setState(state);
                if (!isPlaying) {
                    mCurrentState = mNextState;
                }
                isPlaying = true;
                if (mNextState == mCurrentState) {
                    if (state == STATE_LISTENING) {
                        startListeningAnim();
                    } else if (state == STATE_LOADING) {
                        startLoadingAnim();
                    }
                } else {
                    startTransformAnim();
                }
            }
        });
    }

    public void startLoadingAnim() {

        if (mAnimatorTimerSet != null) {
            mAnimatorTimerSet.cancel();
        }

        mAnimatorTimerSet = getAnimatorLoadingSet();
        if (mAnimatorTimerSet != null) {
            mAnimatorTimerSet.start();
        }
    }

    private void startTransformAnim() {
        if (mNextAnimatorTimerSet != null) {
            mNextAnimatorTimerSet.cancel();
        }
        if (mTransformAnimatorTimer != null) {
            mTransformAnimatorTimer.cancel();
        }
        mTransformAnimatorTimer = buildTransformAnimatorTimer(mCurrentState, mNextState);

        if (mNextState == STATE_LISTENING) {
            mNextAnimatorTimerSet = getAnimatorCircleSet();
        } else {
            mNextAnimatorTimerSet = getAnimatorLoadingSet();
        }

        if (mTransformAnimatorTimer != null) {
            mTransformAnimatorTimer.start();
        }
        if (mNextAnimatorTimerSet != null) {
            mNextAnimatorTimerSet.start();
        }
    }

    public void startListeningAnim() {
        if (mAnimatorTimerSet != null) {
            mAnimatorTimerSet.cancel();
        }
        AnimatorSet animatorTimerSet = getAnimatorCircleSet();
        if (animatorTimerSet == null) return;

        mAnimatorTimerSet = animatorTimerSet;
        mAnimatorTimerSet.start();
    }

    @Nullable
    private AnimatorSet getAnimatorCircleSet() {
        AnimatorSet animatorTimerSet = new AnimatorSet();
        ValueAnimator firstAnimatorTimer = buildCircleAnimatorTimer(mAnimationCircle[0]);
        ValueAnimator secondAnimatorTimer = buildCircleAnimatorTimer(mAnimationCircle[1]);
        if (firstAnimatorTimer == null || secondAnimatorTimer == null) return null;
        secondAnimatorTimer.setStartDelay(ANIMATION_CIRCLE_TIMEOUT / 2);
        animatorTimerSet.playTogether(firstAnimatorTimer, secondAnimatorTimer);
        return animatorTimerSet;
    }


    @Nullable
    private AnimatorSet getAnimatorLoadingSet() {
        ValueAnimator valueAnimator = buildLoadingAnimatorTimer();
        if (valueAnimator == null) return null;
        AnimatorSet animatorTimerSet = new AnimatorSet();
        animatorTimerSet.play(valueAnimator);
        return animatorTimerSet;
    }

    @Nullable
    private ValueAnimator buildCircleAnimatorTimer(final AnimationCircle circle) {
        if (mFixedRadius <= 0 || circle == null) return null;
        ValueAnimator animatorTimer = ValueAnimator.ofFloat(mFixedRadius, Math.min(getWidth(),getHeight()) / 2F);
        animatorTimer.setDuration(ANIMATION_CIRCLE_TIMEOUT);
        animatorTimer.setRepeatCount(ValueAnimator.INFINITE);
        animatorTimer.setInterpolator(linearInterpolator);
        animatorTimer.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float dx = (float) animation.getAnimatedValue();
                float fraction = 1 - animation.getAnimatedFraction();

                float radius = dx;
                int color = argb((int) (Color.alpha(ANIMATION_MAIN_COLOR) * fraction), Color.red(ANIMATION_MAIN_COLOR), Color.green(ANIMATION_MAIN_COLOR), Color.blue(ANIMATION_MAIN_COLOR));

                if (mCurrentState != mNextState) {
                    color = Color.TRANSPARENT;
                }
                if (circle.radius != radius || circle.color != color) {
                    circle.radius = radius;
                    circle.color = color;
                    postInvalidate();
                }

            }
        });
        return animatorTimer;
    }

    @Nullable
    private ValueAnimator buildLoadingAnimatorTimer() {
        if (mFixedRadius <= 0) return null;
        ValueAnimator animatorTimer = ValueAnimator.ofFloat(0, 1);
        animatorTimer.setDuration(ANIMATION_LOADING_TIMEOUT);
        animatorTimer.setRepeatCount(ValueAnimator.INFINITE);
        animatorTimer.setInterpolator(new AccelerateDecelerateInterpolator());
        animatorTimer.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float fraction = animation.getAnimatedFraction();
                int angle = (int) (LOADING_START_ANGLE + fraction * 360);
                if (mCurrentAngle != angle) {
                    mCurrentAngle = angle;
                    postInvalidate();
                }
            }
        });
        return animatorTimer;
    }


    @Nullable
    private ValueAnimator buildTransformAnimatorTimer(final int currentState, final int nextState) {
        if (mFixedRadius <= 0) return null;

        final int alpha = Color.alpha(MAIN_COLOR);
        final int red = Color.red(MAIN_COLOR);
        final int green = Color.green(MAIN_COLOR);
        final int blue = Color.blue(MAIN_COLOR);


        ValueAnimator animatorTimer = ValueAnimator.ofFloat(currentState, nextState);
        animatorTimer.setDuration(ANIMATION_LOADING_TIMEOUT);
        animatorTimer.setInterpolator(accelerateDecelerateInterpolator);
        animatorTimer.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float animatedValue = (float) animation.getAnimatedValue();
                if (mCurrentState != mNextState) {
                    mTransformListeningColor = argb((int) (alpha * animatedValue), red, green, blue);
                    mTransformLoadingColor = argb((int) (alpha * (1 - animatedValue)), red, green, blue);
                    Log.d("animatedValue", " --- >" + animatedValue);
                    postInvalidate();
                }

            }
        });

        animatorTimer.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                resetAnimationState();
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                super.onAnimationCancel(animation);
                resetAnimationState();
            }
        });
        return animatorTimer;
    }

    private void resetAnimationState() {
        mCurrentState = mNextState;

        if (mAnimatorTimerSet != null) {
            if (mAnimatorTimerSet != mNextAnimatorTimerSet) {
                mAnimatorTimerSet.cancel();
            }
        }
        mAnimatorTimerSet = mNextAnimatorTimerSet;
    }


    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        stopPlay();

    }

    public void stopPlay() {
        isPlaying = false;
        mCurrentAngle = LOADING_START_ANGLE;
        try {
            if (mAnimatorTimerSet != null) {
                mAnimatorTimerSet.cancel();
            }
            if (mNextAnimatorTimerSet != null) {
                mNextAnimatorTimerSet.cancel();
            }
            if (mShakeAnimatorTimer != null) {
                mShakeAnimatorTimer.cancel();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        resetAnimationCircle();
        postInvalidate();
    }

    private void resetAnimationCircle() {
        for (AnimationCircle circle : mAnimationCircle) {
            if (circle != null) {
                circle.radius = mFixedRadius;
            }
        }
    }

    public static int argb(
            @IntRange(from = 0, to = 255) int alpha,
            @IntRange(from = 0, to = 255) int red,
            @IntRange(from = 0, to = 255) int green,
            @IntRange(from = 0, to = 255) int blue) {
        return (alpha << 24) | (red << 16) | (green << 8) | blue;
    }

    public boolean isPlaying() {
        return isPlaying;
    }


    private void updateShakeRatio(final float ratio) {
        long currentTimeMillis = System.currentTimeMillis();
        if (currentTimeMillis - mStartShakeTime >= 150) {
            mNextShakeRatio = ratio;
            if (mShakeRatio != mNextShakeRatio) {
                startShakeAnimation();
            }
            mStartShakeTime = currentTimeMillis;
        }
    }


    private void startShakeAnimation() {

        if (mShakeAnimatorTimer != null) {
            mShakeAnimatorTimer.cancel();
        }

        mShakeAnimatorTimer = ValueAnimator.ofFloat(mShakeRatio, mNextShakeRatio);
        mShakeAnimatorTimer.setDuration(100);
        mShakeAnimatorTimer.setInterpolator(accelerateDecelerateInterpolator);
        mShakeAnimatorTimer.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float ratio = (float) animation.getAnimatedValue();
                if (mShakeRatio != ratio) {
                    mShakeRatio = ratio;
                    postInvalidate();
                }
            }
        });

        mShakeAnimatorTimer.start();

    }

    public void setMaxShakeRange(int maxShakeRange) {
        this.mMaxShakeRange = maxShakeRange;
        if (this.mMaxShakeRange <= 0) this.mMaxShakeRange = 100;
    }

    public void updateShakeValue(int volume) {

        if (this.getVisibility() != View.VISIBLE || !isAttachedToWindow()) return;
        if (!isPlaying) return;

        float ratio = volume * 1.0f / this.mMaxShakeRange;

        if (ratio < 1f / 4) {
            ratio = 0;
        }
        if (ratio >= 1f / 4 && ratio < 2f / 4) {
            ratio = 1f / 4;
        }
        if (ratio >= 2f / 4 && ratio < 3f / 4) {
            ratio = 2f / 4;
        }

        if (ratio >= 3f / 4) {
            ratio = 1f;
        }
        updateShakeRatio(ratio);
    }

    public boolean isAttachedToWindow() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            return super.isAttachedToWindow();
        } else {
            return getWindowToken() != null;
        }
    }

    private static class AnimationCircle {
        private float radius;
        private int color;
        private int token;

        AnimationCircle(int token, int radius, int color) {
            this.radius = radius;
            this.color = color;
            this.token = token;
        }
    }
}

四、总结

总体上这个设计不是很难,难点是状态切换的一些过渡设计,保证上一个动画结束完成之后才能展示下一个动画,其词就是抖动逻辑,实际上也不是很复杂,第三方SDK的音量值一般都是有的,实时获取就好了。

相关推荐
路在脚下@23 分钟前
spring boot的配置文件属性注入到类的静态属性
java·spring boot·sql
森屿Serien26 分钟前
Spring Boot常用注解
java·spring boot·后端
轻口味1 小时前
命名空间与模块化概述
开发语言·前端·javascript
前端小小王1 小时前
React Hooks
前端·javascript·react.js
苹果醋32 小时前
React源码02 - 基础知识 React API 一览
java·运维·spring boot·mysql·nginx
迷途小码农零零发2 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
Hello.Reader2 小时前
深入解析 Apache APISIX
java·apache
娃哈哈哈哈呀2 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
菠萝蚊鸭2 小时前
Dhatim FastExcel 读写 Excel 文件
java·excel·fastexcel