Android 让你的布局飞起来

作者: 伪文艺大叔 | 来源:发表于2017-01-09 16:29 被阅读4991次
    xiaoguo.gif

    前言

    在Android项目开发中一个界面的显示状态包括好几种:内容界面,loading界面,网络错误界面等等;以前开发的时候都是直接把这些界面include到main界面中,然后动态去切换界面,后来发现这样处理不容易复用到其他项目中,而且在activity中处理这些状态的显示和隐藏比较乱,所以就想着能不能封装一个类来管理这些状态View的切换。

    思路

    为了让View状态的切换和Activity彻底分离开,必须把这些状态View都封装到一个管理类中,然后暴露出几个方法来实现View之间的切换,因为在不同的项目中可以需要的View也不一样,所以考虑把管理类设计成builder模式来自由的添加需要的状态View。

    实现

    通常一个界面会包括:内容,空数据,异常错误,加载,网络错误等5种状态的View,所以我们就设置这5种状态View的切换

    public static final class Builder {    
    
            private Context context;    
            private int loadingLayoutResId;    
            private int contentLayoutResId;    
            private ViewStub netWorkErrorVs;    
            private int netWorkErrorRetryViewId;
            private ViewStub emptyDataVs;
            private int emptyDataRetryViewId;
            private ViewStub errorVs;
            private int errorRetryViewId;
            private int retryViewId;
            private OnShowHideViewListener onShowHideViewListener;   
            private OnRetryListener onRetryListener; 
    
            public Builder(Context context) {       
                this.context = context;    
            }    
    
            public Builder loadingView(@LayoutRes int loadingLayoutResId) {    
                this.loadingLayoutResId = loadingLayoutResId;        
                return this;    
            }    
    
            public Builder netWorkErrorView(@LayoutRes int newWorkErrorId) {    
                netWorkErrorVs = new ViewStub(context);     
                netWorkErrorVs.setLayoutResource(newWorkErrorId);        
                return this;    
            }    
    
           public Builder emptyDataView(@LayoutRes int noDataViewId) {    
                emptyDataVs = new ViewStub(context);        
                emptyDataVs.setLayoutResource(noDataViewId);       
                return this;   
           }    
    
           public Builder errorView(@LayoutRes int errorViewId) {        
                errorVs = new ViewStub(context);   
                errorVs.setLayoutResource(errorViewId);        
                return this;    
           }    
    
          public Builder contentView(@LayoutRes int contentLayoutResId) {       
                this.contentLayoutResId = contentLayoutResId;        
                return this;    
          }    
    
            public Builder netWorkErrorRetryViewId(int netWorkErrorRetryViewId) {
                this.netWorkErrorRetryViewId = netWorkErrorRetryViewId;
                return this;
            }
    
            public Builder emptyDataRetryViewId(int emptyDataRetryViewId) {
                this.emptyDataRetryViewId = emptyDataRetryViewId;
                return this;
            }
    
            public Builder errorRetryViewId(int errorRetryViewId) {
                this.errorRetryViewId = errorRetryViewId;
                return this;
            }
    
            public Builder retryViewId(int retryViewId) {
                this.retryViewId = retryViewId;
                return this;
            }
    
          public Builder onShowHideViewListener(OnShowHideViewListener onShowHideViewListener) {       
                 this.onShowHideViewListener = onShowHideViewListener;        
                 return this;    
          }
    
           public Builder onRetryListener(OnRetryListener onRetryListener) {
                this.onRetryListener = onRetryListener;
                return this;
            }    
    
          public StatusLayoutManager build() {        
                 return new StatusLayoutManager(this);   
          }
    }
    

    状态管理类用到了建造者模式,上面是builder内部类,总共有11个属性,loadingLayoutResId和contentLayoutResId代表等待加载和显示内容的xml文件;netWorkErrorVs,emptyDataVs,errorVs代表另外几种异常状态,那为什么这几种状态要用ViewStub,因为在界面状态切换中loading和内容View都是一直需要加载显示的,但是其他的3个只有在没数据或者网络异常的情况下才会加载显示,所以用ViewStub来加载他们可以提高性能。

    在错误的几个界面需要重试按钮重新加载数据,netWorkErrorRetryViewId, emptyDataRetryViewId, errorRetryViewId分别为几个状态界面重试按钮的id, 如果这几个按钮的id是一样的话就直接给retryViewId属性赋值即可,retryViewId优先级最高。

    onShowHideViewListener为状态View显示隐藏监听事件
    onRetryListener为重试加载按钮的监听事件

    接下来需要把这些View添加到一个根View中返回给Activity,为了方便显示隐藏这些View,我们在根View中定义一个集合属性,然后把这些View添加到集合当中管理

    /** *  存放布局集合 */
    private SparseArray<View> layoutSparseArray = new SparseArray();
    

    这个集合Key为id,Value为View,id为根View类内部自定义的id,通过id找到对应的View来显示隐藏View,下面通过一个方法来看下它的切换逻辑

    /** *  显示空数据 */
    public void showEmptyData() {    
         if(inflateLayout(LAYOUT_EMPTYDATA_ID))      
          showHideViewById(LAYOUT_EMPTYDATA_ID);
    }
    

    首先调用inflateLayout方法,方法返回true然后调用showHideViewById方法,
    下面来看看inflateLayout方法的实现

    private boolean inflateLayout(int id) {    
        boolean isShow = true;    
        if(layoutSparseArray.get(id) != null) return isShow;    
        switch (id) {        
           case LAYOUT_NETWORK_ERROR_ID:            
             if(mStatusLayoutManager.netWorkErrorVs != null) {    
               View view = mStatusLayoutManager.netWorkErrorVs.inflate();
               retryLoad(view, mStatusLayoutManager.netWorkErrorRetryViewId);
               layoutSparseArray.put(id, view);                
               isShow = true;            
             } else {                
               isShow = false;            
             }            
             break;        
    
           case LAYOUT_ERROR_ID:            
               if(mStatusLayoutManager.errorVs != null) {
                  View view = mStatusLayoutManager.errorVs.inflate();
                  retryLoad(view, mStatusLayoutManager.errorRetryViewId);   
                  layoutSparseArray.put(id, view);                
                  isShow = true;            
               } else {                
                  isShow = false;           
               }            
               break;        
    
          case LAYOUT_EMPTYDATA_ID:            
              if(mStatusLayoutManager.emptyDataVs != null) {
                  View view = mStatusLayoutManager.emptyDataVs.inflate();
                  retryLoad(view, mStatusLayoutManager.emptyDataRetryViewId);     
                  layoutSparseArray.put(id, view);                
                  isShow = true;            
              } else {                
                  isShow = false;            
              }            
              break;    
          }    
          return isShow;
    }
    

    方法里面通过id判断来执行不同的代码,首先判断ViewStub是否为空,如果为空就代表没有添加这个View就返回false,不为空就加载View并且添加到集合当中,然后调用showHideViewById方法显示隐藏View,retryLoad方法是给重试按钮添加事件,先来看看showHideViewById方法逻辑

    private void showHideViewById(int id) {    
        for(int i = 0; i < layoutSparseArray.size(); i++) {        
            int key = layoutSparseArray.keyAt(i);        
            View valueView = layoutSparseArray.valueAt(i);        
            //显示该view        
            if(key == id) {            
                valueView.setVisibility(View.VISIBLE);            
                if(mStatusLayoutManager.onShowHideViewListener != null) 
                   mStatusLayoutManager.onShowHideViewListener.onShowView(valueView, key);
             } else {           
                 if(valueView.getVisibility() != View.GONE) {     
                     valueView.setVisibility(View.GONE);                
                     if(mStatusLayoutManager.onShowHideViewListener != null) 
                        mStatusLayoutManager.onShowHideViewListener.onHideView(valueView, key);
                 } 
             }        
       }    
    }
    

    通过id找到需要显示的View并且显示它,隐藏其他View,如果显示隐藏监听事件不为空,就分别调用它的显示和隐藏方法,下面再来看看retryLoad方法

     public void retryLoad(View view, int id) {
            View retryView = view.findViewById(mStatusLayoutManager.retryViewId != 0 ? mStatusLayoutManager.retryViewId : id);
            if(retryView == null || mStatusLayoutManager.onRetryListener == null) return;
            retryView.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    mStatusLayoutManager.onRetryListener.onRetry();
                }
            });
      }
    

    可以看出retryViewId 的优先级最好,如果它不为0,就用它去查找View实例,然后如果View实例和重试监听都不为空就添加点击事件,点击事件里调用onRetryListener监听的onRetry方法。

    使用

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            initToolBar();
    
            LinearLayout mainLinearLayout = (LinearLayout) findViewById(R.id.main_rl);
            statusLayoutManager = StatusLayoutManager.newBuilder(this)
                    .contentView(R.layout.activity_content)
                    .emptyDataView(R.layout.activity_emptydata)
                    .errorView(R.layout.activity_error)
                    .loadingView(R.layout.activity_loading)
                    .netWorkErrorView(R.layout.activity_networkerror)
                    .retryViewId(R.id.button_try)
                    .onShowHideViewListener(new OnShowHideViewListener() {
                        @Override
                        public void onShowView(View view, int id) {
                        }
    
                        @Override
                        public void onHideView(View view, int id) {
                        }
                    }).onRetryListener(new OnRetryListener() {
                        @Override
                        public void onRetry() {
                            statusLayoutManager.showLoading();
    
                            new Thread(new Runnable() {
                                @Override
                                public void run() {
                                    try {
                                        Thread.sleep(1000);
                                    } catch (InterruptedException e) {
                                        e.printStackTrace();
                                    }
    
                                    runOnUiThread(new Runnable() {
                                        @Override
                                        public void run() {
                                            statusLayoutManager.showContent();
                                        }
                                    });
                                }
                            }).start();
    
                        }
                    }).build();
    
            mainLinearLayout.addView(statusLayoutManager.getRootLayout(), 1);
    
            statusLayoutManager.showLoading();
        }
    

    StatusLayoutManager提供了一系列的方法来显示不同布局View之间的切换

      statusLayoutManager.showLoading(); 显示loading加载view
    
      statusLayoutManager.showContent(); 显示你的内容view
    
      statusLayoutManager.showEmptyData(); 显示空数据view
    
      statusLayoutManager.showError(); 显示error view
    
      statusLayoutManager.showNetWorkError();  显示网络异常view
    

    结束语

    至此,核心逻辑和代码都已经分析完成,想看如何调用和源码的朋友可以移步至:https://github.com/chenpengfei88/StatusLayout

    我还有一篇封装底部导航栏的文章,大家有兴趣也可以看看
    http://www.jianshu.com/p/7cccb5c054da#

    相关文章

      网友评论

      • 一起睡个觉:这个项目会继续维护 优化 下去吗
        伪文艺大叔:@zxy_it 会啊
      • 721d739b6619:博主,纯探讨,想问问你的StatusLayoutManager类为什么还要建一个Builder内部类?有何意义呀?设置属性不能直接在StatusLayoutManager的getset方法,只要return回this也可以实现你所谓的建造者模式。
        721d739b6619:@伪文艺大叔 谢谢,解惑了
        伪文艺大叔:这个设置完以后。设置的参数是不能随便改的,如果用set方法就随时可以在项目的任何地方改
      • 37d5fc52de9e:说实话,给的demo真的很不咋的
      • 38bc7d17a79b:这样会不会多了一层FrameLayout视图结构,会影响性能吗?
        8e126084ea1c:您好博主,在您的这个框架基础上我想再次封装没有实现的了。 因为我想把这些状态全部放在BaseActivity里面,然后把content的布局暴露出来,然后再用butterknife注解的方式来实现。。可是我失败啦,请问像这样怎么封装呢?方便提点一二嘛
        38bc7d17a79b:@伪文艺大叔 谢谢楼主分享
        伪文艺大叔: @cj150104116 相对于便利来说,这点性能可以忽略不计
      • LPhoenix:调用时候怎么感觉有点麻烦呢
        伪文艺大叔: @第四维度 我暂时也没啥更好的方案😅
        LPhoenix:@伪文艺大叔 是啊,有点多
        伪文艺大叔:你是感觉那些监听看起来麻烦吗
      • 萌萌的白天:不错的思路 但是 。可不可以在优化下。
        伪文艺大叔: @萌萌的白天 嗯,用到项目中的话还会有细节可以优化
        萌萌的白天:我只是觉得 还可不可以 在其他方面优化
        伪文艺大叔: @萌萌的白天 方便说哪里不合理吗😅

      本文标题:Android 让你的布局飞起来

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