Andorid skinning framework AndSkin source code analysis and advantages and disadvantages

AndSkin introduction and tutorial

AndSkin gaybug: https://github.com/RrtoyewxXu/andSkin

Instructions written by the author of AndSkin : http://blog.csdn.net/zhi184816/article/details/53436761

AndSkin source code analysis

initialization

BaseSkinApplication analysis

There is actually just one line of initialization code in BaseSkinApplication: SkinLoader.getDefault().init(this). The SkinLoader.getDefault()singleton is used to return the SkinLoader object, and nothing is done in the middle, so this part of the code is omitted.

    public void init(Context context) {
        mLoadSkinDeliver = new LoadSkinDeliver();
        DataManager.getDefault().init(context, mLoadSkinDeliver);

        String pluginAPKPackageName = DataManager.getDefault().getPluginPackageName();
        String pluginAPKPath = DataManager.getDefault().getPluginPath();
        String pluginAPKSuffix = DataManager.getDefault().getResourceSuffix();

        GlobalManager.getDefault().init(context, pluginAPKPackageName, pluginAPKPath, pluginAPKSuffix);
        ResourceManager.getDefault().init(pluginAPKPackageName, pluginAPKPath, pluginAPKSuffix, mLoadSkinDeliver);
    }

The initialization code is basically here. Many objects initialized here will be used later, so this method and various objects generated in the middle will be explained in more detail below.

LoadSkinDeliver is an inner class of SkinLoader, inherited from IDeliver, and there is a Handler inside that obtains the main thread looper, which is responsible for message distribution. In the subsequent skinning operation message distribution, LoadSkinDeliver is responsible for notifying each View of each interface.

    private class LoadSkinDeliver implements IDeliver {
        private Handler mHandler = new Handler(Looper.getMainLooper());
        ...
    }

DataManager implements the ILoadSkin interface. In fact, the function of this class is to save the suffix (suffix) of the current skin in the way of SP. For dynamic skinning, it also saves the plugin package package name (plugin_package_name) and the plugin package path (plugin_path). The role is equivalent to the encapsulation class of SP, I believe readers can see through it quickly. It will not be described in detail here.

The GlobalManager class is a JavaBean that stores ApplicationContext, PackageName, PluginAPKPackageName, PluginAPKPath and ResourceSuffix properties in memory. Other than that, nothing works.

ResourceManager is more important and the essence of dynamic skinning. ResourceManager implements the ILoadSkin interface and holds a reference to a Resource.

public class ResourceManager implements ILoadSkin {
    private Resource mResource;
    private IDeliver mIDeliver;
    ...
    void init(String pluginPackageName, String pluginPath, String pluginSuffix, IDeliver deliver) {
        mIDeliver = deliver;
        smartCreateResource(pluginPackageName, pluginPath, pluginSuffix, true);
    }

    private boolean smartCreateResource(String pluginPackageName, String pluginPath, String suffix, boolean firstInit) {
        boolean shouldCreate = checkIfReCreateDateResource(pluginPackageName, pluginPath, suffix);
        if (shouldCreate) {
            try {
                createDataResource(pluginPackageName, pluginPath, suffix);
                mIDeliver.postResourceManagerLoadSuccess(firstInit, pluginPackageName, pluginPath, suffix);
            } catch (Exception e) {
                e.printStackTrace();
                mIDeliver.postResourceManagerLoadError(firstInit);
            }
        } else {
            mResource.changeResourceSuffix(suffix);
            mIDeliver.postResourceManagerLoadSuccess(false, pluginPackageName, pluginPath, suffix);
        }

        return mResource != null;
    }
}

In the smartCreateResource method, it will firstly judge whether the Resource object needs to be regenerated according to pluginPackageName, pluginPath, suffix. Here, the corresponding LocalResource or PluginResource object is generated according to local skinning or dynamic skinning. This is just initialization, and we don't need to go into depth for the time being. This part will be analyzed in detail in the subsequent skinning chapters. This is the first instantiation, mResource==null, that is, shouldCreate is true.

    private void createDataResource(String pluginPackageName, String pluginPath, String suffix) throws Exception {
        mResource = ResourceFactory.newInstance().createResource(pluginPackageName, pluginPath, suffix, mIDeliver);
    }
public abstract class ResourceFactory {
    private ResourceFactory() {
    }

    public static ResourceFactory newInstance() {
        return new ResourceFactoryImp();
    }

    public abstract Resource createResource(String pluginPackageName, String pluginPath, String suffix, IDeliver deliver) throws Exception;


    static class ResourceFactoryImp extends ResourceFactory {
        private ResourceFactoryImp() {

        }

        @Override
        public Resource createResource(String pluginPackageName, String pluginPath, String suffix, IDeliver deliver) throws Exception {
            String packageName = GlobalManager.getDefault().getPackageName();
            Context context = GlobalManager.getDefault().getApplicationContext();

            if (!TextUtils.isEmpty(pluginPackageName) && !pluginPackageName.equals(packageName)) {
                return new PluginResource(context, pluginPackageName, pluginPath, suffix);
            }
            return new LocalResource(context, pluginPackageName, pluginPath, suffix);
        }
    }

}

The mResource attribute will be initialized here, and the generated logic is also very simple, which is to determine whether it is dynamic skinning based on the registration. If not, return PluginResource, otherwise return LocalResource. Both classes inherit from Resource and rewrite some methods. The difference is that the AssetManager of the dynamic plugin (.apk) is passed in when the PluginResource is instantiated. This AssetManager is built using the path of the dynamic plugin. This part will still be analyzed in the skinning chapter, and now go back to ResourceManager#smartCreateResource. After obtaining the Resource, call mIDeliver.postResourceManagerLoadSuccessthe dispatch message to the listener to tell the listener that it has initialized itself. The code is reflected as follows:

            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    if (firstInit && mOnInitLoadSkinResourceListener != null) {
                        mOnInitLoadSkinResourceListener.onInitResourceSuccess();
                    } else {
                        boolean findResourceSuccess = notifyAllChangeSkinObserverListToFindResource();

                        if (findResourceSuccess) {
                            postGetAllResourceSuccessOnMainThread(pluginPackageName, pluginPath, resourceSuffix);
                        } else {
                            postGetResourceErrorOnMainThread();
                        }
                    }
                }
            });

skinning

Skinning The Activity that needs to be skinned inherits from BaseSkinActivity. BaseSkinActivity inherits from AppCompatActivity and implements the IChangeSkin interface. The core code is as follows:

public class BaseSkinActivity extends AppCompatActivity implements IChangeSkin {
    protected BaseSkinActivity mActivity;
    private SkinLayoutInflater mSkinLayoutInflater;
    ...

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        mActivity = this;
        if (shouldRegister()) {
            mSkinLayoutInflater = new SkinLayoutInflater(this);
        }
        super.onCreate(savedInstanceState);
    }

    @Override
    public void setContentView(@LayoutRes int layoutResID) {
        super.setContentView(layoutResID);
        if (shouldRegister()) {
            findLayoutInflaterSkinViews();
            generateStatusBarIfShould();
            SkinLoader.getDefault().register(mActivity);
        }
    }

Since it LayoutInflaterCompat.setFactoryis only valid when it is called for the first time, AppCompatActivity#installViewFactory (which is called in onCreate, and LayoutInflaterCompat.setFactory is called in the method) is called in advance before being called LayoutInflaterCompat.setFactory. The code is reflected as:

public class SkinLayoutInflater {
    ...
    public SkinLayoutInflater(BaseSkinActivity baseSkinActivity) {
        this.mBaseSkinActivity = baseSkinActivity;
        mSkinInflaterFactory = new SkinInflaterFactory();

        mDynamicAddSkinViewList = new ArrayList<>();
        mLayoutInflaterSkinViewList = new ArrayList<>();

        LayoutInflaterCompat.setFactory(mBaseSkinActivity.getLayoutInflater(), mSkinInflaterFactory);
    }
    ...
}

The SkinInflaterFactory here implements the LayoutInflaterFactory interface. In this way, in the page that inherits BaseSkinActivity, the parsing work of View from XML to View is handed over to SkinInflaterFactory.

Let's go back to BaseSkinActivity#setContentView. super.setContentView(layoutResID)The findLayoutInflaterSkinViews method is called after the call. Before the View changes from XML to View, SkinInflaterFactory#onCreateView is called. This part of the logic is reflected in LayoutInflater#createViewFromTag of the framework layer. If you are interested, you can check my other blog post Android XML layout file parsing process source code parsing .

There is a property mSkinViewList in SkinInflaterFactory, which saves all the ids of Views that need to be skinned. Next, we will analyze how to obtain these IDs, which is also a core difficulty of the skinning framework.

public class SkinInflaterFactory implements LayoutInflaterFactory {
    private List<SkinView> mSkinViewList = new ArrayList<>();


    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        boolean isSkinEnable = attrs.getAttributeBooleanValue(ConfigConstants.SKIN_NAMES_SPACE, ConfigConstants.ATTR_SKIN_ENABLE, false);
        String attrList = attrs.getAttributeValue(ConfigConstants.SKIN_NAMES_SPACE, ConfigConstants.ATTR_SKIN_LIST);
        if (isSkinEnable) {
            try {
                if (TextUtils.isEmpty(attrList)) {
                    parseSkinAttr(context, attrs, name);
                } else {
                    attrList = attrList.trim();
                    parseSkinAttrByAttrList(context, attrs, attrList, name);
                }
            } catch (Exception e) {
                e.printStackTrace();
                SkinL.e("解析xml文件失败,请检查xml文件");
            }
        }
        return null;
    }
    ...
}

Let's first look at the approximate logic:

SKIN_NAMES_SPACE is a constant: http://schemas.android.com/android/andSkin .
ATTR_SKIN_ENABLE is constant: enable
ATTR_SKIN_LIST is constant: attrs

First check if the View has the enable property set. If not, no skinning is required. If so, then determine which properties need to be skinned. All properties and corresponding values ​​of View can be obtained here, so the code to find and add to mSkinViewList will not be parsed one by one. It is no different from writing a list.add() normally. It should be noted that here onCreateView returns null, which means that the job of creating View is handed over to LayoutInflater#onCreateView. Another thing to note is that the id of the View is saved here, not the reference of the real View. Obviously, if null is returned, the View has not been created at this time.

Therefore, the initialization and preparation work is really completed here, and the next step is to actually call the skinning API to realize the analysis of skinning.

local skinning

Suppose I now have two sets of skins, one is called day and the other is called night. The day skin is the default skin, and the file name is probably: icon_search.png. night is the night skin, and the file name is probably icon_search_night.png. Then, the code to switch from day skin to night in the app is:

SkinLoader.getDefault().loadSkin("night");

A line of code to achieve skinning, very simple to use. follow up.

public class SkinLoader implements ILoadSkin {
    ...
    @Override
    public void loadSkin(String suffix) {
        loadSkin("", "", suffix);
    }

    @Override
    public void loadSkin(String pluginPackageName, String pluginPath, String suffix) {
        loadSkinInner(pluginPackageName, pluginPath, suffix, true);
    }

    private void loadSkinInner(String pluginPackageName, String pluginPath, String suffix, boolean needCallSkinChangeListener) {
        cancelLoadSkinTask();
        startLoadSkinTask(pluginPackageName, pluginPath, suffix, needCallSkinChangeListener);
    }

    private void startLoadSkinTask(String pluginAPKPackageName, String pluginAPKPath, String resourceSuffix, boolean needCallSkinChangeListener) {
        mLoadSkinTask = new LoadSkinTask();
        mLoadSkinTask.setNeedCallSkinChangeListener(needCallSkinChangeListener);
        mLoadSkinTask.execute(pluginAPKPackageName, pluginAPKPath, resourceSuffix);
    }
    ...
}

After a series of overloads, it enters the startLoadSkinTask method. LoadSkinTask inherits from AsyncTask. After instantiating the mLoadSkinTask property, set needCallSkinChangeListener to true. Finally, the execute() method is called, and the pluginAPKPackageName, pluginAPKPath, and resourceSuffix are passed in. Because it is a local skinning, the pluginAPKPackageName and pluginAPKPath parameters are temporarily null.

In onPreExecute(), all observers will be notified to start skinning, usually there is only one observer. Call DataManager#loadSkin in doInBackground. follow up.

public class DataManager implements ILoadSkin {
    ...
    @Override
    public void loadSkin(String pluginPackageName, String pluginPath, String suffix) {
        if (pluginPackageName != null && pluginPackageName.equals(getPluginPackageName())
                && pluginPath != null && pluginPath.equals(getPluginPath())
                && suffix != null && suffix.equals(getResourceSuffix())) {

            mDeliver.postDataManagerLoadError();
        } else {
            savePluginPackageName(pluginPackageName);
            savePluginPath(pluginPath);
            saveResourceSuffix(suffix);
            mDeliver.postDataManagerLoadSuccess(pluginPackageName, pluginPath, suffix);
        }
    }
}

The three save methods in else all save the information of the skin to be applied in the SP file, mainly suffix.

        @Override
        public void postDataManagerLoadSuccess(String pluginPackageName, String pluginPath, String resourceSuffix) {
            SkinL.d("保存本次换肤的相关信息成功");
            ResourceManager.getDefault().loadSkin(pluginPackageName, pluginPath, resourceSuffix);
        }

After saving SP information in DataManager, LoadSkinDeliver distributes messages to ResourceManager#loadSkin.

    @Override
    public void loadSkin(String pluginPackageName, String pluginPath, String suffix) {
        try {
            smartCreateResource(pluginPackageName, pluginPath, suffix, false);
        } catch (Exception e) {
            e.printStackTrace();
            mIDeliver.postResourceManagerLoadError(false);
        }
    }

The smartCreateResource method was briefly analyzed during initialization. The difference from last time is that this time the last parameter firstInit is false.

    private boolean smartCreateResource(String pluginPackageName, String pluginPath, String suffix, boolean firstInit) {
        boolean shouldCreate = checkIfReCreateDateResource(pluginPackageName, pluginPath, suffix);
        SkinL.d("should create resource : " + shouldCreate);
        if (shouldCreate) {
            try {
                createDataResource(pluginPackageName, pluginPath, suffix);
                mIDeliver.postResourceManagerLoadSuccess(firstInit, pluginPackageName, pluginPath, suffix);
            } catch (Exception e) {
                e.printStackTrace();
                mIDeliver.postResourceManagerLoadError(firstInit);
            }
        } else {
            mResource.changeResourceSuffix(suffix);
            mIDeliver.postResourceManagerLoadSuccess(false, pluginPackageName, pluginPath, suffix);
        }

        return mResource != null;
    }

Since LocalResource is created by default during initialization, shouldCreate is false here, and the logic of else is followed. mResource.changeResourceSuffix(suffix)Just simply record the suffix of night. Then continue to distribute messages by LoadSkinDeliver.

        public void postResourceManagerLoadSuccess(final boolean firstInit, final String pluginPackageName, final String pluginPath, final String resourceSuffix) {
            SkinL.d("生成Resource对象成功");

            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    if (firstInit && mOnInitLoadSkinResourceListener != null) {
                        mOnInitLoadSkinResourceListener.onInitResourceSuccess();
                    } else {
                        boolean findResourceSuccess = notifyAllChangeSkinObserverListToFindResource();

                        if (findResourceSuccess) {
                            postGetAllResourceSuccessOnMainThread(pluginPackageName, pluginPath, resourceSuffix);
                        } else {
                            postGetResourceErrorOnMainThread();
                        }
                    }
                }
            });
        }

This time firstInit is false. Finally got to go to the logic of skinning. . .

    private boolean notifyAllChangeSkinObserverListToFindResource() {
        boolean findResourceSuccess = true;
        SkinL.d("通知所有的观察者查找资源");
        for (IChangeSkin changeSkin : mChangeSkinObserverList) {
            findResourceSuccess = changeSkin.findResource();
            if (!findResourceSuccess) {
                break;
            }
        }
        return findResourceSuccess;
    }

        @Override
        public void postGetAllResourceSuccessOnMainThread(String pluginPackageName, String pluginPath, String resourceSuffix) {
            SkinL.d("查找所有资源成功");
            GlobalManager.getDefault().flushPluginInfos(pluginPackageName, pluginPath, resourceSuffix);
            notifyAllChangeSkinObserverListToApplySKin();
        }

    private void notifyAllChangeSkinObserverListToApplySKin() {
        SkinL.d("通知所有的组件进行换肤");
        for (IChangeSkin changeSkin : mChangeSkinObserverList) {
            changeSkin.changeSkin();
        }
    }

All BaseSkinActivity objects will be added to the mChangeSkinObserverList property. That is to say, the findResource method of all BaseSkinActivity objects will be called first to find all the resources needed for skinning, and then changeSkin will be called uniformly. The logic is clear, and the two methods are directly viewed in order.

public class BaseSkinActivity extends AppCompatActivity implements IChangeSkin {
    @Override
    public boolean findResource() {

        ...
        List<SkinView> layoutInflaterSkinViewList = mSkinLayoutInflater.getLayoutInflaterSkinViewList();
        for (IChangeSkin skinView : layoutInflaterSkinViewList) {
            findResourceSuccess = skinView.findResource();
            if (!findResourceSuccess) {
                break;
            }
        }
        ...
        return findResourceSuccess;
    }

    @Override
    public void changeSkin() {
        List<SkinView> layoutInflaterSkinViewList = mSkinLayoutInflater.getLayoutInflaterSkinViewList();
        for (IChangeSkin skinView : layoutInflaterSkinViewList) {
            skinView.changeSkin();
        }
    }
}

SkinView implements the IChangeSkin interface. When executing SkinInflaterFactory#onCreateView, it adds the View of mSkinViewList as the wrapper class of the View that needs to be skinned, which stores the id of the View that needs to be skinned. When called mSkinLayoutInflater.getLayoutInflaterSkinViewList(), findViewById will be performed and the View corresponding to the id will also be stored in the SkinView. follow up.

public class SkinView implements IChangeSkin {
    @Override
    public boolean findResource() {
        boolean changed = true;
        for (BaseSkinAttr attr : mSkinAttrList) {
            changed = attr.findResource();
            if (!changed) {
                break;
            }
        }

        return changed;
    }

    @Override
    public void changeSkin() {
        for (BaseSkinAttr attr : mSkinAttrList) {
            attr.applySkin(mView);
        }
    }
}

mSkinAttrList saves all the attributes of View that need to be skinned. Currently AndSkin only supports three properties, namely "background", "src" and "TextColor". BaseSkinAttr is an abstract class with three subclasses: BackgroundAttr, SrcAttr and TextColorAttr. Here, BackgroundAttr is used as an example for analysis, and the same is true for the other two.

public class BackgroundAttr extends BaseSkinAttr {

    public BackgroundAttr(String mAttrType, String mAttrName, String mAttrValueRef) {
        super(mAttrType, mAttrName, mAttrValueRef);
    }

    @Override
    public boolean findResource() {
        resetResourceValue();

        if (TYPE_ATTR_DRAWABLE.equals(mAttrType)) {
            mFindDrawable = ResourceManager.getDefault().getDataResource().getDrawableByName(mAttrValueRef);
            return mFindDrawable != null;

        } else if (TYPE_ATTR_COLOR.equals(mAttrType)) {
            mFindColor = ResourceManager.getDefault().getDataResource().getColorByName(mAttrValueRef);
            return mFindColor != Resource.VALUE_ERROR_COLOR;

        }
        return true;
    }

    @Override
    public void applySkin(View view) {
        if (TYPE_ATTR_DRAWABLE.equals(mAttrType) && mFindDrawable != null) {
            view.setBackgroundDrawable(mFindDrawable);
            SkinL.d(view + " : " + mAttrName + " apply " + mAttrValueRef);

        } else if (TYPE_ATTR_COLOR.equals(mAttrType) && mFindColor != Resource.VALUE_ERROR_COLOR) {
            view.setBackgroundColor(mFindColor);
            SkinL.d(view + " : " + mAttrName + " apply " + mAttrValueRef);
        }

        resetResourceValue();
    }
}

ResourceManager.getDefault().getDataResource().getXXXByNameObtain mFindDrawable or mFindColor in findResource . It can be set to View in the applySkin method. At this point, we have seen the entire outline in its entirety, and the only thing that is bad now is ResourceManager.getDefault().getDataResource().getXXXByNamethe implementation details, which is the real essence of skinning.

Local skinning uses the LocalResource object.

public class LocalResource extends Resource {

    public LocalResource(Context baseSkinActivity, String pluginPackageName, String pluginPath, String resourcesSuffix) {
        super(baseSkinActivity, pluginPackageName, pluginPath, resourcesSuffix);
        mResources = baseSkinActivity.getResources();
    }

    @Override
    public Drawable getDrawableByName(String drawableResName) {
        Drawable trueDrawable = null;
        drawableResName = appendSuffix(drawableResName);
        SkinL.d("getDrawableByName drawableResName:" + drawableResName);

        try {
            int trueDrawableId = mResources.getIdentifier(drawableResName, "drawable", GlobalManager.getDefault().getPackageName());
            trueDrawable = mResources.getDrawable(trueDrawableId);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return trueDrawable;
    }
}

public abstract class Resource {
    final String appendSuffix(String name) {
        if (!TextUtils.isEmpty(mResourcesSuffix)) {
            return name + "_" + mResourcesSuffix;
        }

        return name;
    }
}

As mentioned earlier: mResource.changeResourceSuffix(suffix)just simply record the suffix of night. Now we will use the night suffix. First concatenate the name in getDrawableByName, for example: concatenate icon_search as icon_search_night. Then mResources.getIdentifierget the id of the spliced ​​resource. Then convert it to the corresponding Drawable. At this point, the local skinning process is complete.

Dynamic skinning

Compared with local skinning, dynamic skinning has the same skinning process, the only difference is that the resources of dynamic skinning are in another apk file. Local skinning is to obtain the corresponding resources through the application's Resources, so the problem that dynamic skinning needs to deal with is how to obtain the Resources object of the external apk. This is the same as plug-in loading resources, just construct the AssetManager of the external apk. The core code is as follows:

public class PluginResource extends Resource {

    public PluginResource(Context baseSkinActivity, String pluginPackageName, String pluginPath, String resourcesSuffix) throws Exception {
        super(baseSkinActivity, pluginPackageName, pluginPath, resourcesSuffix);
        loadPlugin();
    }

    private void loadPlugin() throws Exception {
        File file = new File(PATH_EXTERNAL_PLUGIN + "/" + mPluginPath);
        SkinL.d(file.getAbsolutePath());
        if (mPluginPath == null || !file.exists()) {
            throw new IllegalArgumentException("plugin skin not exit, please check");
        }

        AssetManager assetManager = null;

        assetManager = AssetManager.class.newInstance();
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
        addAssetPath.invoke(assetManager, file.getAbsolutePath());

        Resources superRes = mContext.getResources();
        mResources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
        SkinL.d("加载外部插件的皮肤成功");
    }
    ...
}

AndSkin Advantages

  1. Supports in-app skinning and dynamic skinning
  2. Do not interfere with the generation of View

AndSkin lacks/slots

Because of writing a bunch of constants in ConfigConstants. So SKIN_NAMES_SPACE in xml must be " http://schemas.android.com/android/andSkin ". Similarly, the color value of the status bar must be status_bar_color.

In order not to interfere with the generation of the View, the id of the View is stored, which means that the Views that need to be skinned in the same layout file have the id attribute set and cannot be repeated. Suppose you need to dynamically inflate 10 xmls in the code, then you have to write 10 identical xmls, but the ID of the View is different. This is very crap. . .

src loading should expose an interface so that users can use the image loading framework to load. . .

I have found so much so far, welcome to complain. .

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325568816&siteId=291194637