您的位置:首页 > 其它

利用属性动画实现一个不一样的SplashView

2017-07-06 15:56 369 查看
直接上效果



这个效果是在学属性动画时做的一个效果,现在就当复习了,顺便记录下来。

自定义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) {

}
}
4000
>

展开状态⬇️

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实现看来
a975
稍简单点,不用计算,不了解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);
}


好了。效果实现了。总体来说不难。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: