美文网首页
MultiStatusLayout

MultiStatusLayout

作者: 莫比乌丝环丶 | 来源:发表于2019-10-12 20:34 被阅读0次

    一、场景以及解决的问题

    实际项目中,经常会遇到,刚进入某一个界面,需要请求数据显示加载中布局,网络错误显示网络错误布局,服务器错误时显示服务器错误布局,列表数据为空时显示空布局。
    最开始的时候,会将这几个布局全部堆积在主布局中设置为Gone,等到需要的时候再去Visible。这样子会有以下几个问题:

    • 一次性会将所有情况下对应的布局全部加载
    • 导致真正的页面布局臃肿,不利于后期维护
    • 做不到按需加载或懒加载
      MultiStatusLayout支持按需加载、便于调用控制、支持扩展至任意Layout

    二、实际效果以及项目中配置

    1.效果

    demo

    2.项目中集成配置

    详细配置以及用法,参见github

    1)gradle集成
    allprojects {
            repositories {
                ...
                maven { url 'https://jitpack.io' }
            }
        }
        
    dependencies {
            implementation 'com.github.Walll-E.MultiStatusLayout:library:1.0.7'
            annotationProcessor 'com.github.Walll-E.MultiStatusLayout:compiler:1.0.7'
        }
    
    2)使用

    定义一个类比如MultiStatusInit ,类顶部添加注解MultiStatus,点击AndroidStudio的build即可。静静的等待编译完毕,双击shift按钮出现搜索框,输入 MultiStatus 就会检索出来相关的类,如下配置的四个Layout:编译生成的类如下:
    MultiStatusLayoutMultiStatusConstraintLayoutMultiStatusFrameLayoutMultiStatusLinearLayout

    @MultiStatus(value = {
            RelativeLayout.class, 
            ConstraintLayout.class, 
            FrameLayout.class, 
            LinearLayout.class},
            
            provider = {
                    RelativeLayoutConstraintProvider.class,
                    ConstraintLayoutConstraintProvider.class, 
                    FrameLayoutConstraintProvider.class, 
                    LinearLayoutConstraintProvider.class})
    public class MultiStatusInit {
    }
    

    三、Talk is cheap,show me the code

    1.项目结构介绍:

    Annotation:MultiStatus属性分别是:value和provider。

    • value:代表需要动态生成的系统以及第三方Layout,例如RelativeLayoutConstraintLayout
    • provider:对应value中Layout的约束提供类,例如RelativeLayoutConstraintProviderConstraintLayoutConstraintProvider这两个由sdk内部提供

    Compiler:根据 注解MultiStatus中配置的value,provider动态生成相应的MultiStatusxxxxxxLayout,apt生成供外部使用的核心类

    Library:核心module

    • MultiStatusEvent:利用apt生成的MultiStatusxxxxxxLayout实现这个接口,此接口提供生成类的一些行为(showLoading、showContent、showEmpty等)
    • OnReloadDataListener:网络错误,服务器等错误时,显示相应布局中重试接口
    • MultiStatusHelper:根据不同情况显示相应布局的核心类

    2.MultiStatusxxxxLayout提供的属性介绍:

    属性名称 说明
    loadingLayout 加载中的布局
    emptyLayout 数据为空时的布局
    netErrorLayout 网络错误时的布局
    errorLayout 加载失败时的布局
    otherLayout 扩充的布局
    targetViewId 子控件中任何时候都显示的控件id
    netErrorReloadViewId 网络错误重试按钮id
    errorReloadViewId 加载失败重试按钮id
    contentReferenceIds showContent()调用后,contentReferenceIds不受其控制;id之间的间隔英文','
    emptyReferenceIds showEmpty()调用后,emptyReferenceIds不受其控制;id之间的间隔英文','
    errorReferenceIds showError()调用后,errorReferenceIds不受其控制;id之间的间隔英文','
    netErrorReferenceIds showNetError()调用后,netErrorReferenceIds不受其控制;id之间的间隔英文','
    otherReferenceIds showOther()调用后,otherReferenceIds不受其控制;id之间的间隔英文','
    loadingReferenceIds showLoading()调用后,loadingReferenceIds不受其控制;id之间的间隔英文','

    3.核心代码:

    Annotation

    MultiStatus代码如下:

    @Retention(RetentionPolicy.CLASS)
    @Target(ElementType.TYPE)
    public @interface MultiStatus {
        Class<? extends ViewGroup>[] value() default {};
    
        Class<? extends ViewConstraintProvider>[] provider() default {};
    }
    
    • value:继承自ViewGroupView,例如RelativeLayoutLinearLayoutConstraintLayout等。
    • provider:实现ViewConstraintProvider这个接口的类,项目内提供RelativeLayoutConstraintProviderConstraintLayoutConstraintProvider
      需要注意的是:value配置的值和provider值顺序必须一致。如果不一致,可能导致一些不可预测的bug -.-

    MultiStatusHelper

    //将xml中获取的字符串ids,解析为单个的字符串id
    private void setIds(String referenceIds, int type) {
            if (referenceIds == null) return;
            int begin = 0;
            while (true) {
                int end = referenceIds.indexOf(",", begin);
                if (end == -1) {
                    addId(referenceIds.substring(begin), type);
                    return;
                }
                addId(referenceIds.substring(begin, end), type);
                begin = end + 1;
            }
        }
        //将单个的字符串id解析为能供系统识别的id
        private void addId(String idString, int type) {
            if (idString == null || mContext == null) return;
            idString = idString.trim();
            int tag = 0;
            try {
                // id.class中的id为:com.wall_e.multiStatusLayout.R.id;
                Class res = id.class;
                Field field = res.getField(idString);
                tag = field.getInt(null);
            } catch (Exception e) {
                e.printStackTrace();
            }
            //如果tag==0,证明没有获取到相应的id
            if (tag == 0) {
                tag = mContext.getResources().getIdentifier(idString, "id", mContext.getPackageName());
            }
            if (tag == 0) {
                Log.d(TAG, "xml中配置的referenceIds并不能被解析,当前的Id:" + idString);
                return;
            }
            //将解析传来的id放入mReferenceIds 缓存起来
            if (mReferenceIds == null) {
                mReferenceIds = new ArrayMap<>();
            }
            if (mReferenceIds.containsKey(type)) {
                List<Integer> list = mReferenceIds.get(type);
                if (list != null && !list.contains(tag)) {
                    list.add(tag);
                }
            } else {
                List<Integer> list = new ArrayList<>();
                list.add(tag);
                mReferenceIds.put(type, list);
            }
        }
    

    以上代码是将app:contentReferenceIds="actionButtonCenter,actionButtonRight,actionButtonLeft"
    中的contentReferenceIds中的id字符串解析为R.id.actionButtonCenter类型,首先截取出来单个的字符串id,然后用包(com.wall_e.multiStatusLayout)下的R.id,反射获取对应的id,如果为0,利用mContext.getResources().getIdentifier(idString, "id", mContext.getPackageName());获取id,如果不为0,则缓存在mReferenceIds 中以供后面的使用。

     /**
         * 加载相应状态的布局,并且添加至ViewGroup中
         *
         * @param index       存放布局容器的索引
         * @param layoutResId 布局资源id
         * @return 返回相应状态的布局
         */
          private View inflateAndAddViewInLayout(int index, int layoutResId) {
            int realIndex = mRealIndex.get(index, -1);
            View view;
            if (realIndex == -1) {
                view = ViewGroup.inflate(mContext, layoutResId, null);
                if (mViewConstraintProvider != null) {
                    mViewConstraintProvider.addViewBlewTargetView(view, mTargetViewId, mParent);
                }
                realIndex = mParent.indexOfChild(view);
                if (realIndex == -1) {
                    mParent.addView(view);
                    realIndex = mParent.getChildCount()-1;
                }
                mRealIndex.put(index, realIndex);
            } else {
                view = mParent.getChildAt(realIndex);
            }
            return view;
        }
    

    首先去判断mRealIndex是否缓存过这个View在ViewGroup中的索引,如果不为-1,则表示此种type的View还没有加载进ViewGroup,利用ViewGroup.inflate(mContext, layoutResId, null);加载完相应的View之后,如果mViewConstraintProvider(View的约束提供)不为空,则将View添加进ViewGroup中并且添加相应的依赖关系 ,所谓的依赖关系主要是mTargetViewId与view的依赖关系,mTargetViewId可以为界面title的id。

       /**
         * 如果有背景,则不需要隐藏其他view
         *
         * @param view
         * @return
         */
        private boolean hasBackground(View view) {
            if (mParent instanceof LinearLayout || mParent instanceof GridLayout) {
                return false;
            } else {
                Drawable drawable = view.getBackground();
                if (drawable instanceof ColorDrawable) {
                    ColorDrawable colorDrawable = (ColorDrawable) drawable;
                    int color = colorDrawable.getColor();
                    return color != Color.TRANSPARENT;
                }
                return drawable instanceof BitmapDrawable;
            }
        }
    

    如果mParent 是LinearLayout或者GridLayout,直接返回false。调用showLoading,showEmpty,showError等方法时,需要不显示布局中其他View,因为这两种ViewGroup布局原理的问题,需要直接隐藏其他View
    如果不是上面的那两种View,获取他们的background,如果是ColorDrawable并且 colorDrawable.getColor()的值不是Color.TRANSPARENT;如果是BitmapDrawable则不隐藏,其他的一概隐藏

      /**
         * 按需隐藏相关的View
         *
         * @param type
         */
        private void hideViews(int type) {
            ViewGroup parent = mParent;
            int targetViewId = mTargetViewId;
            int count = mParent.getChildCount();
            List<Integer> referenceIds = null;
            //根据type获取对应的不受showEmpty、showLoading等控制的缓存id list
            if (mReferenceIds != null) {
                referenceIds = mReferenceIds.get(type);
            }
            int realIndex = mRealIndex.get(type, -1);
            type = isCollectionEmpty(referenceIds) ? -1 : type;
            List<View> views;
            //根据相应的type做出相应的隐藏逻辑
            switch (type) {
                case OTHER_TYPE:
                    views = accordingToTypeShow(realIndex, referenceIds, mOnOtherReferenceIdsAction, parent, targetViewId);
                    if (mOnOtherReferenceIdsAction != null) {
                        mOnOtherReferenceIdsAction.showOtherAction(views);
                    }
                    break;
                case LOADING_TYPE:
                    views = accordingToTypeShow(realIndex, referenceIds, mOnLoadingReferenceIdsAction, parent, targetViewId);
                    if (mOnLoadingReferenceIdsAction != null) {
                        mOnLoadingReferenceIdsAction.showLoadingAction(views);
                    }
                    break;
                case EMPTY_TYPE:
                    views = accordingToTypeShow(realIndex, referenceIds, mOnEmptyReferenceIdsAction, parent, targetViewId);
                    if (mOnEmptyReferenceIdsAction != null) {
                        mOnEmptyReferenceIdsAction.showEmptyAction(views);
                    }
                    break;
                case ERROR_TYPE:
                    views = accordingToTypeShow(realIndex, referenceIds, mOnErrorReferenceIdsAction, parent, targetViewId);
                    if (mOnErrorReferenceIdsAction != null) {
                        mOnErrorReferenceIdsAction.showErrorAction(views);
                    }
                    break;
                case NET_ERROR_TYPE:
                    views = accordingToTypeShow(realIndex, referenceIds, mOnNetErrorReferenceIdsAction, parent, targetViewId);
                    if (mOnNetErrorReferenceIdsAction != null) {
                        mOnNetErrorReferenceIdsAction.showNetErrorAction(views);
                    }
                    break;
                default:
                    for (int i = 0; i < count; i++) {
                        //如果是当前的type在parent中的真正索引等于当前所以,跳过
                        if (i == realIndex) continue;
                        View view = parent.getChildAt(i);
                        //如果view的id==targetViewId 并且当前View是Gone并且当前view是ViewStub 跳过
                        if (targetViewId != view.getId()
                                && view.getVisibility() != GONE
                                && !(view instanceof ViewStub)
                        ) {
                            view.setVisibility(GONE);
                        }
                    }
                    break;
            }
        }
    
        
        private List<View> accordingToTypeHide(int realIndex, List<Integer> referenceIds, OnReferenceViewAction action, ViewGroup mParent, int mTargetViewId) {
            List<View> views = null;
            int childCount = mParent.getChildCount();
            for (int i = 0; i < childCount; i++) {
                if (i == realIndex) continue;
                View view = mParent.getChildAt(i);
                int id = view.getId();
                //缓存中有此id,则此view不受showLoading、showEmpty等方法控制
                if (referenceIds.contains(id)) {
                    //如果type对应的OnReferenceViewAction不为空,将此id对应的view添加至list中返回给上层,用于相应方法调用时触发
                    if (action == null)continue;
                    if (views==null){
                        views = new ArrayList<>();
                    }
                        views.add(view);
                    continue;
                }
                //如果view的id==targetViewId 并且当前View是Gone并且当前view是ViewStub 跳过
                if (mTargetViewId != view.getId()
                        && view.getVisibility() != GONE
                        && !(view instanceof ViewStub)) {
                    view.setVisibility(GONE);
                }
            }
            return views;
        }
    
    

    上面这段代码的核心是处理布局中无条件隐藏的view,不必隐藏须满足以下两个条件:

    • View的id是mTargetViewId
    • View是ViewStub
      特殊说明:
      首先根据type获取相应缓存id list,type为LOADING_TYPE时对应loadingReferenceIds 和 OnLoadingReferenceIdsAction,OnLoadingReferenceIdsAction会回调loadingReferenceIds 中的views,以便对这些view单独做处理。其他的type同理。
    /**
         * 显示
         */
        public void showContent() {
            if (mViewType == CONTENT_TYPE)
                return;
            mViewType = CONTENT_TYPE;
            int count = mParent.getChildCount();
            int size = mRealIndex.size();
            count -= size;
            List<Integer> contentIds = null;
            List<View> contentView = null;
            if (mReferenceIds != null) {
                contentIds = mReferenceIds.get(CONTENT_TYPE);
            }
            if (contentIds != null) {
                contentView = new ArrayList<>();
            }
            boolean hasContentAction = mOnContentReferenceIdsAction != null;
            for (int i = 0; i < count; i++) {
                View view = mParent.getChildAt(i);
                if (contentIds != null && contentIds.contains(view.getId())) {
                    if (hasContentAction)
                        contentView.add(view);
                    continue;
                }
                if (!(view instanceof ViewStub) && view.getVisibility() != VISIBLE) {
                    view.setVisibility(VISIBLE);
                }
            }
            for (int i = 0; i < size; i++) {
                mParent.getChildAt(mRealIndex.valueAt(i)).setVisibility(GONE);
            }
            if (hasContentAction) {
                mOnContentReferenceIdsAction.showContentAction(contentView);
            }
        }
    

    showContent()这个方法用于显示布局中原有的控件。网络请求成功之后您就可以调用这个方法。
    首先获取mReferenceIds缓存的CONTENT_TYPE的ids,遍历parent中的view如果view的id在contentIds中,并且OnContentReferenceIdsAction不为空,将view添加至contentView中。
    最后OnContentReferenceIdsAction不为空,将contentview返回供上层调用处理。

    接下来我们看看自动生成相关Layout的代码类——MultiStatusProcessor

    具体关于APT相关介绍不是本文重点,如果感兴趣自己可以google/baidu。

    @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
            Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(MultiStatus.class);
            if (elements == null || elements.isEmpty()) {
                return true;
            }
            mMessager.printMessage(Diagnostic.Kind.NOTE, "<<<<<<<<<<<<<<<<<<< MultiStatusProcessor process START >>>>>>>>>>>>>>>");
            Map<String, String> viewProviderMap = new HashMap<>();
            List<String> viewClassList = new ArrayList<>();
            List<String> providerClassList = new ArrayList<>();
            //解析MultiStatus注解里面value和provider
            parseParam(elements, viewClassList, providerClassList);
            //将解析出来的value和provider合并
            mergeList(viewClassList, providerClassList, viewProviderMap);
            try {
                //真正生成代码的地方
                generate(viewProviderMap);
            } catch (IOException  e) {
                mMessager.printMessage(Diagnostic.Kind.ERROR, "Exception occurred when generating class file.");
                e.printStackTrace();
            }
            mMessager.printMessage(Diagnostic.Kind.NOTE, "<<<<<<<<<<<<<<<<<<< MultiStatusProcessor process END >>>>>>>>>>>>>>>");
            return true;
        }
    

    APT自动化生成代码的核心方法。

    • 解析注解MultiStatus中的value和provider中的值
    • 需要value和provider中的值对应顺序一致,然后进行合并操作
    • 核心的代码生成逻辑
       private void generate(Map<String, String> viewProviderMap) throws IOException {
            for (Map.Entry<String, String> entry : viewProviderMap.entrySet()) {
                String clazz = entry.getKey();
                String provider = entry.getValue();
                int lastDotIndex = clazz.lastIndexOf(".");
                String superPackageName = clazz.substring(0, lastDotIndex);
                String superClassName = clazz.substring(lastDotIndex + 1);
                String className;
                //因为第一个版本只支持RelativeLayout,当时类名为MultiStatusLayout
                //为了兼容后期其他Layout,生成类的前面都加MultiStatus,例如:MultiStatusLinearLayout
                if (superClassName.equals("RelativeLayout")) {
                    className = CLASS_PREFIX + "Layout";
                } else {
                    className = CLASS_PREFIX + superClassName;
                }
    
                mMessager.printMessage(Diagnostic.Kind.NOTE, clazz + "=======>" + className);
    
                TypeSpec.Builder builder = TypeSpec.classBuilder(className)
                        .addJavadoc(CLASS_JAVA_DOC)
                        // 注释 1
                        .addModifiers(Modifier.PUBLIC)
                        // 注释 2
                        .superclass(ClassName.get(superPackageName, superClassName))
                        // 注释 3
                        .addSuperinterface(ClassName.get(PACKAGE_NAME, "MultiStatusEvent"))
                        //注释 4
                        .addField(ClassName.get(PACKAGE_NAME, "MultiStatusHelper"), "mMultiStatusHelper", Modifier.PRIVATE);
                //生成方法的具体操作
                generateMethod(builder, clazz, provider);
    
                JavaFile javaFile = JavaFile.builder(PACKAGE_NAME, builder.build()).build();
                javaFile.writeTo(mFilter);
            }
        }
    

    1.生成类是public
    2.定义生成类的全路径包名,类名
    3.生成的类实现MultiStatusEvent接口
    4.生成类添加成员变量mMultiStatusHelper

      private void constructor(TypeSpec.Builder builder, String clazz, String providerClassPath) {
            TypeName contextType = ClassName.get("android.content", "Context");
            TypeName attributeSetType = ClassName.get("android.util", "AttributeSet");
            MethodSpec constructorOne = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(contextType, "context")
                    .addStatement("this(context,null)")
                    .build();
            MethodSpec constructorTwo = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(contextType, "context")
                    .addParameter(attributeSetType, "attrs")
                    .addStatement("this(context,attrs,0)")
                    .build();
            MethodSpec constructorThree = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(contextType, "context")
                    .addParameter(attributeSetType, "attrs")
                    .addParameter(TypeName.INT, "defStyleAttr")
                    .addStatement("super(context,attrs,defStyleAttr)")
                    .addStatement("mMultiStatusHelper = new MultiStatusHelper(context,attrs,defStyleAttr,this)")
                    //注释1
                    .addStatement("generateProviderClass($S)", providerClassPath)
                    .build();
            builder.addMethod(constructorOne)
                    .addMethod(constructorTwo)
                    .addMethod(constructorThree);
        }
    

    上面代码片段是生成类构造器,因为生成的类也是Layout,所以需要构造View的基本构造器,最终还是交由MultiStatusHelper中处理。
    注释1:providerClassPath为,实现ViewConstraintProvider接口的全路径,利用generateProviderClass()方法生成相应的class,然后供后续使用。

     private void setViewConstraintProviderClass(TypeSpec.Builder builder, String className, String providerClassPath) {
            MethodSpec methodSpec = MethodSpec.methodBuilder("generateProviderClass")
                    .addModifiers(Modifier.PRIVATE)
                    .addParameter(String.class, "providerClassPath")
                    .beginControlFlow("if(providerClassPath == null)")
                    .addStatement("return")
                    .endControlFlow()
                    .beginControlFlow("try")
                    .addStatement("$T providerClass = $T.forName(providerClassPath)", Class.class, Class.class)
                    .addStatement("mMultiStatusHelper.setViewConstraintProvider(providerClass)")
                    .addStatement("} catch ($T e) { \n e.printStackTrace()", ClassNotFoundException.class)
                    .endControlFlow()
                    .build();
            builder.addMethod(methodSpec);
        }
    

    上面代码片段是生成generateProviderClass(String providerClassPath)的代码。
    最终编译生成的java代码如下:

    private void generateProviderClass(String providerClassPath) {
        if(providerClassPath == null) {
          return;
        }
        try {
          Class providerClass = Class.forName(providerClassPath);
          mMultiStatusHelper.setViewConstraintProvider(providerClass);
          } catch (ClassNotFoundException e) { 
               e.printStackTrace();
        }
      }
    

    终于哔哔哔哔完了,太难了!!!您只看到我扣在屏幕上的字,却看不到我滴在键盘上的泪(´༎ຶД༎ຶ`)。
    如果有什么问题。请发送邮件至pittleeeeee@gmail.com

    相关文章

      网友评论

          本文标题:MultiStatusLayout

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