咱们常常用的Loading动画竟然还有这种姿式

背景

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

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

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

加载中、加载失败、空数据的UI风格,通常来讲在App内的全部页面中须要保持一致,也就是须要作到全局统一。github

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中使用,这基本上就够了。bash

可是,这种封装方式仍是存在耦合:页面与它所使用的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:app

  • 建立一个工具类,用于管理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做为方法的返回值返回

上代码更容易理解:ide

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)工具

一、 依赖Gloading

compile 'com.billy.android:gloading:1.0.0'
复制代码

二、 建立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) {
            //设置当前的加载状态:加载中、加载失败、空数据等
            //其中,加载失败可判断当前是否联网,可现实无网络的状态
            // 属于加载失败状态下的一个分支,可自行决定是否实现
        }
    }
}
复制代码

三、 初始化Gloading的默认Adapter

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

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

四、在须要使用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);
复制代码

五、 使用Holder来显示各类加载状态

//显示加载中的状态,一般是显示一个加载动画
holder.showLoading() 

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

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

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

//若是以上默认提供的状态不能知足使用,可以使用此方法调用其它状态
holder.showLoadingStatus(status)
复制代码

更多API详情请查看 Gloading JavaDocs

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

六、封装到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();
    }

}
复制代码

七、 兼容多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添加加载状态

为View添加加载状态

总结

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

另外,本文着重介绍了如何使用Gloading来轻松实现低耦合的全局LoadingView。并且,还能够按照本文的思路来解决项目中其它解耦工做。

最后,你的star会成为做者开源的动力哦 :)

相关文章
相关标签/搜索