Detailed explanation of the implementation principle of dynamic skinning by Android architects (from source code analysis to in-depth analysis)

Detailed explanation of the implementation principle of dynamic skinning by Android architects (from source code analysis to in-depth analysis)

[[418584]]

Preface

Today we are going to talk about the principle of dynamic skin changing in app

Skin changing is divided into dynamic skin changing and static skin changing.

1. Static skinning principle

This skinning method, also known as internal skinning, is to place multiple sets of the same resources inside the APP to switch resources.

This skinning method has many disadvantages, for example, poor flexibility, only able to replace built-in resources, and too large apk size. In our application apk, general image files can account for about half of the apk size.

Of course, this method is not completely useless. For example, in our application, it is just a normal switch between day and night modes, and there is no need to change the pictures, etc., just change the color, then this method is very practical.

2. Discussion on the Principle of Dynamic Skin Change

Dynamic skinning includes replacing image resources, layout colors, fonts, text colors, status bar and navigation bar colors;

The dynamic skinning steps include:

Collect controls that need skinning

Loading a Skin Pack

Replace resources

1. What kind of file is a skin package?

Understanding by analyzing the skin package of NetEase Cloud Music

Download NetEase Cloud Music through the emulator and change the skin.

You can find our skin package in the device/data/data/com.netease.cloudmusic/files/theme directory and cp it to the computer.

Change the file format to zip and decompress it.

After the above steps we get the following files

We can see that the content of his file is exactly the same as the content format of our usual apk;

2. Explore the implementation principle

Find the answer from the SDK source code, here we only look at the main process

First, in the onCreate() method of Activity, we need to call setContentView(int id) to specify the layout file of the current Activity:

  1. public void setContentView(@LayoutRes int layoutResID) {
  2. //Call window's setContentView
  3. getWindow().setContentView(layoutResID);
  4. initWindowDecorActionBar();
  5. }

Window - PhoneWindow

  1. @Override
  2. public void setContentView( int layoutResID) {
  3. //Call LayoutInflater's inflate
  4. mLayoutInflater.inflate(layoutResID, mContentParent);
  5. }

LayoutInflater

  1. @Override
  2. public   View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
  3. // Temp   is the root view that was found in the xml to create the root layout
  4. final View   temp = createViewFromTag(root, name , inflaterContext, attrs);
  5. // Inflate all children under temp against its context. Create a sub-layout and finally call createViewFromTag
  6. rInflateChildren(parser, temp , attrs, true );
  7. }
  8. void rInflate(XmlPullParser parser, View parent, Context context, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
  9. // Loop call createViewFromTag to create sub-layouts
  10. while (((type = parser. next ()) != XmlPullParser.END_TAG ||parser.getDepth() >depth) && type != XmlPullParser.END_DOCUMENT) {
  11. final View   view = createViewFromTag(parent, name , context, attrs);
  12. }
  13. }
  14. // From this method we can see that we try to create View through various Factories  
  15. public final View tryCreateView(@Nullable View parent, @NonNull String name ,@NonNull Context context,@NonNull AttributeSet attrs) {
  16. View   view ;
  17. if (mFactory2 != null ) {
  18. view = mFactory2.onCreateView(parent, name , context, attrs);
  19. } else if (mFactory != null ) {
  20. view = mFactory.onCreateView( name , context, attrs);
  21. } else {
  22. view = null ;
  23. }
  24. if ( view == null && mPrivateFactory != null ) {
  25. view = mPrivateFactory.onCreateView(parent, name , context, attrs);
  26. }
  27. return   view ;
  28. }
  29. View createViewFromTag( View parent, String name , Context context, AttributeSet attrs,boolean ignoreThemeAttr) {
  30. // This is the more important part
  31. // Try to create a View through Factory  
  32. View   view = tryCreateView(parent, name , context, attrs);
  33. // If there is no Factory to create, then call the following method to create View  
  34. if ( view == null ) {
  35. if (-1 == name .indexOf( '.' )) {
  36. // The View provided by the system does not have a ., such as View , ImageView, TextView
  37. view = onCreateView(context, parent, name , attrs);
  38. } else {
  39. // Third-party View or custom view such as com.cbb.xxxView
  40. view = createView(context, name , null , attrs);
  41. }
  42. }
  43. }
  44. // You may be confused here. Why is only android.view . passed here? Many views are not in this package.
  45. protected View onCreateView(String name , AttributeSet attrs) throws ClassNotFoundException {
  46. // Finally, createView is called and the full name of the system view is passed in
  47. return createView( name , "android.view." , attrs);
  48. }
  49. public final View createView(@NonNull Context viewContext, @NonNull String name ,@Nullable String prefix, @Nullable AttributeSet attrs)throws ClassNotFoundException, InflateException {
  50. // Get the View construction method in the cache
  51. Constructor<? extends View > constructor = sConstructorMap.get( name );
  52. // If there is no cache, reflect and get the View construction method and cache it
  53. // It should be noted that the view construction method with two parameters is used here
  54. if (constructor == null ) {
  55. clazz = Class.forName(prefix != null ? (prefix + name ) : name , false ,
  56. mContext.getClassLoader()).asSubclass( View .class);
  57. constructor = clazz.getConstructor(mConstructorSignature);
  58. constructor.setAccessible( true );
  59. sConstructorMap.put( name , constructor);
  60. }
  61. // Use the constructor to create a view  
  62. final View   view = constructor.newInstance(args);
  63. }
  64. // sdk provides a method to set Factory
  65. public void setFactory2(Factory2 factory) {
  66. // Note that mFactorySet will be set to true if it has been set, so we need to set it to false before setting the Factory .  
  67. if (mFactorySet) {
  68. throw new IllegalStateException( "A factory has already been set on this LayoutInflater" );
  69. }
  70. mFactorySet = true ;
  71. }

From the above process analysis, we understand that from setContentView to the process of creating a view, we can see that the system will try to use Factory to create the view before creating the view, so we can also set a custom Factory to replace the system's own creation.

There is a question in the above analysis. Why is only android.view passed here? Many views are not in this package but can be successfully created? Here is a brief analysis of this process.

  1. public   static LayoutInflater from (Context context) {
  2. LayoutInflater LayoutInflater =
  3. (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  4. return LayoutInflater;
  5. }

The above is the instantiation of LayoutInflater. We can see that what is actually returned is context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

We trace back from the source code

The passed in is the Activity's context, Activity's getSystemService(String name)

ContextThemeWrapper's getSystemService(String name)

getBaseContext().getSystemService(name) in ContextWrapper

mBase returned by getBaseContext()

attachBaseContext(Context base) assignment in ContextWrapper

attachBaseContext(context) in Activity;

attachBaseContext(context) in the attach() method in Activity

Call the Activity's attach() method in the performLaunchActivity method in ActivityThread

In the performLaunchActivity method in ActivityThread, ContextImpl appContext = createBaseContextForActivity(r) instantiates Context

In ContextImpl, getSystemService(String name) calls SystemServiceRegistry.getSystemService(this, name) and returns;

Take LAYOUT_INFLATER_SERVICE to SystemServiceRegistry and find that the returned class is PhoneLayoutInflater

After the above steps, we can see that what is actually returned is the PhoneLayoutInflater class

  1. public class PhoneLayoutInflater extends LayoutInflater {
  2. private static final String[] sClassPrefixList = {
  3. "android.widget." ,
  4. "android.webkit." ,
  5. "android.app."  
  6. };
  7. @Override protected View onCreateView(String name , AttributeSet attrs) throws ClassNotFoundException {
  8. for (String prefix : sClassPrefixList) {
  9. View   view = createView( name , prefix, attrs);
  10. if ( view != null ) {
  11. return   view ;
  12. }
  13. }
  14. return super.onCreateView( name , attrs);
  15. }
  16. }

From the above PhoneLayoutInflater source code, we can see that PhoneLayoutInflater is a subclass of LayoutInflater, so it is actually under the spliced ​​three packages. If not, it is under the original view package.

The above questions have been solved

3. Implement view layout interception

Intercept the creation of system views

  1. public class SkinLayoutFactory implements LayoutInflater.Factory2 {
  2. //Package directory listing
  3. private static final String[] sClassPrefixList = {
  4. "android.widget." ,
  5. "android.webkit." ,
  6. "android.app." ,
  7. "android.view."  
  8. };
  9. // Two parameters of the view construction method
  10. private static final Class<?>[] mConstructorSignature = new Class[]{
  11. Context.class, AttributeSet.class};
  12. // The user caches the construction method obtained by reflection to prevent the subsequent repeated reflection of the same type of view
  13. private static final HashMap<String, Constructor<? extends View >> sConstructorMap =
  14. new HashMap<String, Constructor<? extends View >>();
  15. @Nullable
  16. @Override
  17. public   View onCreateView(@Nullable View parent, @NonNull String name , @NonNull Context context, @NonNull AttributeSet attrs) {
  18. // Create the view  
  19. View   view = createViewFromTag(context, name , attrs);
  20. Log.e( "Skin" , "name = " + name + " , view = " + view );
  21. return   view ;
  22. }
  23. /**
  24. * Create a view  
  25. * Determine whether to distinguish between two view types by judging whether they contain.
  26. *
  27. * @param name may be TextView or xxx.xxx.xxxView
  28. */
  29. private View createViewFromTag(Context context, String name , AttributeSet attrs) {
  30. View   view ;
  31. if (-1 == name .indexOf( '.' )) {
  32. view = createViewByPkgList(context, name , attrs);
  33. } else {
  34. view = createView(context, name , attrs);
  35. }
  36. return   view ;
  37. }
  38. /**
  39. * Try to create a view by traversing the system packages . If the previous creation fails and an exception occurs, it will be caught and then continue to try the next package name to create
  40. *
  41. * @param name may be TextView
  42. */
  43. private View createViewByPkgList(Context context, String name , AttributeSet attrs) {
  44. for (String prefix : sClassPrefixList) {
  45. try {
  46. View   view = createView(context, prefix + name , attrs);
  47. if ( view != null ) {
  48. return   view ;
  49. }
  50. } catch (Exception e) {
  51. e.printStackTrace();
  52. }
  53. }
  54. return   null ;
  55. }
  56. /**
  57. * Actually start creating the view  
  58. *
  59. * @param name   The name format is xxx.xxx.xxxView
  60. */
  61. private View createView(Context context, String name , AttributeSet attrs) {
  62. Constructor<? extends View > constructor = sConstructorMap.get( name );
  63. if ( null == constructor) {
  64. try {
  65. Class<? extends View > aClass = context.getClassLoader().loadClass( name ).asSubclass
  66. ( View .class);
  67. constructor = aClass.getConstructor(mConstructorSignature);
  68. sConstructorMap.put( name , constructor);
  69. } catch (Exception e) {
  70. }
  71. }
  72. if ( null != constructor) {
  73. try {
  74. return constructor.newInstance(context, attrs);
  75. } catch (Exception e) {
  76. }
  77. }
  78. return   null ;
  79. }
  80. }

The above code is basically the system source code of cp, so that we can create views ourselves. Now we have to set up the Factory and use the ActivityLifecycleCallbacks provided by the sdk to implement it.

  1. // The system provides the ability to monitor the entire app activity life cycle
  2. public class SkinActivityLifecycle implements Application.ActivityLifecycleCallbacks {
  3. @Override
  4. public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
  5. // Get the corresponding layoutInflater, create skinLayoutFactory and set it in
  6. setFactory2(activity);
  7. }
  8. /**
  9. * Listen to the activity life cycle to set the Factory to intercept the system's view creation
  10. * Note that mFactorySet needs to be set to false  
  11. * There is a flaw here: >28 Then this property cannot be changed using reflection. The system prohibits it.
  12. * You can consider directly reflecting to modify the value of the Factory. This system has no restrictions. There is no practice here.
  13. */
  14. private void setFactory2(Activity activity){
  15. LayoutInflater layoutInflater = LayoutInflater. from (activity);
  16. try {
  17. //Android layout loader uses mFactorySet to mark whether Factory has been set
  18. //If set, throw once
  19. //Set the mFactorySet tag to false  
  20. Field field = LayoutInflater.class.getDeclaredField( "mFactorySet" );
  21. field.setAccessible( true );
  22. field.setBoolean(layoutInflater, false );
  23. } catch (Exception e) {
  24. e.printStackTrace();
  25. }
  26. SkinLayoutFactory skinLayoutFactory = new SkinLayoutFactory();
  27. LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutFactory);
  28. }
  29. }
  30. public class SkinManager {
  31. private static SkinManager instance;
  32. private Application application;
  33. private SkinActivityLifecycle skinActivityLifecycle;
  34. public   static void init(Application application) {
  35. synchronized (SkinManager.class) {
  36. if ( null == instance) {
  37. instance = new SkinManager(application);
  38. }
  39. }
  40. }
  41. public   static SkinManager getInstance() {
  42. return instance;
  43. }
  44. private SkinManager(Application application) {
  45. this.application = application;
  46. //Register Activity life cycle callback
  47. skinActivityLifecycle = new SkinActivityLifecycle();
  48. application.registerActivityLifecycleCallbacks(skinActivityLifecycle);
  49. }
  50. }

After the above is completed, we can output after running

From this we can intercept the creation of the system view and create it ourselves. As for the view list output by the log, everyone should be familiar with the structure of DecorView. I will not go into details here. At this point, we have completed the view creation part.

4. Realize skinning

Filter the required view

As mentioned above, we have intercepted all views. The actual skinning only needs to cache the views that need to be skinned. Here we filter the views by their properties.

// Only the view with these properties set is needed

  1. public class SkinAttribute {
  2. static {
  3. mAttributes.add ( "background" );
  4. mAttributes.add ( "src" );
  5. mAttributes.add ( "textColor" );
  6. mAttributes.add ( "drawableLeft" );
  7. mAttributes.add ( "drawableTop" );
  8. mAttributes.add ( "drawableRight" );
  9. mAttributes.add ( "drawableBottom" );
  10. }
  11. // Filter view  
  12. public void load ( View   view , AttributeSet attrs) {
  13. // List of attributes that can be replaced by this view setting
  14. List<SkinPair> skinPairs = new ArrayList<>();
  15. for ( int i = 0; i < attrs.getAttributeCount(); i++) {
  16. //Get the attribute name
  17. String attributeName = attrs.getAttributeName(i);
  18. //Whether it meets the property name that needs to be filtered
  19. if (mAttributes. contains (attributeName)) {
  20. String attributeValue = attrs.getAttributeValue(i);
  21. // If it is not referenced by the @ symbol, it does not matter, such as ? protecting # and so on - In fact, ? may also need to be changed, here for convenience
  22. if (!attributeValue.startsWith( "@" )) {
  23. continue ;
  24. }
  25. //Resource id
  26. int resId = Integer .parseInt(attributeValue. substring (1));
  27. if (resId != 0) {
  28. // Properties that can be replaced
  29. SkinPair skinPair = new SkinPair(attributeName, resId);
  30. skinPairs.add (skinPair);
  31. }
  32. }
  33. }
  34. // The above has saved the properties that need to be modified in this view into skinPairs
  35. // Check if skinPairs is empty. If not, cache the property information of this view .
  36. if (!skinPairs.isEmpty() || view instanceof TextView) {
  37. SkinView skinView = new SkinView( view , skinPairs);
  38. // To modify the style
  39. skinView.applySkin();
  40. mSkinViews.add (skinView);
  41. }
  42. }
  43. /**
  44. * Traverse the view to set the style
  45. */
  46. public void applySkin() {
  47. for (SkinView mSkinView : mSkinViews) {
  48. mSkinView.applySkin();
  49. }
  50. }
  51. // The view and properties that need to be skinned
  52. static class SkinView {
  53. View   view ;
  54. List<SkinPair> skinPairs;
  55. public SkinView( View   view , List<SkinPair> skinPairs) {
  56. this.view = view ;
  57. this.skinPairs = skinPairs;
  58. }
  59. //Set the style. Here we search in the skin package. If we can't find it, we return the default one.
  60. public void applySkin() {
  61. for (SkinPair skinPair : skinPairs) {
  62. Drawable left = null , top = null , right = null , bottom = null ;
  63. switch (skinPair.attributeName) {
  64. case   "background" :
  65. Object background = SkinResources.getInstance().getBackground(skinPair
  66. .resId);
  67. //Color
  68. if (background instanceof Integer ) {
  69. view .setBackgroundColor(( Integer ) background);
  70. } else {
  71. ViewCompat.setBackground( view , (Drawable) background);
  72. }
  73. break;
  74. case   "src" :
  75. background = SkinResources.getInstance().getBackground(skinPair
  76. .resId);
  77. if (background instanceof Integer ) {
  78. ((ImageView) view ).setImageDrawable(new ColorDrawable(( Integer )
  79. background));
  80. } else {
  81. ((ImageView) view ).setImageDrawable((Drawable) background);
  82. }
  83. break;
  84. case   "textColor" :
  85. ((TextView) view ).setTextColor(SkinResources.getInstance().getColorStateList
  86. (skinPair.resId));
  87. break;
  88. case   "drawableLeft" :
  89. left = SkinResources.getInstance().getDrawable(skinPair.resId);
  90. break;
  91. case   "drawableTop" :
  92. top = SkinResources.getInstance().getDrawable(skinPair.resId);
  93. break;
  94. case   "drawableRight" :
  95. right = SkinResources.getInstance().getDrawable(skinPair.resId);
  96. break;
  97. case   "drawableBottom" :
  98. bottom = SkinResources.getInstance().getDrawable(skinPair.resId);
  99. break;
  100. default :
  101. break;
  102. }
  103. if ( null != left || null != right || null != top || null != bottom) {
  104. ((TextView) view ).setCompoundDrawablesWithIntrinsicBounds( left , top , right ,
  105. bottom);
  106. }
  107. }
  108. }
  109. }
  110. // Used to save attribute name and id
  111. static class SkinPair {
  112. String attributeName;
  113. int resId;
  114. public SkinPair(String attributeName, int resId) {
  115. this.attributeName = attributeName;
  116. this.resId = resId;
  117. }
  118. }
  119. }

Next, we filter and cache where we create the view

  1. public class SkinLayoutFactory implements LayoutInflater.Factory2, Observer {
  2. public   View onCreateView(@Nullable View parent, @NonNull String name , @NonNull Context context, @NonNull AttributeSet attrs) {
  3. // Create the view  
  4. View   view = createViewFromTag(context, name , attrs);
  5. Log.e( "Skin" , "name = " + name + " , view = " + view );
  6. //Filter Views that match the attributes  
  7. skinAttribute.load ( view , attrs);
  8. return   view ;
  9. }
  10. }

At this point, when we create a view, we can filter out the views that may need to change the skin through the SkinAttribute class, and then save the relationship between each view and its attributes. When we need to replace it, we traverse the cached view and reset the corresponding attributes.

5. Make a skin pack

Create a new Android project/module

When you copy the color or image that needs to be replaced, make sure the name is consistent with that in the original project.

After all are replaced, rebuild directly and copy the generated apk package

You can change the name to anything you want, for example, I changed it to theme.skin, which is the skin package.

Copy it to the phone file - in actual application, it should be downloaded from the Internet

6. Load the skin pack

  1. public class SkinManager extends Observable {
  2. /**
  3. * Use skin packs
  4. *
  5. * @param path skin package address
  6. */
  7. public void loadSkin(String path) {
  8. if (TextUtils.isEmpty(path)) {
  9. // Pass in an empty string and use the default
  10. SkinPreference.getInstance().setSkin( "" );
  11. SkinResources.getInstance().reset();
  12. } else {
  13. try {
  14. AssetManager assetManager = AssetManager.class.newInstance();
  15. // Add resources to the resource manager
  16. Method addAssetPath = assetManager.getClass().getMethod( "addAssetPath" , String
  17. .class);
  18. addAssetPath.setAccessible( true );
  19. addAssetPath.invoke(assetManager, path);
  20. // System resources
  21. Resources resources = application.getResources();
  22. // External resource sResource
  23. Resources sResource= new Resources(assetManager, resources.getDisplayMetrics(),
  24. resources.getConfiguration());
  25. //Get the external Apk (skin package) package name
  26. PackageManager mPm = application.getPackageManager();
  27. PackageInfo info = mPm.getPackageArchiveInfo(path, PackageManager
  28. .GET_ACTIVITIES);
  29. String packageName = info.packageName;
  30. //Skin package resources are passed into the tool class SkinResources for easy subsequent search
  31. SkinResources.getInstance().applySkin(sResource, packageName);
  32. //Save the currently used skin package
  33. SkinPreference.getInstance().setSkin(path);
  34. } catch (Exception e) {
  35. e.printStackTrace();
  36. }
  37. }
  38. //Notify observers
  39. setChanged();
  40. notifyObservers();
  41. }
  42. }

The above code loads theme.skin. SkinResources is a tool class that passes in the Resources of the external skin package. The example is as follows

  1. // Find the resource ID in the skin package based on the resource ID in this app
  2. public   int getIdentifier( int resId) {
  3. if (isDefaultSkin) {
  4. return resId;
  5. }
  6. //In the skin package, it is not necessarily the id of the current program
  7. //Get the corresponding id in the current name colorPrimary
  8. // So first get the current name and type and then look for the corresponding id in the skin package
  9. String resName = mAppResources.getResourceEntryName(resId);
  10. String resType = mAppResources.getResourceTypeName(resId);
  11. int skinId = mSkinResources.getIdentifier(resName, resType, mSkinPkgName);
  12. return skinId;
  13. }
  14. // Get the color based on the resource id
  15. public   int getColor( int resId) {
  16. // If the default skin is displayed, return to the default
  17. if (isDefaultSkin) {
  18. return mAppResources.getColor(resId);
  19. }
  20. // Get the resource id in the skin package. The unified name resources in the two packages may have different ids
  21. int skinId = getIdentifier(resId);
  22. if (skinId == 0) {
  23. // Return the resources in the skin package
  24. return mAppResources.getColor(resId);
  25. }
  26. return mSkinResources.getColor(skinId);
  27. }

After loading the skin package here, all we need to do is notify the view to update. I won’t post the code here.

The existing page notifies SkinAttribute to call applySkin() to traverse the cached view to set

For subsequent pages opened, including exiting and re-entering the app, you need to call the load method of SkinAttribute to set it when SkinLayoutFactory calls onCreateView to create the view.

At this point we can implement some basic functions and test them.

Skin replacement has been successfully achieved here

Summarize

Dynamic skinning steps:

Collect controls that need skinning

Loading a Skin Pack

Replace resources

<<:  Mobile phone giants have come to their senses. Samsung announced that it will cancel mobile phone advertisements. When will domestic mobile phones follow suit?

>>:  Tencent's internal test is exposed, is the hand-swipe payment coming? Industry experts: The application prospects are not optimistic

Recommend

Black takeaway tableware is the dirtiest? Judging by color alone is unreliable!

gossip “Black plastic cutlery is the dirtiest” Th...

US regulators want phone makers to add driving mode, emulating airplane mode

Recently, according to the New York Times, the Na...

iOS Development: Swift Calls Objective-C Code

[[120564]] Recently, the new iOS programming lang...

May's hot marketing calendar is here!

From the beginning of the month when jokes about ...

MIUI will "go out" on two legs

On July 5, Xiaomi MIUI director Hong Feng reveale...

Entering the O2O industry, Testin launches O2O one-stop testing service

On August 25, Testin Cloud Testing held a press c...

Marketing Artificial Intelligence Institute: AI for Retail Leaders

Artificial intelligence continues to transform ev...

Why is the improvement of Android 5.1 to Hammer T1 so small?

At 19:00 on January 4, 2016, Smartisan Technology’...

Beware! Don’t squeeze pimples! A man developed a hemangioma because of this…

Everyone loves beauty, but acne comes from time t...

Unboxing the Xiaomi Router Youth Edition: A Must-Have for College Dormitories

On August 13, 2015, Xiaomi launched the Xiaomi Ro...

As of 2019, the global Android App download rankings

introduction It is difficult to rank the global A...