利用属性动画实现一个不一样的SplashView
直接上效果
这个效果是在学属性动画时做的一个效果,现在就当复习了,顺便记录下来。
自定义View重写onDraw方法,用属性动画控制旋转和缩放效果。
动画还是比较简单的可以分为三个过程:
state1.六个小球的旋转
state2.小球向中间收缩
state3.展开显示出下面的内容
我们可以想象成这种场景:我们从服务器请求数据时,开始让小球执行旋转动画,当数据请求成功加载完成后执行收缩动画和展开动画显示内容。
这里我们小小用了一下设计模式——策略模式,我们把每个state定义成一个类,让每个类去实现它自己的动画。
首先定一个抽象类,里面定一个drawState方法。
private SplashState mState;
private abstract class SplashState{
public abstract void drawState(Canvas canvas);
}
然后我们有三个状态,定义三个类,分别实现我们的SplashState
重写drawState方法;
旋转状态 ⬇️
public class RotateState extends SplashState{
@Override
public void drawState(Canvas canvas) {
}
}
收缩状态⬇️
public class MergeState extends SplashState{
@Override
public void drawState(Canvas canvas) {
}
}
展开状态⬇️
public class ExpandState extends SplashState{
@Override
public void drawState(Canvas canvas) {
}
}
这样一来每种模式的动画都有它们自己去实现了,在我们的onDraw方法里就不用if else去判断三种模式了,可以这样写:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(mState==null){
mState = new RotateState();
}
mState.drawState(canvas);
}
接下来就挨个分析每种状态了,首先旋转状态:
public class RotateState extends SplashState{
@Override
public void drawState(Canvas canvas) {
drawBackground(canvas);
drawCircles(canvas);
}
}
drawState里就是画背景和画圆
drawBackground画背景
private void drawBackground(Canvas canvas) {
canvas.drawColor(mBackgroundColor);
}
drawCircles画圆
private void drawCircles(Canvas canvas) {
float spaceAngle = (float) (Math.PI * 2 / mCircleColors.length);
for (int i=0;i<mCircleColors.length;i++){
double angle = spaceAngle * i + mRotateAngle;
float cx = (float) (mCenterX + mCurrentRotateRadius * Math.cos(angle));
float cy = (float) (mCenterY + mCurrentRotateRadius * Math.sin(angle));
mCirclePaint.setColor(mCircleColors[i]);
canvas.drawCircle(cx,cy,mCircleRadius,mCirclePaint);
}
}
六个圆按一周平均分布,spaceAngle是每个圆之间间隔角度,mCenterX,mCenterY是中心点的坐标,cx,cy是最终要确定的每个小圆的圆心,很简单的计算得到cx,cy的值,mCircleRadius为小圆的半径,通过canvas的drawCircle方法来依次画圆。mRotateAngle是用来控制小圆旋转用的,用ValueAnimaor使它在0到Math.PI*2之间变化,从而改变cx,cy的坐标,以达到旋转的效果。
现在mCurrentRotateRadius的值是小圆围绕着旋转的大圆的半径。
看一下控制mRotateAngle角度变化的代码,可以在RotateState的构造方法里执行。
public RotateState(){
animator = ValueAnimator.ofFloat(0,(float) Math.PI * 2f);
animator.setDuration(mRoatationDuration);
animator.setInterpolator(new LinearInterpolator());
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mRotateAngle = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
setVisibility(View.VISIBLE);
}
});
animator.start();
}
这里加了一个线线性插值器,如果不加的话,动画旋转一圈后会停一下,然后再旋转,效果不流畅。而且还设置了RepeatCount值是INFINITE,无限循环。因为停止旋转动画并开启后面的收缩和展开动画需要通过外部设置。提供一个给外部调用的接口,停止旋转动画,开启收缩动画。
public void splashDismiss(){
animator.cancel();
if(mState instanceof RotateState){
mState = new MergeState();
}
}
我们在MainActivity中用Handler模拟加载数据,在3秒后调用splashDismiss开启后面的动画。
public class MainActivity extends AppCompatActivity {
private SplashView splash;
private Handler handler = new Handler();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
splash = (SplashView) findViewById(R.id.splash);
handler.postDelayed(new Runnable() {
@Override
public void run() {
splash.splashDismiss();
}
},3000);
}
}
MainActivity的布局的代码很简单,
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
tools:context="com.example.splashview.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/content">
</LinearLayout>
<com.example.splashview.SplashView
android:id="@+id/splash"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
这时就要开始执行收缩动画MergeState了,MergeState的drawState方法。
@Override
public void drawState(Canvas canvas) {
drawBackground(canvas);
drawCircles(canvas);
}
这里跟RotateState的drawState是一样的也调用了drawCircles方法,因为收缩只需要用ValueAnimator改变大圆的半径mCurrentRotateRadius就好了,在MergeState的构造方法里
public MergeState(){
animator = ValueAnimator.ofFloat(0,mRotationRadius);
animator.setDuration(mMergeDuration);
animator.setInterpolator(new OvershootInterpolator(10));
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mCurrentRotateRadius = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mState = new ExpandState();
}
});
animator.reverse();
}
这里的收缩有一个overshoot的效果,先向外弹射一段距离,再收缩回来,用了OvershootInterpolator插值器。在动画结束后要开启展开动画ExpandState。drawState方法
@Override
public void drawState(Canvas canvas) {
drawBackground(canvas);
}
展开动画对背景进行了操作,当然我们肯定要对drawBackground方法进行修改了。
private void drawBackground(Canvas canvas) {
if(mHoleRadius>0){
float mBgPaintStroke = mDiagnoalDist - mHoleRadius;
float radius = mHoleRadius + mBgPaintStroke / 2;
mBackgroundPaint.setStrokeWidth(mBgPaintStroke);
canvas.drawCircle(mCenterX,mCenterY,radius,mBackgroundPaint);
}else {
canvas.drawColor(mBackgroundColor);
}
}
这里很巧妙的利用改变Paint的StrokeWidth的方法实现这种效果,画背景我们也可以当成是画了一个很大的圆,把屏幕都覆盖了,我们把画笔的宽度设置成了屏幕对角线距离的一半,然后来让画笔的宽度满满减小,对应的也就是中间空心圆的半径mHoleRadius慢慢变大,计算出画圆的半径radius,
我们用ValueAnimator来控制mHoleRadius使其慢慢变大,同样也是在ExpandState的构造方法里。
public ExpandState(){
animator = ValueAnimator.ofFloat(0,mDiagnoalDist);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mHoleRadius = (float) animation.getAnimatedValue();
invalidate();
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
setVisibility(View.GONE);
}
});
animator.setDuration(800);
animator.start();
}
除了上面那种通过改变Paint的StrokeWidth来实现效果,还有一种就是通过设置Paint的Xfermode来达到展开效果,用CLEAR模式就可以轻松实现,这时的drawBackground方法修改如下
private void drawBackground(Canvas canvas) {
canvas.drawColor(mBackgroundColor);
if(mHoleRadius>0){
mHolePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
canvas.drawCircle(mCenterX,mCenterY,mHoleRadius,mHolePaint);
mHolePaint.setXfermode(null);
}
}
用xfermode实现看来稍简单点,不用计算,不了解xfermode的可以去了解下,CLEAR模式是最好理解的一种。
最后再贴一下定义的变量和一些初始化的代码
private ValueAnimator animator;
//小圆的半径
private float mCircleRadius = 20;
//大圆的半径 (小圆围绕大圆旋转形成的)
private float mRotationRadius = 100;
//小圆旋转收缩时的半径
private float mCurrentRotateRadius = mRotationRadius;
//小圆的颜色
private int[] mCircleColors;
//小圆围绕旋转的时间
private long mRoatationDuration = 1200;
//收缩的时间
private long mMergeDuration = 400;
//旋转角度
private float mRotateAngle = 0f;
//画小圆用的画笔
private Paint mCirclePaint;
//画背景的画笔
private Paint mBackgroundPaint;
//背景的颜色
private int mBackgroundColor = Color.WHITE;
//中心点的坐标
private float mCenterX;
private float mCenterY;
//对角线的距离的一半
private float mDiagnoalDist;
//展开动画时中间空心圆的半径
private float mHoleRadius = 0f;
//用xfermode CLEAR模式时 画空心圆的画笔
private Paint mHolePaint;
public SplashView(Context context) {
this(context,null);
}
public SplashView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initView(context);
}
private void initView(Context context) {
mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mCirclePaint.setStyle(Paint.Style.FILL_AND_STROKE);
mCircleColors = context.getResources().getIntArray(R.array.splash_circle_colors);
mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBackgroundPaint.setColor(Color.WHITE);
mBackgroundPaint.setStyle(Paint.Style.STROKE);
mHolePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mHolePaint.setStyle(Paint.Style.FILL);
mHolePaint.setColor(mBackgroundColor);
setLayerType(LAYER_TYPE_SOFTWARE,null);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mCenterX = w/2f;
mCenterY = h/2f;
mDiagnoalDist = (float) (Math.sqrt(w*w+h*h) / 2);
}
好了。效果实现了。总体来说不难。
上一篇: VMware虚拟机配置固定IP
下一篇: Unity - 图形辅助线