版权声明:本文为博主原创文章,未经博主允许不得转载。
本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。
一、效果
这个是国外的一个 App 开场动画,曾获得设计大奖 。
二、分析
看效果是有两个界面,一个是 Splash 小球旋转的加载界面,一个是主界面,很多时候习惯在这边做成两个 Activity 进行跳转。如果条件允许的话,尽量做成两个 View 同时存在一个 Activity 里面,这边采用这种模式。
1.预加载时候显示 SplashView —- 小圆旋转的动画时间是不确定。
2.SplashView 盖在了主界面上面。
3.动画:动画分为三步。小球的旋转动画;小球逃逸和聚合动画(平移);水波纹的扩散动画。
三、搭建
加载的界面,弄成一个自定义控件,这样就可以与主界面放在一个 Activity 里面了。SplashView 一开始就执行小球旋转的动画,当数据加载完的时候,调用 splashDisappear,则开始调用小球的聚合与水波纹扩散的动画。
SplashView:
public class SplashView extends View {
// 整体的背景颜色
private int mSplashBgColor = Color.WHITE;
public SplashView(Context context) {
this(context, null);
}
public SplashView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public SplashView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public SplashView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context);
}
private void init(Context context) {
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(mSplashBgColor);
}
/**
* 数据加载完成之后调用
* 开始后面的动画
*/
public void splashDisappear() {
}
}
使用 ContentView 模拟主界面,虽然这边是继承 AppCompatImageView,只是为了方便,不是说这一定要是个 Image,这是一个 View。
ContentView:
public class ContentView extends android.support.v7.widget.AppCompatImageView {
public ContentView(Context context) {
this(context, null);
}
public ContentView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ContentView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
setImageResource(R.drawable.content);
}
}
布局文件,使用 FrameLayout,让 SplashView 盖在 ContentView 上面。
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.xiaoyue.animatorsplashview.MainActivity">
<com.xiaoyue.animatorsplashview.ContentView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"/>
<com.xiaoyue.animatorsplashview.SplashView
android:id="@+id/splash_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
MainActivity:
public class MainActivity extends AppCompatActivity {
private SplashView splashView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//AppCompatActivity 的设置全屏
supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
splashView = (SplashView) findViewById(R.id.splash_view);
startLoad();
}
Handler handler = new Handler();
private void startLoad() {
//handler 模拟加载数据
handler.postDelayed(new Runnable() {
@Override
public void run() {
//数据加载完毕,进入主界面,调用动画
splashView.splashDisappear();
}
}, 5000);
}
}
这时候运行项目是一个全白的界面,加载界面 SplashView 把主界面 ContentView 盖住了。
四、策略模式
这个开场效果可以分为 3 个动画效果,小球的旋转动画、小球逃逸和聚合动画(平移)和水波纹的扩散动画。每次动画执行的时候都要进行绘制,为了代码优雅,这边采用策略模式进行搭建。
在 SplashView.java 添加几个内部类。
1.动画抽象类
//动画
private ValueAnimator mAnimator;
private SplashState mState = null;
//策略模式:State---三种动画状态
private abstract class SplashState{
//绘制各个状态的界面
public abstract void drawState(Canvas canvas);
//取消动画
public void cancel(){
mAnimator.cancel();
}
}
2.旋转动画
private class RotateState extends SplashState{
public RotateState() {
}
@Override
}
}
3.聚合动画
private class MergingState extends SplashState{
public MergingState() {
}
@Override
public void drawState(Canvas canvas) {
}
}
4.水波纹扩散动画
private class ExpandState extends SplashState{
public ExpandState() {
}
@Override
public void drawState(Canvas canvas) {
}
}
这边动画效果差异不是很大,逻辑也没有很复杂,使用策略模式可能显得有点麻烦。当动画差异较大的时候,策略模式写出来的代码就比较好看一些。
五、旋转动画
1.小球颜色
在初始化的时候获取小球的颜色,这样的话可以自己定义小球个数和颜色,比较灵活。
//小球颜色数组
private int[] mCircleColors;
// 绘制小球的画笔
private Paint mPaint = new Paint();
private void init(Context context) {
mCircleColors = context.getResources().getIntArray(R.array.splash_circle_colors);
//画笔初始化
//消除锯齿
mPaint.setAntiAlias(true);
}
color.xml
<resources>
<color name="splash_bg">#F8F6EC</color>
<color name="orange">#FF9600</color>
<color name="aqua">#02D1AC</color>
<color name="yellow">#FFD200</color>
<color name="blue">#00C6FF</color>
<color name="green">#00E099</color>
<color name="pink">#FF3892</color>
<array name="splash_circle_colors">
<item>@color/blue</item>
<item>@color/green</item>
<item>@color/pink</item>
<item>@color/orange</item>
<item>@color/aqua</item>
<item>@color/yellow</item>
</array>
</resources>
2.onDraw
重写 onDraw 方法,当第一次进来的时候,没有任何动画,直接初始化一个旋转动画,然后调用各个动画的绘制方法 drawState。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mState == null) {
mState = new RotateState();
}
mState.drawState(canvas);
}
3.旋转动画
实现旋转动画,在构造函数中新建新建属性动画,改变旋转的角度,添加属性动画监听,每次执行动画时候进行重新绘制。
// 大圆和小球旋转一圈的时间
private final long mRotationDuration = 1200; //ms
//当前圆旋转角度(弧度)
private float mCurrentRotationAngle = 0F;
/**
* 1.旋转动画
* 控制各个小球的坐标---控制小球的角度变化----属性动画 ValueAnimator
*/
private class RotateState extends SplashState {
public RotateState() {
//计算某个时刻当前的角度是多少? 0~2π
mAnimator = ValueAnimator.ofFloat(0f, 2 * (float)Math.PI);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCurrentRotationAngle = (float) animation.getAnimatedValue();
//刷新,重新绘制
invalidate();
}
});
//动画执行时间设置为 1200ms,
mAnimator.setDuration(mRotationDuration);
//设置无限循环
mAnimator.setRepeatCount(ValueAnimator.INFINITE);
//启动动画
mAnimator.start();
}
@Override
public void drawState(Canvas canvas) {
drawBackground(canvas);
drawCircles(canvas);
}
}
4.绘制背景
直接绘制白色原先是在 onDraw 方法中,移到这边。
/**
* 绘制背景(白色区域)
* @param canvas
*/
private void drawBackground(Canvas canvas) {
canvas.drawColor(mSplashBgColor);
}
5.屏幕中心
很明显,大圆是在屏幕中心(更准确应该是说是在该 View 中心),所以需要计算屏幕中心的坐标。
// 屏幕正中心点坐标
private float mCenterX;
private float mCenterY;
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mCenterX = w/2f;
mCenterY = h/2f;
}
6.绘制小球
根据小球个数先算出相邻小球间的间隔角度,从而可以计算每个小球距离 0 角度的间隔角度,再加上旋转的角度,则为小球当前的角度。
// 大圆(里面包含很多小球的)的半径
private final float mRotationRadius = 90;
// 每一个小球的半径
private final float mCircleRadius = 18;
/**
* 绘制小球
* @param canvas
*/
private void drawCircles(Canvas canvas) {
//每个小球之间的间隔角度 = 2π/小圆的个数
float rotationAngle = (float) (2 * Math.PI / mCircleColors.length);
for (int i=0; i < mCircleColors.length; i++){
/**
* x = r * cos(a) +centerX
* y= r * sin(a) + centerY
* 每个小球 i * 间隔角度 + 旋转的角度 = 当前小球的真正角度
*/
double angle = i*rotationAngle + mCurrentRotationAngle;
float cx = (float) (mRotationRadius * Math.cos(angle) + mCenterX);
float cy = (float) (mRotationRadius * Math.sin(angle) + mCenterY);
mPaint.setColor(mCircleColors[i]);
canvas.drawCircle(cx, cy ,mCircleRadius, mPaint);
}
}
7.效果
这时候运行代码:
小球开始不停的绕着大圈旋转,但是认真看会发现,旋转动画旋转一回后,会卡顿一下。这是由于我们旋转动画设置为重复执行,动画执行过程中,默认是线性的,但是,动画的衔接不会是线性的。这时候需要为动画添加线性插值器,这样可以使衔接过程也是线性。
mAnimator.setInterpolator(new LinearInterpolator());
六、聚合动画
1.切换动画
在开头搭建的时候已经预留了切换的方法,在数据加载完毕之后(用 Handler 延迟 5s 模拟)调用切换动画。
直接把要执行的动画 mState 指向聚合动画即可。
/**
* 数据加载完成之后执行的动画
* 小球聚合和水波纹扩散
*/
public void splashDisappear() {
if (mState != null && mState instanceof RotateState) {
mState.cancel();
post(new Runnable() {
@Override
public void run() {
mState = new MergingState();
}
});
}
}
2.聚合动画
在旋转动画的时候,计算小球的坐标的时候,是根据大圆的半径进行计算,这边直接改变大圆的半径,从而达到聚合的效果。为了保持初始值,从新定义一个当前大圆的半径,用这个来进行计算,初始值为大圆默认半径。(在绘制小球 drawCircles 方法里也要同步改过来)
小球聚合前有一个向外扩散的效果,这里使用一个回荡秋千插值器 AnticipateInterpolator。
//当前大圆的半径
private float mCurrentRotationRadius = mRotationRadius;
/**
* 2.聚合动画
* 要素:大圆的半径不断地变大--变小----》小球的坐标
*/
private class MergingState extends SplashState {
public MergingState() {
mAnimator = ValueAnimator.ofFloat(mRotationRadius, 0f);
mAnimator.setDuration(mRotationDuration);
//插值器实现先扩散再聚合效果
mAnimator.setInterpolator(new AnticipateInterpolator(6f));
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCurrentRotationRadius = (float) animation.getAnimatedValue();
invalidate();
}
});
mAnimator.start();
}
@Override
public void drawState(Canvas canvas) {
drawBackground(canvas);
drawCircles(canvas);
}
}
3.效果
七、扩散动画
1.切换动画
扩散动画是在聚合动画结束之后才开始执行,所以为聚合动画添加一个监听,当执行完毕之后,切换动画为扩散动画。
mAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mState = new ExpandState();
}
});
2.分析
扩散动画是由中间一个小圆慢慢扩大,可以实现的方法很多,这边介绍个比较好的想法。
图中米黄色表示手机屏幕,中间彩色小圆为主界面,这是后加载界面是最外面一个大圆,被挖去中间一个小圆(圆环)。在这边把两个黑色圆圈组成的圆环理解为边沿很粗很粗的圆,则实际圆是圆环中间的那个圆(即黄色的圆)。 通过不断的缩小圆环的边沿,保持圆环的最大一个圆不变,从而使中间的主界面不断扩大。
3.屏幕对角线
需要计算出屏幕对角线一半的长度,即圆环的大圆半径。在 onSizeChanged 时候通过勾股定理计算出来。
//屏幕对角线一半
private float mDiagonalDist;
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mCenterX = w/2f;
mCenterY = h/2f;
mDiagonalDist = (float) Math.sqrt((w*w+h*h))/2f;//勾股定律
}
4.扩散动画
扩散动画变化的是中心要显示的主界面的圆半径大小,为了不显得变换的很突兀,扩散动画的初始显示大小设置为小球的大小。
//空心圆初始半径
private float mHoleRadius = 0F;
/**
* 3.水波纹扩散动画
* 画一个空心圆----画一个圆,让它的画笔的粗细变成很大---不断地减小画笔的粗细。
* 空心圆变化的范围:小球半径 ~ 对角线/2
*/
private class ExpandState extends SplashState {
public ExpandState() {
//花1200ms,计算某个时刻当前的空心圆的半径是多少
mAnimator = ValueAnimator.ofFloat(mCircleRadius, mDiagonalDist);
mAnimator.setDuration(mRotationDuration);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
//当前的空心圆的半径是多少?
mHoleRadius = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
mAnimator.start();
}
@Override
public void drawState(Canvas canvas) {
drawBackground(canvas);
}
}
5.绘制背景
在扩散效果中,背景不能直接画为全白了,要画一个边沿很粗的圆。为绘制背景添加一个新的画笔,在 init 中进行初始化。
// 绘制背景的画笔
private Paint mPaintBackground = new Paint();
private void init(Context context) {
mCircleColors = context.getResources().getIntArray(R.array.splash_circle_colors);
//每个小球之间的间隔角度 = 2π/小圆的个数
rotationAngle = (float) (2 * Math.PI / mCircleColors.length);
//画笔初始化
//消除锯齿
mPaint.setAntiAlias(true);
mPaintBackground.setAntiAlias(true);
//设置样式---边框样式--描边
mPaintBackground.setStyle(Paint.Style.STROKE);
mPaintBackground.setColor(mSplashBgColor);
}
/**
* 绘制背景(白色区域)
* @param canvas
*/
private void drawBackground(Canvas canvas) {
if(mHoleRadius>0f){
//得到画笔的宽度 = 对角线/2 - 空心圆的半径
float strokeWidth = mDiagonalDist - mHoleRadius;
mPaintBackground.setStrokeWidth(strokeWidth);
//画圆的半径 = 空心圆的半径 + 画笔的宽度/2
float radius = mHoleRadius + strokeWidth/2;
canvas.drawCircle(mCenterX,mCenterY,radius,mPaintBackground);
}else {
canvas.drawColor(mSplashBgColor);
}
}
6.效果
到这里就结束了。
可以把动画执行时间,小球半径等属性提取出来作为自定义属性。