Consideration of Multi-State View Framework

In the project, we can encounter different situations, such as no-load view, load view, error view and so on. In these cases, there is also the problem of setting the position of multi-state view. All in all, it's a rather troublesome problem. In this question, let's look at what a multi-state view framework needs or how to build a multi-state view framework.

There will be no illustrations here. You must know what a multi-state view is. In projects, most of us write this multi-state view framework at the beginning of development, and then others can use it directly. The general form is:

<com.longshihan.lh.ui.StatusLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/statuslayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#555555"
tools:context="com.longshihan.testlh.MainActivity">

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World!"/>
</com.longshihan.lh.ui.StatusLayout>

But do you really understand the essence of writing like this? Here's what we're talking about.

For the convenience of the following description, it is named Status Layout, which is the main character of this article.

1. Implementation scheme

The general idea is to inherit a view group as the parent class, add different views into the parent class, and control the display of different functional views in different states. The basic principle is this, but this process can be intrusive to the original layout. How to add and modify the layout non-intrusively is described below.
- How to Choose the Appropriate View Group

First of all, we can make it clear that FrameLayout, Relative Layout and Linear Layout can all be used as the parent layout of Status Layout. Here we only look at it from the perspective of view drawing. It is well known that Relative Layout needs two measure ments when laying out, so it is not appropriate to display and hide from the view with multiple states. For both Linear Layout and FrameLayout, although they can, I prefer FrameLayout because linear layout is always limited to such complex scenarios.

  • How to deal with multi-state view

For multi-state view, we put the main logic under the main page, and the number of views in other states is uncertain, and sometimes this view does not appear, so in this case, dynamic implementation of addview and removeview is a better choice, and lazy loading of view Stub is used for view which is not easy to appear.


Only use can load. Make sure that the overlay layer of the page does not increase as much as possible.

  • Various situations

To clarify the functions to be achieved before implementing a framework:
Layout of implementation:
1. No-load layout
2. Loading Layout
3. Error Layout

Location of implementation:
1. Global substitution
2. Designated Location Replacement
3. Pop-up display

Expansion (layout style with similar functions):
1. Boot Operating Interface

  • Usage

    I believe that Glide has been used here. It's very convenient for the way Builder works.

    Glide.with(mContext)
    .load(url)
    .placeholder(R.drawable.loading_spinner)
    .crossFade()
    .into(myImageView);
    

Many people have seen this model or used it, which means it is too complicated to write. Since it's tedious, why not use tools? You can write a plug-in to convert javabean code into builder. But in Intelij, there is a built-in way to add, and in the shortcut key to setter/getter, there is one button to implement builder.


Select builder on setter template. For setting the error view:

        mStatusLayout.showView(new CustomStateOptions()
                                   .image(R.mipmap.ic_launcher)
                                   .buttonClickListener(new View.OnClickListener() {
                                       @Override
                                       public void onClick(View v) {
                                           Toast.makeText(MainActivity.this, "test", Toast.LENGTH_SHORT).show();
                                       }
                                   })
                                   .error());

Finally, we can add different views in three ways: error (),. Empty (),. Load (). Direct use when in use

  mStatusLayout.showErrorView();

That's all right.

2. Realization

In the first part, we have explained the basic points of attention in writing a multi-state framework. Here we will see how to implement it.

1. Setting the Base Point of Multi-state

We set up a multi-state builder like this:

public class CustomStateOptions {
@DrawableRes
private int imageRes;
private String message;
@IdRes
private int messgaeRes;
private String buttonText;
@IdRes
private int buttonTextRes;
@LayoutRes
private int viewRes;
private boolean isLoading;
private boolean isEmpty;
private boolean isError;
private int listenertype=Status.CONTENTCLICK;
private View.OnClickListener buttonClickListener;
//Specific implementation of builder (omitted here)

}

In this case, it is only suitable to modify the default view. For the custom view, the following section is omitted.

The method of use is shown in the picture above. Now let's talk about the realization of this:

 private void showCustomview(CustomStateOptions options) {
    if (isdialog) {

    } else if (isPositionView) {
        if (mPositionView != null) {
            ViewGroup.LayoutParams layoutParams = mPositionView.getLayoutParams();
            for (int i = 0; i < getChildCount(); i++) {
                if (getChildAt(i) == mPositionView) {
                    getChildAt(i).setTag(ViewTAG);
                    getChildAt(i).setVisibility(GONE);
                }
            }
            ViewGroup viewGroup = (ViewGroup) mPositionView.getParent();
            if (viewGroup instanceof RelativeLayout) {
                mPositionView.setVisibility(INVISIBLE);
            }
            setCustomLayoutOption(options, layoutParams);
        } else {
            for (int i = 0; i < getChildCount(); i++) {
                getChildAt(i).setTag(ViewTAG);
                getChildAt(i).setVisibility(GONE);
            }
            setCustomLayoutOption(options, null);
        }
    } else {
        for (int i = 0; i < getChildCount(); i++) {
            getChildAt(i).setTag(ViewTAG);
            getChildAt(i).setVisibility(GONE);
        }
        setCustomLayoutOption(options, null);
      }
}

As can be seen from the simple code above, the view displayed in dialog mode has not been processed yet, which may involve the problem of dependency injection, and there is no plan to deal with it for the time being. Let's look at what the last else stands for: replacing the global interface with a multi-state view, which is noteworthy: setting a tag for the internal view and hiding the view. Now it's important to note that in the case of positionview, you specify the view to be replaced. It should be noted here that the parent of positionview may be a dependent layout, such as Relative Layout, where the location of the control may require the relative or dependency of other controls, so a pit here is:


Positiononview can't go directly, only INVISIBLE. Then we got his Layout Params (because we're not sure about the type of its components, so instead of their parent class, ViewGroup, we'll use a lot of the properties and methods in ViewGroup).
The next step is to enter the middle layer, which is to determine the loading of the multi-state view.

/**
 * Middle Distribution Layout Options
 *
 * @param options
 * @param layoutParams
 */
private void setCustomLayoutOption(CustomStateOptions options, ViewGroup.LayoutParams
        layoutParams) {
    if (options.isEmpty()) {
        emptyoptions = options;
        if (options.getViewRes() != 0) {
            emptyView = inflater.inflate(options.getViewRes(), null);
        } else {
            showViewOption(options, Status.EMPTY_VIEW);
        }
        if (layoutParams != null) {
            emptyView.setLayoutParams(layoutParams);
        }
    } else if (options.isError()) {
        erroroptions = options;
        if (options.getViewRes() != 0) {
            errorView = inflater.inflate(options.getViewRes(), null);
        } else {
            showViewOption(options, Status.ERROR_VIEW);
        }
        if (layoutParams != null) {
            errorView.setLayoutParams(layoutParams);
        }

    } else if (options.isLoading()) {
        progressoptions = options;
        if (options.getViewRes() != 0) {
            progressView = inflater.inflate(options.getViewRes(), null);
        }
        if (layoutParams != null) {
            progressView.setLayoutParams(layoutParams);
        }

    }
}

The code is relatively simple, that is, pure judgment, set layoutParams, display. Write here may be asked, some controls click where, not in a hurry, now start drawing content and control click events like the view.

private void showViewOption(final CustomStateOptions options, int type) {
    if (mOnClickListener != null && mOnClickListener == null) {
        setOnClick(options.getButtonClickListener());
    }
    switch (type) {
        case Status.EMPTY_VIEW:
            if (options.getImageRes() != 0) {
                emptyImageView.setBackgroundResource(options.getImageRes());
            }
            if (!TextUtils.isEmpty(options.getMessage())) {
                emptyTextView.setText(options.getMessage());
            }
            if (options.getMessgaeRes() != 0) {
                emptyTextView.setText(options.getMessgaeRes());
            }

            switch (options.getListenertype()) {
                case Status.CONTENTCLICK:
                    emptyContentView.setOnClickListener(new OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            mOnClickListener.onClick(v);
                        }
                    });
                    break;
                case Status.TEXTCLICK:
                    emptyTextView.setOnClickListener(new OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            mOnClickListener.onClick(v);
                        }
                    });
                    break;
                case Status.IMAGECLICK:
                    emptyImageView.setOnClickListener(new OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            mOnClickListener.onClick(v);
                        }
                    });
                    break;
                case Status.BUTTONCLICK:
                    emptyTextView.setOnClickListener(new OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            mOnClickListener.onClick(v);
                        }
                    });
                    break;
                default:
                    break;
            }
            break;
    }
}

The above is just to show the drawing content on the empty view and handle click events in various situations. The other wrong views and loading views are similar in style. There is no further elaboration. The operation here is only the callback of the interface. There is no difficulty. You can see it at a glance.
The above affix statement is for the sake of the simplicity of the following usage. Let's show you the usage below.

//Show blank maps
public void showEmptyView() {
    if (emptyoptions != null) {
        showCustomview(emptyoptions);
    } else {
        showCustomview(new CustomStateOptions().empty());
    }
    showContentParentView(emptyView);
}

Run showEmptyView directly on it, and showErrorView will do. The following operations are similar.
Now that there are other view s inserted above, how to clean them up? Here's how to clean them up.

 public void cleanallView() {
    if (usedviews.size() > 0) {
        for (int i = 0; i < usedviews.size(); i++) {
            removeView(usedviews.get(i));
        }
    }
    for (int i = 0; i < getChildCount(); i++) {
        if (ViewTAG.equals(getChildAt(i).getTag())) {
            getChildAt(i).setVisibility(VISIBLE);
        }
    }
}

As you can see from the above, tag is used here, and usedviews are used to save references to View, and aspects are remove d directly. Here's a note: view.setTag(), where the parameters must be immutable, which is also used below.

2. Boot Interface

Presumably everyone has used Viewpage, I do not know if you have used ViewFlipper, you can understand it as a reduced version of the viewpage or gallery, the use is very simple, but also particularly suitable for the current scenario.
Because we don't use much of this interface, we set it up as viewStub. This interface is generally oriented to the overall layout, and clicks are corresponding to global clicks (or individual controls). Based on the above two points, I set the following operations:
Old place, first look at his builder model:

public class CustomGuildeOptions {
  private List<View> mViews;
  private Context mContext;
  private LayoutInflater mInflater;

public CustomGuildeOptions appendView(@LayoutRes int viewids) {
    View currentview = mInflater.inflate(viewids, null);
    mViews.add(currentview);
    return this;
}

public CustomGuildeOptions appendView(View view) {
    mViews.add(view);
    return this;
}

public CustomGuildeOptions appendView(@LayoutRes int viewids, @IdRes int ids) {
    View currentview = mInflater.inflate(viewids, null);
    currentview.setTag(R.id.ids,ids);
    mViews.add(currentview);
    return this;
}

public CustomGuildeOptions appendView(View view, int ids) {
    view.setTag(R.id.ids, ids);
    mViews.add(view);
    return this;
}

//Here are some good details.

Considering that there may be clicks to manipulate a single control (such as showing the next step, etc.), we need to add View and his ID together when adding, and set the specified ID in TAG, so that his click control is bound to view.
Let's see if you load VIewFilpper:

 /**
 * Setting parameters for boot layout
 *
 * @param options
 */
public void showGuildeView(final CustomGuildeOptions options) {
    if (mStubView == null) {
        mStubView = mGuideView.inflate();
        mViewFlipper = (ViewFlipper) mStubView.findViewById(R.id.glideviewflipper);
    }
    guildeoptions = options;
    if (mViewFlipper.getChildCount() == 0) {
        for (int i = 0; i < options.getViews().size(); i++) {
            View view = options.getViews().get(i);
            mViewFlipper.addView(view);
            final int finalI = i;
            View currentid;
            if (view.getTag(R.id.ids) != null) {
                try {
                    currentid = view.findViewById(Integer.valueOf(view.getTag(R.id.ids)
                                                                          .toString()));
                } catch (Exception e) {
                    currentid = view;
                }
            } else {
                currentid = view;
            }
            currentid.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (finalI == options.getViews().size() - 1) {
                        cleanallView();
                    } else {
                        mViewFlipper.showNext();
                    }
                }
            });
        }
    }
}

The logic of the code is relatively simple, using only three methods of ViewFilpper, adding view to it, getting the length, and the next step (generally without the previous step). Get the specified TAG first, determine whether it exists or not, and determine the location of its click. R.id.ids is obviously an ID, but where is the ID defined? You must see the XML configuration file ids.xml by looking at the source code of other open source projects. Perhaps at that time, you wondered why you should define an ID in this place. One of the reasons is that we defined the i.xml. DS is:

<item type="id" name="ids"/>

His method of displaying is consistent with that of displaying an empty view, without adding duplicate code. Now let's look at the usage. It's also very simple:

   mStatusLayout.showGuildeView(new CustomGuildeOptions(this)
            .appendView(R.layout.test1, R.id.testnum)
            .appendView(R.layout.test2)
            .appendView(R.layout.test3)
    );

Repeated operations like appendView above in Builder mode generally have only two functions:
1. Maintain a list internally, and use this list all the time. See the OKHTTP interceptor additions for similar code. Internally, implement an interceptor queue.
2. You reuse it, it's refreshed, and only the last one works.

3. Intrusion-Free Modified Layout

The so-called plug-in-free means that the original code is not invaded to ensure that the business code is relatively pure. The seemingly friendly non-intrusive frameworks like Aspectj (but with stuffing, you can see in build that he generates executable class files), and ButterKinfe, which relies on injection, generate an executable class file.
Now what we have to do is how to achieve an intrusion-free way:
This has to start with the drawing of window s: (this is more complicated, I don't understand it), directly speaking of the part we used: through device monitor, we can see the viewTree of the page.

You can see this android.R.id.content on the top of the whole view. Maybe some children's shoes don't believe it. Well, look at the source code, the source code always has this setting. AppCompatActivity as the observation object, enter the setContentView method, you can see


The key here is the AppCompatDelegate object, but we find that it is only an abstract class. We go in through create:

We can see that there are many objects of his implementation, so we should go into that one. We must say that it is bigger than 14, because this is what we are using. In fact, what we are looking for this time has nothing to do with all of this. Finally, we will go into the class AppCompat Delegate Impl V7 (unbelievable children's shoes can be found by themselves). We will open the class AppCompat Delegate Impl V7 to see the implementation. We will go directly into the method of setContentView and see clearly:

In this case, the implementation is android.R.id.content, which is the outermost layer of the layout that can be operated on. Through this code, we can also clearly know that all our layout is added through this form of addView. Finally, add that the parent class of AppCompat Delegate Impl V7 is AppCompat Delegate Impl Base, while AppCompat Delegate Impl Base is the abstract class AppCompat Delegate Impl Base. Now is the implementation class finally clearing up the inheritance process of Activity, and ultimately all the responses that have been configured by AppCompat Delegate ImplBase?
It's a bit out of the question. We can know from the above that we can add our multi-state layout dynamically in android.R.id.content, and then add the original layout to it, so that we can form the nesting process described in the previous stage, but this time we did not invade the original layout.
Let's take a look at what we need to do. It's also very simple. We add a property in BuildConfig to configure content. This builder is also a configuration item that can add a custom multi-state view, which is not described above.

Let's first look at the configuration id correlation:

 private void parseAttrs(Context context, AttributeSet attrs) {
    inflater = LayoutInflater.from(context);
    //Get the top-level master layout
    contentView = ((Activity) mContext).findViewById(android.R.id.content);
    emptyView = inflater.inflate(R.layout.empty, null);
    emptyContentView = emptyView.findViewById(R.id.emptycontent);
    emptyTextView = (TextView) emptyView.findViewById(R.id.emptytxt);
    emptyImageView = (ImageView) emptyView.findViewById(R.id.emptyimage);
    errorView = inflater.inflate(R.layout.error, null);
    errorContentView = errorView.findViewById(R.id.errorcontent);
    errorTextView = (TextView) errorView.findViewById(R.id.errortxt);
    errorImageView = (ImageView) errorView.findViewById(R.id.errorimage);
    progressView = inflater.inflate(R.layout.progress, null);
    mflipperview = inflater.inflate(R.layout.viewstubflipper, null);
    mGuideView = (ViewStub) mflipperview.findViewById(R.id.stubid);


    // TODO: Adding Adaptive Modifications to 2017/7/29
    TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.StatusLayout);
    String errText = typedArray.getString(R.styleable.StatusLayout_errorText);
    String progressText = typedArray.getString(R.styleable.StatusLayout_progressText);
    String emptyText = typedArray.getString(R.styleable.StatusLayout_emptyText);
    Drawable emptyDrawable = typedArray.getDrawable(R.styleable.StatusLayout_emptyDrawable);
    Drawable errorDrawable = typedArray.getDrawable(R.styleable.StatusLayout_errorDrawable);

    emptyTextView.setText(emptyText);
    errorTextView.setText(errText);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
        errorImageView.setBackground(errorDrawable);
        errorImageView.setBackground(emptyDrawable);
    }
}

Specific configuration process:

 public void setLayoutConfig(BuildConfig config) {
    if (config.mEmptyView != null) {
        this.emptyView = config.mEmptyView;
    }
    if (config.mErrorView != null) {
        this.errorView = config.mErrorView;
    }
    if (config.mLoadingErrorView != null) {
        this.progressView = config.mLoadingErrorView;
    }
    if (config.isContent) {//Explain that there is no parent layout defined in xml and that a parent layout is set in the code
        ViewGroup parentViewGroup = null;
        if (config.mContentView == null) {//Set the overlay layer on the parent layout
            parentViewGroup = (ViewGroup) contentView;
            View parentView = parentViewGroup.getChildAt(0);
            setLayoutParams(parentView.getLayoutParams());
            parentViewGroup.removeView(parentView);
            addView(parentView, 0);
            parentViewGroup.addView(this);
        } else {//Instead of setting the overlay layer at this location, replace it with the specified view
            parentViewGroup = (ViewGroup) config.mContentView.getParent();
            int index = 0;
            for (int i = 0; i < ((ViewGroup) contentView).getChildCount(); i++) {
                if (config.mContentView == ((ViewGroup) contentView).getChildAt(i)) {
                    index = i;
                    break;
                }
            }
            setLayoutParams(config.mContentView.getLayoutParams());
            if (parentViewGroup instanceof RelativeLayout) {//If it's the case of relativelayout, you need to deal with dependencies
                this.mContentView = config.mContentView;
            } else {
                parentViewGroup.removeView(config.mContentView);
                addView(config.mContentView, 0);
                parentViewGroup.addView(this, index);
            }
        }
    }
    this.isdialog = config.isDialog;
    this.isPositionView = config.isPositionView;
    this.mPositionView = config.mPositionView;
    this.isguilde = config.isguild;
}

The other code is better. The key code is

if (config.isContent) {//Explain that there is no parent layout defined in xml and that a parent layout is set in the code

After the code, we need to make two judgments, whether to put the overall view into it or replace it in the designated location, let's explain one by one below:

Because of our code habits, we use a parent layout in the XML layout, which makes it easy for us to get the overall layout view just by taking getChildAt(0) under emptyContentView (android.R.id.content). We just need to remove the layout view from emptyContentView, add it to our custom FrameLayout, and then put it into emptyContentView. We need to pay attention to the order, otherwise there will be the problem of repeated addition.
Next, for replacing the specified view, it's also important to note whether it's Relative Layout. If it's not Relative Layout, replace the view directly. If it's Relative Layout, it can only continue to perform the adaptation operation in loading multi-state view.

So far, the whole process of loading multi-state view has been finished, the code is relatively simple, according to this idea, we will certainly do better.

3. Summary

The above content is the whole process of drawing multi-state view, and there is a pity that if the dialog type view is drawn, if it is only a simple default view, it is simple, but not quite in line with the reality of our scene here, so I think about using a custom view, drawing the background color and shadow part, drawing this custom view as dialog. But there's a problem with this style. The view must be drawn on the screen. This requires a higher judgment on the configuration initialization view (replacement global and replacement specified) and is more complex logically. I didn't think of a good way to implement it (such as dynamically loading dialog Fragment, or relying on injection, which is actually problematic). If you have any good ideas or suggestions on the above ideas and code, please leave a message and make progress together.

Keywords: Android xml OkHttp Fragment

Added by flash-genie on Wed, 05 Jun 2019 22:15:25 +0300