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

embedded/2024/11/23 12:59:05/

前言

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

最终效果如下:
在这里插入图片描述

1. 效果分析


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

效果展示:

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

2. 结构分析


由上面GIF图可知,加载动画组合布局分为 上 中 下 三部分:分别是上面一个ShapeView 负责切换不同形状,中间一个View 用于模拟阴影效果,底部是加载文字,至于所有动效全部由各种动画实现。

为了方便控制上中下三部分内容,需要自定义一个LoadingView,用于包含这三部分View,其内部实现各个View的动画效果。

3. 技术实现


3.1 ShapeView

用于绘制三种图形:圆形、矩形、三角形。且具有形状切换功能,比如 先绘制圆形,圆形展示完绘制矩形,矩形展示完绘制三角形,三角形展示完再绘制圆,首尾相连。

首先我们要确保ShapeView 是个正方形,才能保证绘制出的三种图形都比较规则。

   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);}

接着,要定义三种图形的枚举类,方便切换使用:

    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(),此时我们就拿到了三个顶点坐标了。

  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 阴影效果

一个简单的椭圆背景,后期对其进行缩放实现动效。

<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

<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.ShapeViewandroid:id="@+id/shape_view"android:layout_width="25dp"android:layout_height="25dp"android:layout_marginBottom="82dp"/><Viewandroid:id="@+id/shadow_view"android:layout_width="25dp"android:layout_height="3dp"android:background="@drawable/shadow_bg"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="5dp"android:text="玩命加载中..."/></LinearLayout>

将布局添加进LoadingView

   private void initLayout() {// 添加到该Viewinflate(getContext(), R.layout.layout_loading_view, this);mShapeView = findViewById(R.id.shape_view);mShadowView = findViewById(R.id.shadow_view);startFalling();}

此处的关键技术点是inflate,现在你可能不明白其原理,但只需要记住用它就可以把布局添加进来就行了,后面再慢慢分析其原理。

这样我们就可以在LoadingView去管理ShapeViewShadowView了,接下来就是加上各种动画效果:

下落动画:

    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() {@Overridepublic void onAnimationEnd(Animator animation) {// 改变形状mShapeView.exchangeShape();// 下落完动画,开始上升动画startRising();}});// 如果已有动画在执行,取消之前的动画if (mCurrentAnimatorSet != null && mCurrentAnimatorSet.isRunning()) {mCurrentAnimatorSet.cancel();}mCurrentAnimatorSet = animatorSet;animatorSet.start();}

上升动画:

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() {@Overridepublic void onAnimationEnd(Animator animation) {startFalling();}@Overridepublic void onAnimationStart(Animator animation) {//开始旋转startRotationAnimator();}});// 如果已有动画在执行,取消之前的动画if (mCurrentAnimatorSet != null && mCurrentAnimatorSet.isRunning()) {mCurrentAnimatorSet.cancel();}mCurrentAnimatorSet = animatorSet;animatorSet.start();}

旋转动画:

    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


http://www.ppmy.cn/embedded/139852.html

相关文章

机器学习基础07

目录 1.逻辑回归 1.1原理 1.2API 2.K-Means 2.1算法过程 2.2API 3.SVM&#xff08;支持向量机&#xff09; 3.1算法原理​ 3.2API 1.逻辑回归 逻辑回归(Logistic Regression)是机器学习中的一种分类模型&#xff0c;逻辑回归是一种分类算法。 1.1原理 逻辑回归的输…

uniapp H5上传图片前压缩

问题&#xff1a;需要在上传图片前压缩图片&#xff0c;但是uni.compressImage&#xff08;&#xff09;不支持h5 解决方法&#xff1a;使用canvas降低图片质量 核心代码&#xff1a; // 创建一个 img 元素来加载图片const img new Image()img.src imagePath// 创建 canvas…

自动驾驶之激光雷达

这里写目录标题 1 什么是激光雷达2 激光雷达的关键参数3 激光雷达种类4 自动驾驶感知传感器5 激光雷达感知框架5.1 pointcloud_preprocess5.2 pointcloud_map_based_roi5.3 pointcloud_ground_detection5.4 lidar_detection5.5 lidar_detection_filter5.6 lidar_tracking 1 什么…

封装实现通用的 `forEach` 函数:深入JavaScript的迭代机制与细节优化

封装实现通用的 forEach 函数&#xff1a;深入JavaScript的迭代机制与细节优化 在JavaScript中&#xff0c;forEach 方法是数组对象上一个非常实用的迭代方法&#xff0c;它允许我们遍历数组中的每一个元素&#xff0c;并对每个元素执行指定的回调函数。虽然JavaScript已经内置…

C# 中Timer的三种用法

在 C# 中&#xff0c;Timer 类可以用于在不同情况下定时执行代码。常见的 Timer 类有三种主要用法&#xff0c;分别由不同的命名空间提供&#xff1a; System.Timers.Timer System.Threading.Timer System.Windows.Forms.Timer&#xff08;主要用于 Windows 窗体应用程序&#…

JavaScript数据类型判断

在 JavaScript 中&#xff0c;可以通过多种方式来判断数据类型&#xff0c;以下是常用的几种方法&#xff1a; 1. typeof 操作符 typeof 用于判断基本数据类型和部分对象类型。 console.log(typeof 123); // "number" console.log(typeof "hello"); // &…

android 动画原理分析

一 android 动画分为app内的view动画和系统动画 基本原理都是监听Choreographer的doframe回调 二 app端的实现是主要通过AnimationUtils来实现具体属性的变化通过invilate来驱动 wms来进行更新。这个流程是在app进程完成 这里不是我分析的重点 直接来看下系统动画里面的本地动…

【C++】拆分详解 - 多态

文章目录 一、概念二、定义和实现1. 多态的构成条件2. 虚函数2.1 虚函数的重写/覆盖2.2 虚函数重写的两个例外 3. override 和 final关键字4. 重载/重写/隐藏的对比5. 例题 三、纯虚函数和抽象类四、多态的原理1. 虚函数表2. 实现原理3. 动态绑定和静态绑定 总结 一、概念 多态…