前言
本文将实现早期58同城一个带有弹跳效果的加载动画,且结合图形变换(圆形变正方形、正方形变三角形等)实现一种动态、富有表现力的加载效果。
最终效果如下:
1. 效果分析
- 形状切换: 显示一个不断变化的形状,例如圆形、正方形、三角形之间的变换。
- 弹跳效果: 加载图标会有上下弹跳的动画,增加动感。
- 阴影效果: 动画中随着形状变化,添加一个阴影效果,随着形状变化而收缩或放大。
效果展示:
- 初始状态: 显示一个圆形图标,伴随初始弹跳。
- 动画过程: 随着动画的进行,圆形变为正方形,再变为三角形,每次形状变换后都会触发一次上下弹跳。
- 动画结束: 当动画完成时,图标将继续上下弹跳,直至用户操作或停止。
2. 结构分析
由上面GIF图可知,加载动画组合布局分为 上 中 下 三部分:分别是上面一个ShapeView 负责切换不同形状,中间一个View 用于模拟阴影效果,底部是加载文字,至于所有动效全部由各种动画实现。
为了方便控制上中下三部分内容,需要自定义一个LoadingView ,用于包含这三部分View ,其内部实现各个View的动画效果。
3. 技术实现
3.1 ShapeView
用于绘制三种图形:圆形、矩形、三角形。且具有形状切换功能,比如 先绘制圆形,圆形展示完绘制矩形,矩形展示完绘制三角形,三角形展示完再绘制圆,首尾相连。
首先我们要确保ShapeView 是个正方形,才能保证绘制出的三种图形都比较规则。
Java
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//确保是正方形
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int size = Math.min(width, height);
setMeasuredDimension(size, size);
}
接着,要定义三种图形的枚举类,方便切换使用:
bash
public void exchangeShape(){
switch (currentShapeType){
case CIRCLE:
currentShapeType = ShapeType.SQUARE;
break;
case SQUARE:
currentShapeType = ShapeType.TRIANGLE;
break;
case TRIANGLE:
currentShapeType = ShapeType.CIRCLE;
break;
}
invalidate();
}
public ShapeType getCurrentShapeType() {
return currentShapeType;
}
public enum ShapeType{
CIRCLE, //圆形
SQUARE, //正方形
TRIANGLE //三角形
}
然后,在onDraw中绘制三种图形,其中圆和矩形最简单,有现成的方法canvas.drawCircle
和 canvas.drawRect
在此不做介绍了,三角形需要介绍一下,因为三角形要使用Path
进行绘制,且为了美观要绘制一个等边三角形:
要绘制三角形,我们首先要知道三角形的三个顶点的坐标,才能进行绘制,由于ShapeView
我们已经保证是正方形了,所以顶点坐标就是(getWidth() / 2,0)
,左边顶点坐标不能是(0,getHeight())
因为这样绘制的三角形就是等边三角形了,影响美观,而要实现等边三角形的话就要重新计算Y轴坐标,由上面草图可知,大三角形分成开两个小三角形,我们知道底边和斜边是1/2的关系,由三角定理可知,一份二份根号三份,所以长的直角边就是根号三的getWidth()
,此时我们就拿到了三个顶点坐标了。
bash
if (mPath == null) {
mPath = new Path();
mPath.moveTo(getWidth() / 2,0);
mPath.lineTo((float) 0, (float) ((getHeight() / 2) * Math.sqrt(3)));
mPath.lineTo(getWidth(),(float) ((getHeight() / 2) * Math.sqrt(3)));
mPath.close();
}
canvas.drawPath(mPath,mPaint);
Path
相关函数介绍:
moveTo(float x, float y)
:此方法将"画笔"移动到指定的坐标 (x, y),但是不绘制任何东西。它通常用于开始绘制路径时指定初始位置。
lineTo(float x, float y)
:此方法从当前路径点绘制一条直线到指定的 (x, y) 坐标。
close()
:此方法会连接路径的最后一个点和第一个点,形成一个封闭的路径。对于一个三角形,调用 close()
会把最后一个点与起始点连接起来,闭合三角形。
3.2 阴影效果
一个简单的椭圆背景,后期对其进行缩放实现动效。
bash
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#FF000000"/>
</shape>
3.3 LoadingView(组合)
LoadingView 继承自 LinearLayout
,因为我们要把三部分View都放进该布局中,在layout
中 写出三部分组合的xml
文件:layout_loading_view
xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:background="@color/white"
android:orientation="vertical">
<com.xaye.diyview.view.ShapeView
android:id="@+id/shape_view"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_marginBottom="82dp"/>
<View
android:id="@+id/shadow_view"
android:layout_width="25dp"
android:layout_height="3dp"
android:background="@drawable/shadow_bg"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:text="玩命加载中..."/>
</LinearLayout>
将布局添加进LoadingView:
bash
private void initLayout() {
// 添加到该View
inflate(getContext(), R.layout.layout_loading_view, this);
mShapeView = findViewById(R.id.shape_view);
mShadowView = findViewById(R.id.shadow_view);
startFalling();
}
此处的关键技术点是inflate
,现在你可能不明白其原理,但只需要记住用它就可以把布局添加进来就行了,后面再慢慢分析其原理。
这样我们就可以在LoadingView 去管理ShapeView
和 ShadowView
了,接下来就是加上各种动画效果:
下落动画:
java
private void startFalling() {
// 下落位移动画
ObjectAnimator animator = ObjectAnimator.ofFloat(mShapeView, "translationY", 0, mTranslationY);
animator.setDuration(ANIMATOR_DURATION);
//配合中间阴影缩小
ObjectAnimator scaleAnimator = ObjectAnimator.ofFloat(mShadowView, "scaleX", 1, 0.3f);
scaleAnimator.setDuration(ANIMATOR_DURATION);
// 动画集合
AnimatorSet animatorSet = new AnimatorSet();
// 加速
animatorSet.setInterpolator(new AccelerateInterpolator());
animatorSet.playTogether(animator, scaleAnimator);
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// 改变形状
mShapeView.exchangeShape();
// 下落完动画,开始上升动画
startRising();
}
});
// 如果已有动画在执行,取消之前的动画
if (mCurrentAnimatorSet != null && mCurrentAnimatorSet.isRunning()) {
mCurrentAnimatorSet.cancel();
}
mCurrentAnimatorSet = animatorSet;
animatorSet.start();
}
上升动画:
java
private void startRising() {
ObjectAnimator animator = ObjectAnimator.ofFloat(mShapeView, "translationY", mTranslationY, 0);
animator.setDuration(ANIMATOR_DURATION);
ObjectAnimator scaleAnimator = ObjectAnimator.ofFloat(mShadowView, "scaleX", 0.3f, 1);
scaleAnimator.setDuration(ANIMATOR_DURATION);
AnimatorSet animatorSet = new AnimatorSet();
// 减速
animatorSet.setInterpolator(new DecelerateInterpolator());
animatorSet.playTogether(animator, scaleAnimator);
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
startFalling();
}
@Override
public void onAnimationStart(Animator animation) {
//开始旋转
startRotationAnimator();
}
});
// 如果已有动画在执行,取消之前的动画
if (mCurrentAnimatorSet != null && mCurrentAnimatorSet.isRunning()) {
mCurrentAnimatorSet.cancel();
}
mCurrentAnimatorSet = animatorSet;
animatorSet.start();
}
旋转动画:
java
private void startRotationAnimator() {
ObjectAnimator rotationAnimator = null;
switch (mShapeView.getCurrentShapeType()) {
case CIRCLE:
case SQUARE:
rotationAnimator = ObjectAnimator.ofFloat(mShapeView, "rotation", 0, 180);
break;
case TRIANGLE:
rotationAnimator = ObjectAnimator.ofFloat(mShapeView, "rotation", 0, -60);
break;
}
rotationAnimator.setDuration(ANIMATOR_DURATION);
rotationAnimator.setInterpolator(new DecelerateInterpolator());
rotationAnimator.start();
}
动画分析:
上升和下落是个逆过程,主要差异点是差值器不同,为了显得更真实,上升过程使用了减速差值器DecelerateInterpolator
下落过程使用了加速差值器AccelerateInterpolator
。
其中AnimatorSet
可以将多个动画组合在一起,同时执行。使用 playTogether()
方法,将多个动画传入并同时启动。这在需要同步进行的动画场景中非常有用。
补充:除了同步执行,AnimatorSet
还支持将多个动画按顺序执行,使用 playSequentially()
方法。在这种模式下,前一个动画结束后,下一个动画才会开始。
动画通过控制 ShapeView 的 translationY
属性实现,上升 和 下落动画,通过控制 ShadowView 的 scaleX
控制 X轴 的缩放效果。
接着是在上升的过程中 对图形进行不同角度的旋转,通过 mShapeView.getCurrentShapeType()
可以拿到当前绘制的图形,然后对其 rotation
属性进行旋转操作。
这样所有效果就大功告成了!可以在其他布局中 直接使用 LoadingView,展示效果了。
4. 最后
一个很有意思的自定义View ,在这篇文章中你可以学到到 canvas.drawPath
路径使用,安卓动画组合使用等知识。再会!
源码已上传Github:DiyView
另外给喜欢记笔记的同学安利一款好用的云笔记软件,对比大部分国内的这个算还不错的,免费好用:wolai