前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >我们经常用的Loading动画居然还有这种姿势

我们经常用的Loading动画居然还有这种姿势

作者头像
Android技术干货分享
发布于 2019-04-01 08:48:00
发布于 2019-04-01 08:48:00
2.1K00
代码可运行
举报
文章被收录于专栏:Android技术分享Android技术分享
运行总次数:0
代码可运行

背景


Loading动画几乎每个Android App中都有。

一般在需要用户等待的场景,显示一个Loading动画可以让用户知道App正在加载数据,而不是程序卡死,从而给用户较好的使用体验。

同样的道理,当加载的数据为空时显示一个数据为空的视图、在数据加载失败时显示加载失败对应的UI并支持点击重试会比白屏的用户体验更好一些。

加载中、加载失败、空数据的UI风格,一般来说在App内的所有页面中需要保持一致,也就是需要做到全局统一。

1. 传统的做法
  1. 定义一个(或多个)显示不同加载状态的控件或者xml布局文件(例如:LoadingView
  2. 每个页面的布局中都写上这个view
  3. BaseActivity/BaseFragment中封装LoadingView的初始化逻辑,并封装加载状态切换时的UI显示逻辑,暴露给子类以下方法:
    • void showLoading(); //调用此方法显示加载中的动画
    • void showLoadFailed(); //调用此方法显示加载失败界面
    • void showEmpty(); //调用此方法显示空页面
    • void onClickRetry(); //子类中实现,点击重试的回调方法
  4. BaseActivity/BaseFragment的子类中可通过上一步的封装比较方便地使用加载状态显示功能

这种使用方式耦合度太高,每个页面的布局文件中都需要添加LoadingView,使用起来不方便而且维护成本较高,一旦UI设计师需要更改布局,修改起来成本较高。

2. 好一点的封装方法
  1. 定义一个(或多个)显示不同加载状态的控件或者xml布局文件(例如:LoadingView
  2. 定义一个工具类(LoadingUtil)来管理LoadingView,不同状态显示不同的UI(或者在多个View之间切换显示)
  3. BaseActivity/BaseFragment中对LoadingUtil的使用进行封装,暴露给子类以下方法:
    • void showLoading(); //调用此方法显示加载中的动画
    • void showLoadFailed(); //调用此方法显示加载失败界面
    • void showEmpty(); //调用此方法显示空页面
    • void onClickRetry(); //子类中实现,点击重试的回调方法
    • abstract int getContainerId(); //子类中实现,LoadingUtil动态创建LoadingView并添加到该方法返回id对应的控件中
  4. BaseActivity/BaseFragment的子类中可通过上一步的封装比较方便地使用加载状态显示功能

这种封装的好处是通过封装动态地创建LoadingView并添加到指定的父容器中,让具体页面无需关注LoadingView的实现,只需要指定在哪个容器中显示即可,很大程度地进行了解耦。如果公司只在一个App中使用,这基本上就够了。

但是,这种封装方式还是存在耦合:页面与它所使用的LoadingView仍然存在绑定关系。如果需要复用到其它App中,因为每个App的UI风格可能不同,对应的LoadingView布局也可能会不一样,要想复用必须先将页面与LoadingView解耦。

如何解耦?


1. 梳理一下我们需要实现的效果
  • 页面的LoadingView可切换,且不需要改动页面代码
  • 页面中可指定LoadingView的显示区域(例如导航栏Title不希望被LoadingView覆盖)
  • 支持在Fragment中使用
  • 支持加载失败页面中点击重试
  • 兼容不同页面显示的UI有细微差别(例如提示文字可能不同)
2. 确定思路

说到View的解耦,很容易联想到Android系统中的AdapterView(我们常用的GridView和ListView都是它的子类)及support包里提供的ViewPager、RecyclerView等,它们都是通过Adapter来解耦的,将自身的逻辑与需要动态变化的子View进行分离。我们也可以按照这个思路来解耦LoadingView:

  • 创建一个工具类,用于管理LoadingView各个状态的UI展示
  • 创建一个Adapter接口,外部提供实现类,通过getView方法创建具体的LoadingView
  • 每个App提供一个Adapter的实现,并注册到工具类中
  • 工具类从Adapter.getView获取具体的LoadingView,所以页面中使用的代码无需改动
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
(已实现)页面的LoadingView可切换,且不需要改动页面代码 
  • 由于每个页面或View的加载状态互相之间无关联关系,需要创建一个用于管理具体某个LoadingView的状态持有类:Holder
  • 指定LoadingView所需覆盖的View时,动态新建一个FrameLayout布局
  • 将原View从ParentView中移除,并用它的LayoutParams将FrameLayout添加到ParentView中替代原View在ParentView中的位置
  • 再将原View添加到FrameLayout中
  • 在Fragment.onCreateView/RecyclerView.Adapter.onCreateViewHolder等方法中创建的View时,由于View尚未添加到任何容器中,并无getParent()返回null,此时需要用动态生成的FrameLayout代替原View作为方法的返回值返回

上代码更容易理解:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public Holder wrap(View view) {
    FrameLayout wrapper = new FrameLayout(view.getContext());
    ViewGroup.LayoutParams lp = view.getLayoutParams();
    if (lp != null) {
        wrapper.setLayoutParams(lp);
    }
    if (view.getParent() != null) {
        ViewGroup parent = (ViewGroup) view.getParent();
        int index = parent.indexOfChild(view);
        parent.removeView(view);
        parent.addView(wrapper, index);
    }
    LayoutParams newLp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    wrapper.addView(view, newLp);
    return new Holder(mAdapter, view.getContext(), wrapper);
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
(已实现)页面中可指定LoadingView的显示区域
(已实现)支持在Fragment中使用
另外,还顺带支持在RecyclerView、ListView、GridView、ViewPager等情况下的使用
  • 为了不侵入UI,将加载失败点击重试的点击功能放在Adapter.getView中实现
  • 与Android系统中的Adapter不同的是,我们的Adapter是全局使用的,而失败重试所需执行逻辑每个页面都不一样
  • 因为Holder可以持有每个具体的LoadingView,可以将retryTask通过Holder传递给Adapter
  • 只需要在Adapter.getView时将Holder作为参数传入,即可在创建LoadingView时获取该retryTask对象,并在点击重试按钮时执行retryTask
  • 同理,可以通过Holder传递一些附加参数给Adapter,以兼容在不同页面上布局的细微差异
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
(已实现)支持加载失败页面中点击重试
(已实现)兼容不同页面显示的UI有细微差别(例如提示文字可能不同)

使用Gloading来轻松实现低耦合的全局LoadingView


Gloading是一个基于Adapter思路实现的深度解耦App中全局LoadingView的轻量级工具(只有一个java文件,不到300行,其中注释占100+行,aar仅6K)

1、 依赖Gloading

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
compile 'com.billy.android:gloading:1.0.0'

2、 创建Adapter,在getView方法中实现创建各种状态视图(加载中、加载失败、空数据等)的逻辑

Gloading不侵入UI布局,完全由用户自定义。示例如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class GlobalAdapter implements Gloading.Adapter {
    @Override
    public View getView(Gloading.Holder holder, View convertView, int status) {
        GlobalLoadingStatusView loadingStatusView = null;
        //convertView为可重用的布局
        //Holder中缓存了各状态下对应的View
        //  如果status对应的View为null,则convertView为上一个状态的View
        //  如果上一个状态的View也为null,则convertView为null
        if (convertView != null && convertView instanceof GlobalLoadingStatusView) {
            loadingStatusView = (GlobalLoadingStatusView) convertView;
        }
        if (loadingStatusView == null) {
            loadingStatusView = new GlobalLoadingStatusView(holder.getContext(), holder.getRetryTask());
        }
        loadingStatusView.setStatus(status);
        return loadingStatusView;
    }

    class GlobalLoadingStatusView extends RelativeLayout {

        public GlobalLoadingStatusView(Context context, Runnable retryTask) {
            super(context);
            //初始化LoadingView
            //如果需要支持点击重试,在适当的时机给对应的控件添加点击事件
        }

        public void setStatus(int status) {
            //设置当前的加载状态:加载中、加载失败、空数据等
            //其中,加载失败可判断当前是否联网,可现实无网络的状态
            //      属于加载失败状态下的一个分支,可自行决定是否实现
        }
    }
}

3、 初始化Gloading的默认Adapter

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Gloading.initDefault(new GlobalAdapter());
复制代码

注:可以用AutoRegister在Gloading类装载进虚拟机时自动完成初始化注册,无需在app层执行注册,耦合度更低

4、在需要使用LoadingView的地方获取Holder

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//在Activity中显示, 父容器为: android.R.id.content
Gloading.Holder holder = Gloading.getDefault().wrap(activity);

//传递点击重试需要执行的task,该task在Adapter中用holder.getRetryTask()获取
Gloading.Holder holder = Gloading.getDefault().wrap(activity).withRetry(retryTask);

//传递点击重试需要执行的task和一个任意类型的扩展参数,该参数在Adapter中用holder.getData()获取
Gloading.Holder holder = Gloading.getDefault().wrap(activity).withRetry(retryTask).withData(obj);

or

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//为某个View显示加载状态
//Gloading会自动创建一个FrameLayout,将view包裹起来,LoadingView也显示在其中
Gloading.Holder holder = Gloading.getDefault().wrap(view);

//传递点击重试需要执行的task,该task在Adapter中用holder.getRetryTask()获取
Gloading.Holder holder = Gloading.getDefault().wrap(view).withRetry(retryTask);

//传递点击重试需要执行的task和一个任意类型的扩展参数,该参数在Adapter中用holder.getData()获取
Gloading.Holder holder = Gloading.getDefault().wrap(view).withRetry(retryTask).withData(obj);

5、 使用Holder来显示各种加载状态

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//显示加载中的状态,通常是显示一个加载动画
holder.showLoading() 

//显示加载成功状态(一般是隐藏LoadingView)
holder.showLoadSuccess()

//显示加载失败状态
holder.showFailed()

//数据加载完成,但数据为空
holder.showEmpty()

//如果以上默认提供的状态不能满足使用,可使用此方法调用其它状态
holder.showLoadingStatus(status)

更多API详情请查看 Gloading JavaDocs

更多Demo示例代码请查看 Gloading Demo, 也可下载Demo apk体验

6、封装到BaseActivity/BaseFragment中

  • 让BaseActivity和BaseFragment的子类中使用LoadingView更方便
  • 子类中使用LoadingView的业务逻辑与实现分离
  • 如果原来就是封装到BaseActivity/BaseFragment中的,那么可以无缝切换到Gloading
  • 如果以后需要将Gloading移除替换成其它实现,也无需修改业务代码

示例代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public abstract class BaseActivity extends Activity {

    protected Gloading.Holder mHolder;

    /**
     * make a Gloading.Holder wrap with current activity by default
     * override this method in subclass to do special initialization
     * @see SpecialActivity
     */
    protected void initLoadingStatusViewIfNeed() {
        if (mHolder == null) {
            //bind status view to activity root view by default
            mHolder = Gloading.getDefault().wrap(this).withRetry(new Runnable() {
                @Override
                public void run() {
                    onLoadRetry();
                }
            });
        }
    }

    protected void onLoadRetry() {
        // override this method in subclass to do retry task
    }

    public void showLoading() {
        initLoadingStatusViewIfNeed();
        mHolder.showLoading();
    }

    public void showLoadSuccess() {
        initLoadingStatusViewIfNeed();
        mHolder.showLoadSuccess();
    }

    public void showLoadFailed() {
        initLoadingStatusViewIfNeed();
        mHolder.showLoadFailed();
    }

    public void showEmpty() {
        initLoadingStatusViewIfNeed();
        mHolder.showEmpty();
    }

}

7、 兼容多App场景下的页面、View的复用

每个App的LoadingView可能会不同,只需为每个App提供不同的Adapter,不同App调用不同的Gloading.initDefault(new GlobalAdapter());,具体页面中的使用代码无需改动。

注:如果使用AutoRegister,则只需在不同App中创建各自的 Adapter实现类即可,无需手动注册。只需改动2处gradle文件即可:

  • 修改根目录build.gradle,添加对AutoRegister的依赖
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
buildscript {
    //...
    dependencies {
        //...
        classpath 'com.billy.android:autoregister:使用最新版'
    }
}
  • 修改主application module下的build.gradle,添加如下代码即可实现Adapter的自动注册
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
apply plugin: 'auto-register'
autoregister {
   registerInfo = [
       [
           'scanInterface'             : 'com.billy.android.loading.Gloading$Adapter'
           , 'codeInsertToClassName'   : 'com.billy.android.loading.Gloading'
           , 'registerMethodName'      : 'initDefault'
       ]
   ]
}

演示


1. 为Activity添加加载状态

image

image

image.png

为View添加加载状态

image

image.png

image

image

总结


本文介绍了全局LoadingView在实际使用过程中可能存在的一些耦合情况,并指出了由此会影响多个App的LoadingView的UI风格不一致导致页面难以复用的问题,同时给出了解决思路。

另外,本文着重介绍了如何使用Gloading来轻松实现低耦合的全局LoadingView,喜欢的同学请顺手甩个star支持一下 :)

【附】相关架构及资料

加群 Android IOC架构设计领取获取往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序Flutter全方面的Android进阶实践技术,群内还有技术大牛一起讨论交流解决问题。

image

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019.03.29 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
开发基于SpringBoot的分布式任务中间件DcsSchedule(为开源贡献力量)
咔咔,上面这段代码很熟悉吧,他就是SpringBoot的Schedule定时任务,简单易用。在我们开发中如果需要做一些定时或指定时刻循环执行逻辑时候,基本都会使用到Schedule。
小傅哥
2020/07/14
3370
定时任务不在硬编码,动态定时刷起来 | Java Debug 笔记
啵啵肠
2023/11/29
2790
quartz监控日志(四)自定义QuartzJobBean来实现监控
上面几章介绍了quartz监控的几种方式,下面再介绍一种监听方式:自定义QuartzJobBean
一笠风雨任生平
2020/03/19
2.2K1
利用Spring Boot轻松实现动态定时器开发!
-上面的代码已经上传至gitee 地址:https://gitee.com/zxhTom/crontab.git
Java程序猿
2021/06/17
1.1K0
Spring: 定时任务 @Scheduled 原理分析
当一个方法被加上@Schedule注解,然后做一些相关配置,在Spring容器启动之后,这个方法就会按照@Schedule注解的配置周期性或者延迟执行。Spring是如何办到这个的,本文就讲解一下这块的原理。
Freedom123
2024/03/29
7610
【小家Spring】Spring任务调度@Scheduled的使用以及原理、源码分析(@EnableScheduling)
JDK给我们提供了定时任务的能力,详解之前有篇博文: 【小家java】Java定时任务ScheduledThreadPoolExecutor详解以及与Timer、TimerTask的区别(执行指定次数停止任务)
YourBatman
2019/09/03
3.9K0
动态更改 Spring 定时任务 Cron 表达式的优雅方案!
在 SpringBoot 项目中,我们可以通过@EnableScheduling注解开启调度任务支持,并通过@Scheduled注解快速地建立一系列定时任务。
java进阶架构师
2024/03/06
1.2K1
动态更改 Spring 定时任务 Cron 表达式的优雅方案!
Spring 定时任务框架详解(3)——源码分析
如前文所述,可通过@EnableScheduling注解开启定时任务调度,所以我们从@EnableScheduling注解开始:
张申傲
2020/09/03
8420
聊聊springboot2的ScheduledTasksEndpoint
本文主要研究下springboot2的ScheduledTasksEndpoint
code4it
2018/09/17
8600
彻底弄懂Spring Schedule加载和执行流程
Spring Scheduled Spring定时任务源码分析 入口,启用定时任务注解 @EnableScheduling @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Import(SchedulingConfiguration.class) @Documented public @interface EnableScheduling { } org.springframework.scheduling.annotati
石奈子
2020/06/28
1.9K0
Spring定时任务原理
笔者目前在一家银行工作,正在参与手机银行项目的功能开发,正好碰到一家分行搬迁,直接合并到总行营业部,因此在手机银行上涉及到网点,开户机构的功能的页面,都需要不展示该机构,当笔者刚拿到这个需求的时候,非常惊讶,认为这种功能应该早就做好了,应该是可以直接在后管中进行配置,一通分析下来,发现居然并没有这种功能。
小刘Learning
2024/03/17
2790
Spring定时任务原理
SpringBoot——动态多线程并发定时任务
怎么重新调用 refreshTask()方法:可以另外启一个任务实时监控任务表的数据变化。
默存
2022/06/24
3960
SpringBoot——动态多线程并发定时任务
记一次Spring定时任务非预期执行的解决与原理
但观察日志发现, 有的任务执行间隔并不是1s, 同时可以观察到, 多个task是使用的同一线程执行的, 完全不符合预期.
一个架构师
2022/06/20
5030
记一次Spring定时任务非预期执行的解决与原理
@Scheduled注解的坑,我替你踩了
在一些业务场景中,我们需要执行定时操作来完成一些周期性的任务,比如每隔一周删除一周前的某些历史数据以及定时进行某项检测任务等等。在日常开发中比较简单的实现方式就是使用Spring的@Scheduled(具体使用方法不再赘述)注解。但是这个Spring框架自带的注解其实是有坑的。在修改服务器时间时会导致定时任务不执行情况的发生,粗暴解决办法是当修改服务器时间后,将服务进行重启就可以避免此现象的发生。本文将主要探讨服务器时间修改导致@Scheduled注解失效的原因,同时找到在修改服务器时间后不重启服务的情况下,定时任务仍然正常执行的方法。
慕枫技术笔记
2023/03/20
8250
@Scheduled注解的坑,我替你踩了
任务调度框架 Quartz 用法指南(超详细)
前言 项目中遇到一个,需要 客户自定任务启动时间 的需求。原来一直都是在项目里硬编码一些定时器,所以没有学习过。 很多开源的项目管理框架都已经做了 Quartz 的集成。我们居然连这么常用得东西居然没有做成模块化,实在是不应该。 Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,完全由Java开发,可以用来执行定时任务,类似于java.util.Timer。但是相较于Timer, Quartz增加了很多功能: 持久性作业 - 就是保持调度定时的状态; 作业管理
java思维导图
2022/07/26
3.8K0
任务调度框架 Quartz 用法指南(超详细)
手写动态定时任务
在开发中我们一般使用quartz来做动态的定时任务,但是这是别人写好的开发框架,要是我们自己想手动实现一个呢。那应该怎么做呢。
分享干货的你
2021/04/06
1K0
通过源码理解Spring中@Scheduled的实现原理并且实现调度任务动态装载
最近的新项目和数据同步相关,有定时调度的需求。之前一直有使用过Quartz、XXL-Job、Easy Scheduler等调度框架,后来越发觉得这些框架太重量级了,于是想到了Spring内置的Scheduling模块。而原生的Scheduling模块只是内存态的调度模块,不支持任务的持久化或者配置(配置任务通过@Scheduled注解进行硬编码,不能抽离到类之外),因此考虑理解Scheduling模块的底层原理,并且基于此造一个简单的轮子,使之支持调度任务配置:通过配置文件或者JDBC数据源。
Throwable
2020/06/23
2.6K0
通过源码理解Spring中@Scheduled的实现原理并且实现调度任务动态装载
spring如何设置定时任务详解(@Scheduled)
spring定时任务设置有两种方式,注解和xml配置。推荐使用注解,在本文章也主要介绍注解方式配置
洋仔聊编程
2019/01/15
21.8K0
SpringBoot动态配置定时任务cron(动态改变执行周期)
package model; public class SpringScheduledCron { private String cronId; private String cronKey; private String cronExpression; private String taskExplain; private String status; @Override public String toString() {
JQ实验室
2022/02/10
1.7K0
浅谈Spring中定时任务@Scheduled源码的解析(二)
上面的代码只是讲述了如何获取到task,那么接下来如何将这些task当成定时任务来执行呢
半月无霜
2024/07/25
1740
推荐阅读
相关推荐
开发基于SpringBoot的分布式任务中间件DcsSchedule(为开源贡献力量)
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验