一、前言
在很多app种内置了语音助手,也存在各种动画,主要原因是处理2个阶段问题,第一个是监听声音的等待效果,第二个是语意解析存在一定耗时的等待效果,前者要求有声音输入时有视觉反馈,后者让用户知道在处理某些事情,同时呢,这个效果还能互相切换,这是一般语音监听动画的设计逻辑。本文提供一种,希望对大家有所帮助。
效果图
(gif 有些卡,可能是压缩的原因)
二、实现方法
2.1 过渡动画
必须等待上一个动画结束后再切换为制定状态
2.2 声音抖动计算
本文没有明确计算线性音量,取出音量数据,进行了简单的计算
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 模式之间互相切换
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.4 抖动幅度范围,以适应不同类型的需求
#最大振幅 radarView.setMaxShakeRange(30); #当前值 radarView.updateShakeValue(20);
三、全部代码
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的音量值一般都是有的,实时获取就好了。
以上就是Android实现录音监听动画的示例代码的详细内容,更多关于Android录音监听动画的资料请关注其它相关文章!