美文网首页Android开发经验谈Android技术知识程序员
我们经常用的Loading动画居然还有这种姿势

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

作者: 06fd4cf1f427 | 来源:发表于2019-03-29 21:03 被阅读80次

    背景


    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,所以页面中使用的代码无需改动
    (已实现)页面的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作为方法的返回值返回

    上代码更容易理解:

    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);
    }
    
    (已实现)页面中可指定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,以兼容在不同页面上布局的细微差异
    (已实现)支持加载失败页面中点击重试
    (已实现)兼容不同页面显示的UI有细微差别(例如提示文字可能不同)
    

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


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

    1、 依赖Gloading

    compile 'com.billy.android:gloading:1.0.0'
    

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

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

    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

    Gloading.initDefault(new GlobalAdapter());
    复制代码
    

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

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

    //在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

    //为某个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来显示各种加载状态

    //显示加载中的状态,通常是显示一个加载动画
    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移除替换成其它实现,也无需修改业务代码

    示例代码如下:

    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的依赖
    buildscript {
        //...
        dependencies {
            //...
            classpath 'com.billy.android:autoregister:使用最新版'
        }
    }
    
    • 修改主application module下的build.gradle,添加如下代码即可实现Adapter的自动注册
    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

    领取方式:

    点赞+加群免费获取 Android IOC架构设计

    相关文章

      网友评论

        本文标题:我们经常用的Loading动画居然还有这种姿势

        本文链接:https://www.haomeiwen.com/subject/dkorbqtx.html