Android skinning principle and Android-Skin-Loader framework analysis

Android skinning principle and Android-Skin-Loader framework analysis

Android skinning technology has been a mature technology for a long time, but I only saw it recently when I was learning and getting in touch with hot fixes. After reading some skinning methods and analyzing and summarizing the source code of the Android-Skin-Loader skinning framework that is more recognized on the market, I once again recorded it to commemorate the time I have passed.

Skin Change Introduction

Skinning is essentially a replacement of resources, including fonts, colors, backgrounds, images, sizes, etc. Of course, we have mature APIs that can be used to control code logic. For example, View changes the background color setBackgroundColor, TextView changes the font setTextSize, etc. But as programmers, how can we tolerate skinning every element of every page line by line of code? We need to use the least code to implement the easiest to maintain and the best (dynamic switching, immediate effect) skinning framework.

Skin changing method 1: switch to theme

Use the same resource ID, but customize different resources under different Themes. We switch the resources used when creating interface elements by actively switching to different Themes. This solution does not require much code, and has an obvious disadvantage that it does not support skinning of already created interfaces, and interface elements must be reloaded. GitHub Demo

Skin changing method 2: Load resource pack

Loading resource packages is a skinning method used by various applications, such as the most commonly used input method skins, browser skins, etc. We can put the skin resource files into the installation package, or download and cache them to the disk. Android applications can use this method to change skins. There is a very high-start skinning framework Android-Skin-Loader on GitHub, which changes the skin of the app by loading resource packages. The analysis of this framework is also the main content of this article.

By comparison, we can find that switching Theme can make a small skinning setting (such as the theme of a custom component), and if we want to switch the theme of the entire app, then loading the resource package is currently the better way.

Android skinning knowledge points

Skinning API

Let's first look at some basic APIs provided by Android. By using these APIs, we can replace resource objects within the App.

  1. public class Resources{
  2. public String getString( int id)throws NotFoundException {
  3. CharSequence res = mAssets.getResourceText(id);
  4. if (res != null ) {
  5. return res;
  6. }
  7. throw new NotFoundException( "String resource ID #0x"  
  8. + Integer .toHexString(id));
  9. }
  10. public Drawable getDrawable( int id)throws NotFoundException {
  11. /********Part of the code is omitted*******/
  12. }
  13. public   int getColor( int id)throws NotFoundException {{
  14. /********Part of the code is omitted*******/
  15. }
  16. /********Part of the code is omitted*******/
  17. }

This is the api of Resources class that we often use. We can usually use @+id String type defined in resource file, and then generate id (int type) of corresponding resource file in compiled R.java, so as to call these apis provided by Resources through this id (int type) to get the corresponding resource object. This is no problem in the same app, but how can we get this id value in skin package?

  1. public class Resources{
  2. /********Part of the code is omitted*******/
  3. /**
  4. * Returns a resource identifier given a resource name.
  5. *@paramname describes the name of the resource
  6. *@paramdefType resource type
  7. *@paramdefPackage package name
  8. *
  9. *@ return Returns the resource id, 0 indicates that the resource was not found
  10. */
  11. public   int getIdentifier(String name , String defType, String defPackage){
  12. if ( name == null ) {
  13. throw new NullPointerException( "name is null" );
  14. }
  15. try {
  16. return   Integer .parseInt( name );
  17. } catch (Exception e) {
  18. // Ignore  
  19. }
  20. return mAssets.getResourceIdentifier( name , defType, defPackage);
  21. }
  22. }

Resources provides the ability to use @+id, Type, and PackageName to find out whether there is an id of the corresponding PackageName in the AssetManager whose type and id value can correspond to the parameters, and return it. Then we can use this id to call the Resource API to get the corresponding resource.

One thing we need to pay attention to here is that the mAssets object of the Resources object called by the getIdentifier(String name, String defType, String defPackage) method and the getString(int id) method must be the same and contain the resource package PackageName.

AssetManager Construction

How to construct an AssetManager object instance containing a specific packageName resource?

  1. public final class AssetManagerimplements AutoCloseable{
  2. /********Part of the code is omitted*******/
  3. /**
  4. * Create a new AssetManager containing only the basic system assets.
  5. * Applications will not generally use this method, instead retrieving the
  6. * appropriate asset manager with {@linkResources#getAssets}. Not   for  
  7. * use by applications.
  8. * {@hide}
  9. */
  10. public AssetManager(){
  11. synchronized (this) {
  12. if (DEBUG_REFS) {
  13. mNumRefs = 0;
  14. incRefsLocked(this.hashCode());
  15. }
  16. init( false );
  17. if (localLOGV) Log.v(TAG, "New asset manager: " + this);
  18. ensureSystemAssets();
  19. }
  20. }

From the constructor of AssetManager, we can see that there is a {@hide}, so in other classes, the AssetManager instance is created directly. But don't forget that there is also a reflection mechanism in Java that can create class objects.

  1. AssetManager assetManager = AssetManager.class.newInstance();

How to make the created assetManager contain resource information of a specific PackageName? We can find the corresponding API in AssetManager and call it.

  1. public final class AssetManagerimplements AutoCloseable{
  2. /********Part of the code is omitted*******/
  3. /**
  4. * Add an additional set   of assets to the asset manager. This can be
  5. * either a directory or ZIP file. Not   for use by applications. Returns  
  6. * the cookie of the added asset, or 0 on failure.
  7. * {@hide}
  8. */
  9. public final int addAssetPath(String path){
  10. synchronized (this) {
  11. int res = addAssetPathNative(path);
  12. if (mStringBlocks != null ) {
  13. makeStringBlocks(mStringBlocks);
  14. }
  15. return res;
  16. }
  17. }
  18. }

Similarly, this method does not support external calls, we can only call it through reflection.

  1. /**
  2. * apk path
  3. */
  4. String apkPath = Environment.getExternalStorageDirectory()+ "/skin.apk" ;
  5. AssetManager assetManager = null ;
  6. try {
  7. AssetManager assetManager = AssetManager.class.newInstance();
  8. AssetManager.class.getDeclaredMethod( "addAssetPath" , String.class).invoke(assetManager, apkPath);
  9. } catch (Throwable th) {
  10. th.printStackTrace();
  11. }

At this point we can construct our own skinning Resources.

Skinning Resources Structure

  1. public Resources getSkinResources(Context context){
  2. /**
  3. * Plugin apk path
  4. */
  5. String apkPath = Environment.getExternalStorageDirectory()+ "/skin.apk" ;
  6. AssetManager assetManager = null ;
  7. try {
  8. AssetManager assetManager = AssetManager.class.newInstance();
  9. AssetManager.class.getDeclaredMethod( "addAssetPath" , String.class).invoke(assetManager, apkPath);
  10. } catch (Throwable th) {
  11. th.printStackTrace();
  12. }
  13. return new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
  14. }

Use resource skinning in resource pack

We can combine all the above codes together to achieve the skinning of the app using the resources in the resource package.

  1. public Resources getSkinResources(Context context){
  2. /**
  3. * Plugin apk path
  4. */
  5. String apkPath = Environment.getExternalStorageDirectory()+ "/skin.apk" ;
  6. AssetManager assetManager = null ;
  7. try {
  8. AssetManager assetManager = AssetManager.class.newInstance();
  9. AssetManager.class.getDeclaredMethod( "addAssetPath" , String.class).invoke(assetManager, apkPath);
  10. } catch (Throwable th) {
  11. th.printStackTrace();
  12. }
  13. return new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
  14. }
  15. @Override
  16. protected void onCreate(Bundle savedInstanceState){
  17. super.onCreate(savedInstanceState);
  18. setContentView(R.layout.activity_main);
  19. ImageView imageView = (ImageView) findViewById(R.id.imageView);
  20. TextView textView = (TextView) findViewById(R.id.text);
  21. /**
  22. * Plugin resource object
  23. */
  24. Resources resources = getSkinResources(this);
  25. /**
  26. * Get image resources
  27. */
  28. Drawable drawable = resources.getDrawable(resources.getIdentifier( "night_icon" , "drawable" , "com.tzx.skin" ));
  29. /**
  30. * Get text resources
  31. */
  32. int color = resources.getColor(resources.getIdentifier( "night_color" , "color" , "com.tzx.skin" ));
  33.  
  34. imageView.setImageDrawable(drawable);
  35. textView.setText(text);
  36.  
  37. }

Through the above introduction, we can simply change the skin of the current page. However, if we want to make a mature skinning framework, then this alone is not enough. Let's improve our thinking level. If we directly use the resource files in the skin resource package when creating the View, then this will undoubtedly make the skinning easier to maintain.

LayoutInflater.Factory

If you have read my previous article about LayoutInflater&Factory, you can skip this part.

Fortunately, Android provides us with a way to make modifications when View is produced.

  1. public abstract class LayoutInflater{
  2. /***Part of the code is omitted****/
  3. public interface Factory {
  4. public   View onCreateView(String name , Context context, AttributeSet attrs);
  5. }
  6.  
  7. public interface Factory2extends Factory{
  8. public   View onCreateView( View parent, String name , Context context, AttributeSet attrs);
  9. }
  10. /***Part of the code is omitted****/
  11. }

We can set the Factory for the Window object of the current page when creating it, so when the View in the Window is created, it will be created first through the Factory that we set. For the usage of Factory and related precautions, please move to Meet LayoutInflater&Factory, which contains all the relevant knowledge points about Factory.

Android-Skin-Loader Analysis

initialization

Initialize the skinning framework and import the resource package that needs to be skinned (currently an apk file containing only resource files).

  1. public class SkinApplicationextends Application{
  2. public void onCreate(){
  3. super.onCreate();
  4. initSkinLoader();
  5. }
  6. /**
  7. * Must call init first  
  8. */
  9. private void initSkinLoader(){
  10. SkinManager.getInstance().init(this);
  11. SkinManager.getInstance(). load ();
  12. }
  13. }

Constructing a skinning object

Import the resource package that needs to be skinned and construct a skinned Resources instance.

  1. /**
  2. * Load resources from apk in asyc task
  3. *@paramskinPackagePath path of skin apk
  4. *@paramcallback callback to notify user  
  5. */
  6. public void load (String skinPackagePath,final ILoaderListener callback){
  7.      
  8. new AsyncTask<String, Void, Resources>() {
  9.  
  10. protected void onPreExecute(){
  11. if (callback != null ) {
  12. callback.onStart();
  13. }
  14. };
  15.  
  16. @Override
  17. protected Resources doInBackground(String... params){
  18. try {
  19. if (params. length == 1) {
  20. String skinPkgPath = params[0];
  21.                      
  22. File file = new File(skinPkgPath);
  23. if(file == null || !file.exists()){
  24. return   null ;
  25. }
  26.                      
  27. PackageManager mPm = context.getPackageManager();
  28. // Retrieve an installation package file outside the program
  29. PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
  30. //Get the installation package registration
  31. skinPackageName = mInfo.packageName;
  32. //Build a skin-changing AssetManager instance
  33. AssetManager assetManager = AssetManager.class.newInstance();
  34. Method addAssetPath = assetManager.getClass().getMethod( "addAssetPath" , String.class);
  35. addAssetPath.invoke(assetManager, skinPkgPath);
  36. //Build a skinning Resources instance
  37. Resources superRes = context.getResources();
  38. Resources skinResource = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());
  39. //Store the current skin path
  40. SkinConfig.saveSkinPath(context, skinPkgPath);
  41.                      
  42. skinPath = skinPkgPath;
  43. isDefaultSkin = false ;
  44. return skinResource;
  45. }
  46. return   null ;
  47. } catch (Exception e) {
  48. e.printStackTrace();
  49. return   null ;
  50. }
  51. };
  52.  
  53. protected void onPostExecute(Resources result){
  54. mResources = result;
  55.  
  56. if (mResources != null ) {
  57. if (callback != null ) callback.onSuccess();
  58. //Update the skinnable interface
  59. notifySkinUpdate();
  60. } else {
  61. isDefaultSkin = true ;
  62. if (callback != null ) callback.onFailed();
  63. }
  64. };
  65.  
  66. } .execute (skinPackagePath);
  67. }

Defining the base class

The common code of the base class of the skinning page implements the basic skinning function.

  1. public class BaseFragmentActivityextends FragmentActivityimplements ISkinUpdate,IDynamicNewView{
  2.      
  3. /***Part of the code is omitted****/
  4.      
  5. //Customize LayoutInflater.Factory
  6. private SkinInflaterFactory mSkinInflaterFactory;
  7.      
  8. @Override
  9. protected void onCreate(Bundle savedInstanceState){
  10. super.onCreate(savedInstanceState);
  11.      
  12. try {
  13. //Set LayoutInflater's mFactorySet to true , indicating that mFactory has not been set, otherwise an exception will be thrown.
  14. Field field = LayoutInflater.class.getDeclaredField( "mFactorySet" );
  15. field.setAccessible( true );
  16. field.setBoolean(getLayoutInflater(), false );
  17. //Set LayoutInflater's MFactory
  18. mSkinInflaterFactory = new SkinInflaterFactory();
  19. getLayoutInflater().setFactory(mSkinInflaterFactory);
  20.  
  21. } catch (NoSuchFieldException e) {
  22. e.printStackTrace();
  23. } catch (IllegalArgumentException e) {
  24. e.printStackTrace();
  25. } catch (IllegalAccessException e) {
  26. e.printStackTrace();
  27. }
  28.          
  29. }
  30.  
  31. @Override
  32. protected void onResume(){
  33. super.onResume();
  34. //Register skin management object
  35. SkinManager.getInstance().attach(this);
  36. }
  37.      
  38. @Override
  39. protected void onDestroy(){
  40. super.onDestroy();
  41. //Unregister skin management object
  42. SkinManager.getInstance().detach(this);
  43. }
  44. /***Part of the code is omitted****/
  45. }

SkinInflaterFactory

  • SkinInflaterFactory creates a View and changes the skin of the View.

Constructing a View

  1. public class SkinInflaterFactoryimplements Factory{
  2. /***Part of the code is omitted****/
  3. public   View onCreateView(String name , Context context, AttributeSet attrs){
  4. //Read the skin:enable property of View , false means no skin change is required
  5. // if this is   NOT enable to be skinned, simply skip it
  6. boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false );
  7. if (!isSkinEnable){
  8. return   null ;
  9. }
  10. //Create View  
  11. View   view = createView(context, name , attrs);
  12. if ( view == null ) {
  13. return   null ;
  14. }
  15. //If the View is created successfully, reskin the View
  16. parseSkinAttr(context, attrs, view );
  17. return   view ;
  18. }
  19. //Create a View . For an analogy, see the createViewFromTag method of LayoutInflater.
  20. private View createView(Context context, String name , AttributeSet attrs){
  21. View   view = null ;
  22. try {
  23. if (-1 == name .indexOf( '.' )){
  24. if ( "View" .equals( name )) {
  25. view = LayoutInflater. from (context).createView( name , "android.view." , attrs);
  26. }
  27. if ( view == null ) {
  28. view = LayoutInflater. from (context).createView( name , "android.widget." , attrs);
  29. }
  30. if ( view == null ) {
  31. view = LayoutInflater. from (context).createView( name , "android.webkit." , attrs);
  32. }
  33. } else {
  34. view = LayoutInflater. from (context).createView( name , null , attrs);
  35. }
  36.  
  37. Li( "about to create " + name );
  38.  
  39. } catch (Exception e) {
  40. Le( "error while create 【" + name + "】 : " + e.getMessage());
  41. view = null ;
  42. }
  43. return   view ;
  44. }
  45. }

Skinning the produced View

  1. public class SkinInflaterFactoryimplements Factory{
  2. //Store the View that needs to be skinned in the current Activity  
  3. private List<SkinItem> mSkinItems = new ArrayList<SkinItem>();
  4. /***Part of the code is omitted****/
  5. private void parseSkinAttr(Context context, AttributeSet attrs, View   view ){
  6. // All attribute tags of the current View
  7. List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();
  8.          
  9. for ( int i = 0; i < attrs.getAttributeCount(); i++){
  10. String attrName = attrs.getAttributeName(i);
  11. String attrValue = attrs.getAttributeValue(i);
  12.              
  13. if(!AttrFactory.isSupportedAttr(attrName)){
  14. continue ;
  15. }
  16. //Filter the value of the attribute in the view attribute tag to be a reference type
  17. if(attrValue.startsWith( "@" )){
  18. try {
  19. int id = Integer .parseInt ( attrValue.substring (1));
  20. String entryName = context.getResources().getResourceEntryName(id);
  21. String typeName = context.getResources().getResourceTypeName(id);
  22. //Construct SkinAttr instance, attrname, id, entryName, typeName
  23. //The name of the attribute (background), the id value of the attribute ( int type), the id value of the attribute (@+id, string type), the value type of the attribute (color)
  24. SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);
  25. if (mSkinAttr != null ) {
  26. viewAttrs.add (mSkinAttr) ;
  27. }
  28. } catch (NumberFormatException e) {
  29. e.printStackTrace();
  30. } catch (NotFoundException e) {
  31. e.printStackTrace();
  32. }
  33. }
  34. }
  35. //If the current View needs to be skinned, add it to mSkinItems
  36. if(!ListUtils.isEmpty(viewAttrs)){
  37. SkinItem skinItem = new SkinItem();
  38. skinItem.view = view ;
  39. skinItem.attrs = viewAttrs;
  40.  
  41. mSkinItems.add (skinItem);
  42. //Whether to use external skin for skinning
  43. if(SkinManager.getInstance().isExternalSkin()){
  44. skinItem.apply();
  45. }
  46. }
  47. }
  48. }

Resource Acquisition

Find the corresponding resource name through the current resource id, and then find the resource id corresponding to the resource name in the skin package.

  1. public class SkinManagerimplements ISkinLoader{
  2. /***Part of the code is omitted****/
  3. public   int getColor( int resId){
  4. int originColor = context.getResources().getColor(resId);
  5. //Whether the skin is not downloaded or the default skin is currently used
  6. if(mResources == null || isDefaultSkin){
  7. return originColor;
  8. }
  9. //According to the resId value, get the corresponding xml @+id String type value
  10. String resName = context.getResources().getResourceEntryName(resId);
  11. //Get the corresponding resId in mResources of the skin package according to resName
  12. int trueResId = mResources.getIdentifier(resName, "color" , skinPackageName);
  13. int trueColor = 0;
  14. try{
  15. //Get the corresponding resource value according to resId
  16. trueColor = mResources.getColor(trueResId);
  17. }catch(NotFoundException e){
  18. e.printStackTrace();
  19. trueColor = originColor;
  20. }
  21.          
  22. return trueColor;
  23. }
  24. public Drawable getDrawable( int resId){...}
  25. }

other

In addition, the following skin management APIs are added (download, listen for callbacks, apply, cancel, exception handling, extension modules, etc.).

Summarize

Changing skin is so simple~!~!

The article ends here. If you have anything else to communicate, please leave a message~!~!

<<:  It’s that simple to adapt web pages to iPhoneX

>>:  How do I implement offline caching of web pages step by step?

Recommend

User operation: a simple and easy-to-use user growth methodology

This article introduces in detail the specific im...

How to use data analysis to drive user growth?

Using data to gain insight into users and underst...

The universal formula for user growth

Liang Ning said: “Growth capability is the abilit...

Lagou Java Engineer High-Paying Training Camp 5th

Lagou Java Engineer High-Paying Training Camp 5 R...

Wedding photography advertising promotion case!

After the epidemic, the May Day holiday is approa...

Product operation strategy: in-depth analysis of cold start!

Many well-known and popular products on the marke...

Analyzing the 4 ways to use Douyin to promote accounts!

What is a Douyin account? A Douyin account that i...