先放图:
第一个动画是常见的播放器在听音识曲时的水波纹动画,第二个则是很多加载资源或请求网络时会用到的加载加载动画。
他们的实现都非常简单。
第一个水波纹动画
其实说白了就是画几个圈。然后让他们延时循环执行一个动画集合。这个动画集合包括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值怎么取呢?
将动画放慢来看看:
首先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倍,这样他们才能同时到达“终点”。