android Plug-in Theme Solution (Part I -- Use of Layout InflateFactory)

Keywords: xml Android REST

android Plug-in Theme Solution (Part I - Use of Layout InflateFactory)

Label (Space Separation): android theme skin plug-in

"Make it like Netease Music!"
The product manager's requirement is that the theme skin can be replaced like Netease Music. Of course, many app s have skin switching. The product manager also clearly indicates that the background has the skin theme management ability, so this function must be plugged-in and can not be simply realized by compiling multiple sets of value s in the resource file.
Let's do it step by step.

First, regardless of plug-in, let's see how to quickly replace view resources without changing the xml you've written.

What does setContentView() do?
We know that when the default onCreate() is executed, the setContentView(int id) method is immediately called to bind the layout resource to activity. Let's see what setContentView does.

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

This is the setContentView() method in the Activity class, so let's move on.

@Override  
public void setContentView(int layoutResID) {  
   if (mContentParent == null) {  
       installDecor();  
   } else {  
       mContentParent.removeAllViews();  
   }  
   mLayoutInflater.inflate(layoutResID, mContentParent);  
   final Callback cb = getCallback();  
   if (cb != null && !isDestroyed()) {  
       cb.onContentChanged();  
   } 
}  

Here we see the familiar inflatable class, we can not create View without Inflate, so what does it do when creating View?

   public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
   ....
       final View temp = createViewFromTag(root, name, inflaterContext, attrs);
       ...
}

We found this way to create view

 View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
        boolean ignoreThemeAttr) {
        ...
          View view;
        if (mFactory2 != null) {
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if (mFactory != null) {
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null;
        }
        if (view == null && mPrivateFactory != null) {
            view = mPrivateFactory.onCreateView(parent, name, context, attrs);
        }
        ...
        }

Ultimately, our view is created through factory.
And this fact is the tool we need to focus on this time.

   /**
 * Used with {@code LayoutInflaterCompat.setFactory()}. Offers the same API as
 * {@code LayoutInflater.Factory2}.
 */
public interface LayoutInflaterFactory {
    /**
     * Hook you can supply that is called when inflating from a LayoutInflater.
     * You can use this to customize the tag names available in your XML
     * layout files.
     *
     * @param parent The parent that the created view will be placed
     * in; <em>note that this may be null</em>.
     * @param name Tag name to be inflated.
     * @param context The context the view is being created in.
     * @param attrs Inflation attributes as specified in XML file.
     *
     * @return View Newly created view. Return null for the default
     *         behavior.
     */
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs);}

LayoutInflateFactory is an interface with only one method. We focus on annotations.

Provides you with a hook (which can be understood as listening) to generate the corresponding View object for returning when inflate with the name as a comparison.

So, the whole process of inflate is basically that XML Parser parses xml, then passes it to Factory to produce the corresponding View class according to its name, and then returns to the upper layer to add it. Well, we can implement this interface and set various attributes to the generated view in onCreate(), so that we can write fixed resources in XML dynamically. Change the properties of view.

Custom Factory

 @Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {

    boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);
    AppCompatDelegate delegate = mAppCompatActivity.getDelegate();
    View view = delegate.createView(parent, name, context, attrs);
    if (view instanceof TextView && SkinConfig.isCanChangeFont()) {
        TextViewRepository.add(mAppCompatActivity, (TextView) view);
    }

    if (isSkinEnable || SkinConfig.isGlobalSkinApply()) {
        if (view == null) {
            view = ViewProducer.createViewFromTag(context, name, attrs);
        }
        if (view == null) {
            return null;
        }
        parseSkinAttr(context, attrs, view);
    }
    return view;
}

Here is the isSkinEnable we use to mark the view in xml that needs to change skin (custom namespace). Next, we judge whether the TextView part is used to change fonts. Let's leave it alone. The rest of the ways to create the View are the same as the source code. After creating the View, we use the parseSkinAttr() method to process the view. Of course, before we need to determine whether the view needs to be switched.
In this method, we can process the view, change the background, change the src of the imageView, and so on.
waring
This factory is only used to change the theme when the interface is initialized (such as opening an activity). After the interface is drawn, we need to change the attributes of all views in the interface, and call other methods. So in order to mark those views that need to be changed, we can define a set in this method and save it at the time of initialization for convenience. Continued processing.

  SkinItem skinItem = new SkinItem();
        skinItem.view = view;
        skinItem.attrs = viewAttrs;
        mSkinItemMap.put(skinItem.view, skinItem);

SknItem is the encapsulated object of view and its properties that need to be changed.
Replace the factory of activity with a custom factory

  @Override
protected void onCreate(Bundle savedInstanceState) {
    mSkinInflaterFactory = new SkinInflaterFactory();
    mSkinInflaterFactory.setAppCompatActivity(this);
    LayoutInflaterCompat.setFactory(getLayoutInflater(), mSkinInflaterFactory);
    super.onCreate(savedInstanceState);
    changeStatusColor();
}

Here I suggest writing a baseActivity that binds activity to Factory before super.onCreate.

In this way, we can control the properties of the view when creating the view after each activity starts, and after we save these views, we can also accurately find the view that needs to be changed and make corresponding modifications under the condition that the activity has been created.

Posted by nemonoman on Sat, 01 Jun 2019 11:21:51 -0700