重学 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

相关推荐
老码沉思录2 小时前
Android开发实战班 - 数据持久化 - Room 数据库应用
android·jvm·数据库
Dnelic-3 小时前
【笔记】Android Gradle Plugin配置文件相关说明-libs.versions.toml
android·ide·笔记·gradle·plugin·版本控制
建群新人小猿3 小时前
积分商品编辑规格里积分未会显问题处理
android·java·开发语言·前端·php
老码沉思录3 小时前
Android开发实战班 - 应用架构 - 单向数据流(Unidirectional Data Flow, UDF)
android·架构
NotesChapter6 小时前
android viewpager2 嵌套 recyclerview 手势冲突
android·gitee
少说多做3436 小时前
Android 网络请求(二)OKHttp网络通信
android·网络·okhttp
云璃丶夢紡6 小时前
用源码编译虚幻引擎,并打包到安卓平台
android·游戏引擎·虚幻
找藉口是失败者的习惯6 小时前
蓝牙 GATT 协议及其在 Android 的实现
android
guoruijun_2012_46 小时前
php 与 thinkphp 13 张 表 关联 查询,a.pry_key=b.pry_key and c.pry_key= b.pry_key 代码示例
android·c语言·php