重学 Android 自定义 View 系列(七):仿58同城加载动画

前言

本文将实现早期58同城一个带有弹跳效果的加载动画,且结合图形变换(圆形变正方形、正方形变三角形等)实现一种动态、富有表现力的加载效果。

最终效果如下:

1. 效果分析


  • 形状切换: 显示一个不断变化的形状,例如圆形、正方形、三角形之间的变换。
  • 弹跳效果: 加载图标会有上下弹跳的动画,增加动感。
  • 阴影效果: 动画中随着形状变化,添加一个阴影效果,随着形状变化而收缩或放大。

效果展示:

  1. 初始状态: 显示一个圆形图标,伴随初始弹跳。
  2. 动画过程: 随着动画的进行,圆形变为正方形,再变为三角形,每次形状变换后都会触发一次上下弹跳。
  3. 动画结束: 当动画完成时,图标将继续上下弹跳,直至用户操作或停止。

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.drawCirclecanvas.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 去管理ShapeViewShadowView了,接下来就是加上各种动画效果:

下落动画:

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() 方法。在这种模式下,前一个动画结束后,下一个动画才会开始。

动画通过控制 ShapeViewtranslationY 属性实现,上升 和 下落动画,通过控制 ShadowViewscaleX 控制 X轴 的缩放效果。

接着是在上升的过程中 对图形进行不同角度的旋转,通过 mShapeView.getCurrentShapeType() 可以拿到当前绘制的图形,然后对其 rotation属性进行旋转操作。

这样所有效果就大功告成了!可以在其他布局中 直接使用 LoadingView,展示效果了。

4. 最后


一个很有意思的自定义View ,在这篇文章中你可以学到到 canvas.drawPath路径使用,安卓动画组合使用等知识。再会!

源码已上传Github:DiyView

另外给喜欢记笔记的同学安利一款好用的云笔记软件,对比大部分国内的这个算还不错的,免费好用:wolai

相关推荐
TroubleMaker22 分钟前
OkHttp源码学习之retryOnConnectionFailure属性
android·java·okhttp
叶羽西2 小时前
Android Studio IDE环境配置
android·ide·android studio
发飙的蜗牛'3 小时前
23种设计模式
android·java·设计模式
花追雨12 小时前
Android -- 双屏异显之方法一
android·双屏异显
小趴菜822712 小时前
安卓 自定义矢量图片控件 - 支持属性修改矢量图路径颜色
android
氤氲息12 小时前
Android v4和v7冲突
android
KdanMin12 小时前
高通Android 12 Launcher应用名称太长显示完整
android
chenjk412 小时前
Android不可擦除分区写文件恢复出厂设置,无法读写问题
android
袁震12 小时前
Android-Glide缓存机制
android·缓存·移动开发·glide
工程师老罗12 小时前
Android笔试面试题AI答之SQLite(2)
android·jvm·sqlite