The open-source framework android-skin-Loader is used here. The framework is no longer maintained and meets the basic functional requirements.

Based on using

Add the dependent

Import the library lib as a Module so that you can add functionality to your needs.

use

  • Inherit BaseActivity or BaseFragmentActivity or BaseFragment
  • Init in Application
public class YourApplication extends Application {
	public void onCreate() { super.onCreate(); // Must call init first SkinManager.getInstance().init(this); SkinManager.getInstance().load(); }}Copy the code
  • Identify the view to be skinned in the layout
/ / namespace XMLNS: skin = "http://schemas.android.com/android/skin"<TextView
     skin:enable="true" 
      />
Copy the code
  • Set the skin from the generated skin file
File skin = new File("skin path");
SkinManager.getInstance().load(skin.getAbsolutePath(),
				new ILoaderListener() {
					@Override
					public void onStart(a) {}@Override
					public void onSuccess(a) {}@Override
					public void onFailed(a) {}});Copy the code

Generate a skin file

Generate the skin file APk

Create an App Module (remember not a Library Module). The module_name/ SRC /main/ Java directory can be deleted directly. Then add the resource files you want to replace in the res directory. Remember: The resource file to be replaced must have the same name as the resource file in the main module. It is then packaged directly to generate apK files.

Copy to the main Module

Copy the APk file to a directory in the main Module, such as main_module/ SRC /main/assets.

Change the skin file name extension

To prevent the user from clicking on the skin file to install, you can change the file suffix to.skin. Or you can customize a suffix.

Generate multiple skin files

To generate multiple skin files, do the configuration directly in Gradle without creating multiple modules. Create different types of skin file directories under the skin_module/ SRC directory at the same level as main. This allows you to compile the APK for different skins.

  • Add the buildType
 android {
     buildTypes{
         bmw {
             
         }
         benz {
             
         }
         toyota {
             
         }
     }
 }

Copy the code
  • Custom Task Customizes a task by obtaining buildTypes to generate the corresponding folder.

    task createAllBuildTypeChildDir() {// Iterate through the subdirectories under main/res and generate the corresponding directories for the different buildType. Def file = new file () def file = new file ()"${project.rootDir}/skin_module/src/main/res")
      file.listFiles().each { childFile ->
         def dirName =  childFile.name
          project.extensions.each { extension ->
              extension.getByName("android").properties.each { property ->
                  if (property.key == "buildTypes") {
                      property.value.each { value ->
                          def variantName =  value["name"]
                          if (("debug"! = variantName) && ("release"! = variantName)){ def dest = new File("${project.rootDir}/skin_module/src/"+variantName+"/res",dirName)
                              if(! dest.exists()){ dest.mkdirs() } } } } } } } }Copy the code

Modify the resource files in different folders, and then compile the corresponding skin files.

Identify the view that needs to be skinned

The namespace is defined in SkinConfig and added to the layout that needs to be skinned.

public class SkinConfig {
	public  static final String  NAMESPACE = "http://schemas.android.com/android/skin";
}
Copy the code

How is this namespace used? The onCreateView method of the SkinInflaterFactory first determines whether the namespace exists in the layout

boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);
Copy the code

Only layouts that use this namespace in the layout can be skinned.

SkinInflaterFactory

SkinInflaterFactory implements Layoutinflater.Factory. This class is called before the setContentView(Layout) method to filter and modify the view we want to skin.

  • onCreateView
@Override
	public View onCreateView(String name, Context context, AttributeSet attrs) {
		// if this is NOT enable to be skined , simplly skip it 
		boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);
        if(! isSkinEnable){return null;
        }
		
		View view = createView(context, name, attrs);
		
		if (view == null) {return null;
		}
		
		parseSkinAttr(context, attrs, view);
		
		return view;
	}
Copy the code
  • createView
private View createView(Context context, String name, AttributeSet attrs) {
		View view = null;
		try {
			if (-1 == name.indexOf('. ')) {if ("View".equals(name)) {
					view = LayoutInflater.from(context).createView(name, "android.view.", attrs);
				} 
				if (view == null) {
					view = LayoutInflater.from(context).createView(name, "android.widget.", attrs);
				} 
				if (view == null) {
					view = LayoutInflater.from(context).createView(name, "android.webkit.", attrs); }}else {
	            view = LayoutInflater.from(context).createView(name, null, attrs);
	        }
		} catch (Exception e) { 
			L.e("error while create 【" + name + "]." + e.getMessage());
			view = null;
		}
		return view;
	}
Copy the code

If (-1== name.indexof (‘.’)) this statement determines whether the View in the layout contains a full pathname. TextView, Button, etc. Therefore, when generating objects for these views, you need to complete the path.

  • parseSkinAttr
private void parseSkinAttr(Context context, AttributeSet attrs, View view) {
   	List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();
   	
   	for(int i = 0; i < attrs.getAttributeCount(); i++){ String attrName = attrs.getAttributeName(i); String attrValue = attrs.getAttributeValue(i); // Determine whether the ATTR supports replacement during skin peels. It is up to the user to determine which ATTR supports skin peelsif(! AttrFactory.isSupportedAttr(attrName)){continue;
   		}
   		
   	    if(attrValue.startsWith("@"){try {// The resource id corresponding to the attrValue int ID = integer.parseint (attrvalue.substring (1)); String entryName = Context.getResources ().getResourceEntryName(id); String entryName = Context.getResources ().getResourceEntryName(id); // The attrValue corresponds to the resource type, such as color, string, drawable and so ontypeName = context.getResources().getResourceTypeName(id); SkinAttr mSkinAttr = attrFactory. get(attrName, id, entryName, id)typeName);
   				if(mSkinAttr ! = null) { viewAttrs.add(mSkinAttr); } } catch (NumberFormatException e) { e.printStackTrace(); } catch (NotFoundException e) { e.printStackTrace(); }}}if(! ListUtils.isEmpty(viewAttrs)){ SkinItem skinItem = new SkinItem(); skinItem.view = view; skinItem.attrs = viewAttrs; mSkinItems.add(skinItem);if(SkinManager.getInstance().isExternalSkin()){
   			//通过SkinAttr的实现类实现换肤功能
   			skinItem.apply();
   		}
   	}
   }
Copy the code

SkinManager

Responsible for initializing and switching skins. The method to peel is load(String path,ILoaderListener listener).

PackageManager mPm = context.getPackageManager();
PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
skinPackageName = mInfo.packageName;

AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class); addAssetPath.invoke(assetManager, skinPkgPath); Resources superRes = context.getResources(); // Reconstruct a Resource object that is skinned. Resources skinResource = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());Copy the code

When the skin package is loaded, the corresponding Resource object Resource of the skin package is reconstructed by the way. This will help us access the resources in the skin pack. To dynamically switch a resource in the main Module. For example, in my project, the logo in the middle of the QR code must be switched when the skin is changed. New methods in SkinManager.

ResId is the resource name of the image in the main Module. Such as: R.drawable.icon_app public Bitmap getLogo(int resId){ Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), resId);if(null! =bitmap&&isDefaultSkin){returnbitmap; } // Name of the resource. For example: icon_app String resName = context.getResources().getResourceEntryName(resId); // The actual id of the resource in the skin package. This is done through the Resource of the skin package. inttrueResId = mResources.getIdentifier(resName, "drawable", skinPackageName);
		bitmap = BitmapFactory.decodeResource(mResources, trueResId);
		return bitmap;
	}
Copy the code

Custom SkinAttr

The open source library doesn’t have perfect control toggling support, and ImageView doesn’t. We can inherit SkinAttr to support ImageView peels.

public class ImageAttr extends SkinAttr {
    @Override
    public void apply(View view) {
        if (view instanceof ImageView){
            if(attrName.equals(IMAGE_SRC)) { ((ImageView) view).setImageDrawable(SkinManager.getInstance().getDrawable(attrValueRefId)); }}}}Copy the code

Add ImageView support to AttrFactory.

public class AttrFactory {
	public static final String IMAGE_SRC = "src";
	public static SkinAttr get(String attrName, int attrValueRefId, String attrValueRefName, String typeName){
	SkinAttr mSkinAttr = null;
	 if (IMAGE_SRC.equals(attrName)){
	 / / generated ImageAttr
			mSkinAttr = newImageAttr(); }}... }public static boolean isSupportedAttr(String attrName){
		return BACKGROUND.equals(attrName) || TEXT_COLOR.equals(attrName)
				||LIST_SELECTOR.equals(attrName) || DIVIDER.equals(attrName)
				/ / support ImageView
				||IMAGE_SRC.equals(attrName);
	}
Copy the code