提到自定义view,大家肯定都知道重点无非是onMeasure()
(测量),onLayout()
(view的排放),onDraw()
(绘制),涉及到与用户交互的,可能还需要额外处理onTouchEvent(MotionEvent event)
(touch操作),这篇文章主要通过一个自定义view的demo,让大家了解Canvas和ValueAnimator。好了,废话不多说,本文demo效果如下:
可以发现上述动画效果,是在Canvas中画了多个不同颜色的圆形图案,通过不断的变化他的半径大小,圆心坐标来实现的。我们可以将动画分成两部分:
- 1.半径变大且圆心向屏幕中心聚拢
- 2.半径缩小且圆心向外扩散
draw之前我们肯定要先获取到当前的宽高值:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth=w;
mHeight=h;
}
至于每个圆的旋转操作其实很简单,我们只要统一对画布做旋转处理就可以达到每个图案都在旋转的动画效果,而并不是对图案直接操作。
看代码大家应该会更好理解一点。
- 旋转画布,旋转角度通过属性动画获得:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < circleCount; i++) {
canvas.rotate(-rotateAngle-i*(360/circleCount),mWidth/2,mHeight/2);
mPaint.setColor(mColors[i]);
canvas.drawCircle(mWidth/2- gapCircle,mHeight/2-gapCircle,radiu,mPaint);
canvas.rotate(rotateAngle+i*(360/circleCount),mWidth/2,mHeight/2);
}
}
细心的同学肯定发现了,我们对Canvas旋转了两次,而且每次旋转度数都相同,只是方向相反,这么做只是为了让Canvas坐标与屏幕永远重合,方便圆的旋转动画。需要注意的是,canvas.rotate
只是旋转了坐标,而不是许多人认为的旋转了整个画布,因此如果我们在rotate之前就draw了某个图案,无论你怎么旋转,图案相对于手机屏幕的位置,都不会改变!只是改变了其在Canvas中的坐标值。
如果还觉得不明白的同学可以看图理解~~
下面我们就来分析动画中圆形图案改变的变化的属性值:半径,圆心坐标,Canvas角度,这些动态的值都可以通过属性动画ValueAnimator.ofInt()
获取得到,直接上第一部分半径变大且圆心向屏幕中心聚拢的代码:
protected void customAnimation() {
Collection<Animator> animList = new ArrayList<>();
ValueAnimator rotateAnimator=ValueAnimator.ofInt(0,360);
rotateAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
rotateAngle= (int) valueAnimator.getAnimatedValue();
//invalidate();
}
});
animList.add(rotateAnimator);
ValueAnimator gapAnimator=ValueAnimator.ofInt(DensityUtil.dip2px(context,60),0);
gapAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
gapCircle= (int) valueAnimator.getAnimatedValue();
}
});
animList.add(gapAnimator);
ValueAnimator radiuAnimator=ValueAnimator.ofInt(radiu,radiu*4);
radiuAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
radiu= (int) valueAnimator.getAnimatedValue();
invalidate();
}
});
animList.add(radiuAnimator);
AnimatorSet animatorSet=new AnimatorSet();
animatorSet.setDuration(duration);
animatorSet.playTogether(animList);
animatorSet.setInterpolator(new LinearInterpolator());
animatorSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
oppositeAnimation();
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
allAnim.add(animatorSet);
animatorSet.start();
}
通过为每个属性动画设置UpdateListener
来动态获取我们需要的属性值,animatorSet.playTogether
则是将上述三个动画同时进行,另外我们还可以为动画设置不同的插值器animatorSet.setInterpolator(new LinearInterpolator());
。在第一部分动画结束时onAnimationEnd(Animator animator)
,我们直接start第二部分的动画,只需改变属性动画中from-to的值就行,代码几乎一样,就不重复贴了。
文末扩展一下自定义view经常用到的invalidate()
和requestLayout();
的区别:
invalidate()
:请求重绘view,既只执行onDraw(Canvas canvas)
,而不另外测量与排放view。
requestLayout()
:只调用onMeasure()
,onLayout()
,不再进行重绘。