先放图:

android 录音效果 波浪 录音波纹效果_ide

第一个动画是常见的播放器在听音识曲时的水波纹动画,第二个则是很多加载资源或请求网络时会用到的加载加载动画。

他们的实现都非常简单。

第一个水波纹动画

其实说白了就是画几个圈。然后让他们延时循环执行一个动画集合。这个动画集合包括3个动画:scaleX动画(水平方向放大)、scaleY动画(竖直方向放大)、alpha动画(透明度动画)

自定义一个展示动画的layout:

public class RippleAnimationView extends RelativeLayout {
	...
}

然后在构造方法中调用init方法:

private void init(Context context, AttributeSet attributeSet) {
        UIUtils.getInstance(context);
        mViewList = new ArrayList<>();
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        TypedArray array = context.obtainStyledAttributes(attributeSet, R.styleable.RippleAnimationView);
        mAnim_Color = array.getColor(R.styleable.RippleAnimationView_ripple_anim_color, ContextCompat.getColor(context, R.color.rippleColor));
        mAnim_Type = array.getInt(R.styleable.RippleAnimationView_ripple_anim_type, 0);
        if (mAnim_Type == 0) {
            mPaint.setStyle(Paint.Style.FILL);
        } else if (mAnim_Type == 1) {
            mPaint.setStyle(Paint.Style.STROKE);
        }
        mRadius = array.getInt(R.styleable.RippleAnimationView_radius, DEFAULT_RADIUS);
        mStrokeWidth = array.getInt(R.styleable.RippleAnimationView_strokeWidth, DEFAULT_STROKE_WIDTH);
        array.recycle();
        mPaint.setStrokeWidth(UIUtils.getInstance().getWidth(mStrokeWidth));
        mPaint.setColor(mAnim_Color);

        LayoutParams params = new LayoutParams(UIUtils.getInstance().getWidth(mRadius + mStrokeWidth),
                UIUtils.getInstance().getWidth(mRadius + mStrokeWidth));
        params.addRule(CENTER_IN_PARENT, TRUE);

        int singleDelay = RIPPLE_DURATION / RIPPLE_COUNT;   //单个水波纹执行时间
        List<Animator> list = new ArrayList<>();

        for (int i = 0; i < RIPPLE_COUNT; i++) {   //产生4条水波纹,这个可以自行设置数量
            RippleCircleView rippleCircleView = new RippleCircleView(this);
            addView(rippleCircleView,params);
            mViewList.add(rippleCircleView);

            final ObjectAnimator scaleXAnimator = ObjectAnimator.ofFloat(rippleCircleView, View.SCALE_X,1f,maxScale);
            scaleXAnimator.setRepeatCount(ValueAnimator.INFINITE);
            scaleXAnimator.setRepeatMode(ValueAnimator.RESTART);
            scaleXAnimator.setDuration(RIPPLE_DURATION);
            scaleXAnimator.setStartDelay(i * singleDelay);
            list.add(scaleXAnimator);

            final ObjectAnimator scaleYAnimator = ObjectAnimator.ofFloat(rippleCircleView,View.SCALE_Y,1f,maxScale);
            scaleYAnimator.setRepeatMode(ValueAnimator.RESTART);
            scaleYAnimator.setRepeatCount(ValueAnimator.INFINITE);
            scaleYAnimator.setStartDelay(i * singleDelay);
            scaleYAnimator.setDuration(RIPPLE_DURATION);
            list.add(scaleYAnimator);

            final ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(rippleCircleView,View.ALPHA,1.0f,0);
            alphaAnimator.setRepeatCount(ValueAnimator.INFINITE);
            alphaAnimator.setRepeatMode(ValueAnimator.RESTART);
            alphaAnimator.setStartDelay(i * singleDelay);
            alphaAnimator.setDuration(RIPPLE_DURATION);
            list.add(alphaAnimator);
        }

        mAnimatorSet = new AnimatorSet();

        mAnimatorSet.playTogether(list);
        mAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());   //先加速后减速插值器
    }

步骤可分解为:

1、从xml文档中获取设置的一些属性,如radius,strokeWidth值等等,没有设置的话就给默认值。

2、初始化几个水波纹View(这个我设置4个),然后将他们设置为INVISIBLE后通过addView添加到自定义ViewGroup中,至于水波纹是什么,就像前面说的一样很简单,真的只是画个圆而已:

public class RippleCircleView extends View {
		。。。

	    @Override
	    protected void onDraw(Canvas canvas) {
	        super.onDraw(canvas);
	        int radius = Math.min(getWidth(),getHeight()) / 2;
	        canvas.drawCircle(radius,radius,radius - mStrokeWidth,mPaint);
	    }
}

3、之后编写3个动画,分别是scaleX,scaleY和alpha,这样的话随着动画的执行,这个初始的水波纹View(其实就是个圈),就会越来越大,然后透明度越来越低。如何让他们一起执行呢?借用一个AnimatorSet类,通过这个类可以一次把3个动画添加在一起,然后调用playTogether就可以一起执行动画了。

最后通过外部事件(如点击事件),启动动画,完事。

imageView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(rippleAnimationView.isRunning()){
                    Log.d(TAG, "onClick: "+"stop");
                    rippleAnimationView.stopAnimation();
                }else{
                    Log.d(TAG, "onClick: "+"start");
                    rippleAnimationView.startAnimation();
                }
            }
        });

第二个动画是加载时的等待动画

这个动画也很简单,只需要借助PathMeasure类编可以简单的完成。

首先初始化一个Path类,通过它的名字也知道它是一个路径类,然后调用它的方法addCircle添加一个圆

mPath.addCircle(300f,300f,mRadius,Path.Direction.CW);    //第三个参数表示逆时针 or 顺时针

这是如果通过canvas来绘制它,那么和通过canvas直接绘制一个圆是看起来是没有区别的。

mPath.addCircle(300f,300f,mRadius,Path.Direction.CW);
canvas.drawPath(mPath,mPaint);

canvas.drawCircle(300,300,mRadius,mPaint);

(以上2种绘圆方式从效果上看没有什么区别。)

PathMeasure首先要通过setPath方法设置路径
PathMeasure有个方法getSegment可以截取当前所设置Path的部分路径。

它所传参数如下:

mPathMeasure.getSegment(float startD,float stopD,Path dst,boolean startWithMoveTo)

第一个参数为从开始点,第二个参数为结束点。一个开始一个结束刚好能获取一个路径上的某一段距离。第三个为绘制后的输出Path,简单说就是new 一个Path对象然后丢进去,它就会帮你设值。第四个参数有点难结束,看看官方文档怎么 说:Begin the segment with a moveTo if startWithMoveTo is true。

简单来说就是会从Path所在的起始点开始截取,如果为false的话,然后会从0,0开始。一般为true。其实这个参数解释起来不懂的话很简单,设置为false看看什么效果,设置为true后又是什么效果,自己试试也没什么损失,还能印象深刻。

回到View中来,同样的,在构造方法中,调用init方法:

private void init(Context context,AttributeSet attributeSet){
		。。。。
		。。。。
        mPath.addCircle(300f,300f,mRadius,Path.Direction.CW);
        mPathMeasure = new PathMeasure(mPath,true);
        ValueAnimator animator = ValueAnimator.ofFloat(0,1f);
        animator.setDuration(2000);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mAnimateValue = (float) animation.getAnimatedValue();
                invalidate();
            }
        });
        animator.start();
    }

前面都是些初始化对象以及获得xml文件中的属性方法就不贴了。说是这个ValueAnimator动画有什么用。其实它不是一个动画,但是通过配合invalidateI()就能产生动画。这个动画开启后,会在2000ms内(可以自己设定),从0到1逐渐增加。(比如:0.12、0.21。。。0.94、1这样子增加)
而在addUpdateListener就可以获取到这个值。
获得这个值后通过调用invalidate会再次调用onDraw方法。

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mDst.reset();
        mSegmentLength = mAnimateValue * mPathMeasure.getLength();
        float halfSegment = mPathMeasure.getLength() / 2;
        float start = 0;
        if(mSegmentLength > halfSegment){
            start = (mSegmentLength - halfSegment) * 2;
        }
        mPathMeasure.getSegment(start,mSegmentLength,mDst,true);
        canvas.drawPath(mDst,mPaint);
    }

首先先将mDst这个Path重置了,因为onDraw方法会被调用很多很多次。
然后通过PathMeasure的getSegment方法来取片段了,这个start值和stop值怎么取呢?

将动画放慢来看看:

android 录音效果 波浪 录音波纹效果_初始化_02

首先stop值很简单,stop值直接设置为mAnimateValue × Path长度。其中这个mAnimateValue ∈ [0 - 1]。

可以发现这个start值,在stop值< 0.5 * Path总长度的时候,它一直是0,当stop值 > 0.5 * Path总长度的时候,它开始增加了,并且它会和stop值同时到达终点(即起始点)。stop值从一半的位置走到剩下半程和start值从0跑完全程的时间是一样的!也就是start的“速度”应该是stop值的刚好2倍!因此就有了下面的代码:

if(mSegmentLength > halfSegment){
            start = (mSegmentLength - halfSegment) * 2;
        }

其中mSegmentLength > halfSegment表示的意思是:一半的距离之后,stop接下里的设值。
而start的值应为stop值的2倍,这样他们才能同时到达“终点”。