[[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: - public void setContentView(@LayoutRes int layoutResID) {
- //Call window's setContentView
- getWindow().setContentView(layoutResID);
- initWindowDecorActionBar();
- }
Window - PhoneWindow - @Override
- public void setContentView( int layoutResID) {
- //Call LayoutInflater's inflate
- mLayoutInflater.inflate(layoutResID, mContentParent);
- }
LayoutInflater - @Override
- public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
- // Temp is the root view that was found in the xml to create the root layout
- final View temp = createViewFromTag(root, name , inflaterContext, attrs);
- // Inflate all children under temp against its context. Create a sub-layout and finally call createViewFromTag
- rInflateChildren(parser, temp , attrs, true );
- }
- void rInflate(XmlPullParser parser, View parent, Context context, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
- // Loop call createViewFromTag to create sub-layouts
- while (((type = parser. next ()) != XmlPullParser.END_TAG ||parser.getDepth() >depth) && type != XmlPullParser.END_DOCUMENT) {
- final View view = createViewFromTag(parent, name , context, attrs);
- }
- }
- // From this method we can see that we try to create View through various Factories
- public final View tryCreateView(@Nullable View parent, @NonNull String name ,@NonNull Context context,@NonNull AttributeSet attrs) {
- 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);
- }
- return view ;
- }
- View createViewFromTag( View parent, String name , Context context, AttributeSet attrs,boolean ignoreThemeAttr) {
- // This is the more important part
- // Try to create a View through Factory
- View view = tryCreateView(parent, name , context, attrs);
- // If there is no Factory to create, then call the following method to create View
- if ( view == null ) {
- if (-1 == name .indexOf( '.' )) {
- // The View provided by the system does not have a ., such as View , ImageView, TextView
- view = onCreateView(context, parent, name , attrs);
- } else {
- // Third-party View or custom view such as com.cbb.xxxView
- view = createView(context, name , null , attrs);
- }
- }
- }
- // You may be confused here. Why is only android.view . passed here? Many views are not in this package.
- protected View onCreateView(String name , AttributeSet attrs) throws ClassNotFoundException {
- // Finally, createView is called and the full name of the system view is passed in
- return createView( name , "android.view." , attrs);
- }
- public final View createView(@NonNull Context viewContext, @NonNull String name ,@Nullable String prefix, @Nullable AttributeSet attrs)throws ClassNotFoundException, InflateException {
- // Get the View construction method in the cache
- Constructor<? extends View > constructor = sConstructorMap.get( name );
- // If there is no cache, reflect and get the View construction method and cache it
- // It should be noted that the view construction method with two parameters is used here
- if (constructor == null ) {
- clazz = Class.forName(prefix != null ? (prefix + name ) : name , false ,
- mContext.getClassLoader()).asSubclass( View .class);
- constructor = clazz.getConstructor(mConstructorSignature);
- constructor.setAccessible( true );
- sConstructorMap.put( name , constructor);
- }
- // Use the constructor to create a view
- final View view = constructor.newInstance(args);
- }
- // sdk provides a method to set Factory
- public void setFactory2(Factory2 factory) {
- // 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 .
- if (mFactorySet) {
- throw new IllegalStateException( "A factory has already been set on this LayoutInflater" );
- }
- mFactorySet = true ;
- }
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. - public static LayoutInflater from (Context context) {
- LayoutInflater LayoutInflater =
- (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- return LayoutInflater;
- }
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 - public class PhoneLayoutInflater extends LayoutInflater {
- private static final String[] sClassPrefixList = {
- "android.widget." ,
- "android.webkit." ,
- "android.app."
- };
- @Override protected View onCreateView(String name , AttributeSet attrs) throws ClassNotFoundException {
- for (String prefix : sClassPrefixList) {
- View view = createView( name , prefix, attrs);
- if ( view != null ) {
- return view ;
- }
- }
- return super.onCreateView( name , attrs);
- }
- }
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 - public class SkinLayoutFactory implements LayoutInflater.Factory2 {
- //Package directory listing
- private static final String[] sClassPrefixList = {
- "android.widget." ,
- "android.webkit." ,
- "android.app." ,
- "android.view."
- };
- // Two parameters of the view construction method
- private static final Class<?>[] mConstructorSignature = new Class[]{
- Context.class, AttributeSet.class};
- // The user caches the construction method obtained by reflection to prevent the subsequent repeated reflection of the same type of view
- private static final HashMap<String, Constructor<? extends View >> sConstructorMap =
- new HashMap<String, Constructor<? extends View >>();
- @Nullable
- @Override
- public View onCreateView(@Nullable View parent, @NonNull String name , @NonNull Context context, @NonNull AttributeSet attrs) {
- // Create the view
- View view = createViewFromTag(context, name , attrs);
- Log.e( "Skin" , "name = " + name + " , view = " + view );
- return view ;
- }
- /**
- * Create a view
- * Determine whether to distinguish between two view types by judging whether they contain.
- *
- * @param name may be TextView or xxx.xxx.xxxView
- */
- private View createViewFromTag(Context context, String name , AttributeSet attrs) {
- View view ;
- if (-1 == name .indexOf( '.' )) {
- view = createViewByPkgList(context, name , attrs);
- } else {
- view = createView(context, name , attrs);
- }
- return view ;
- }
- /**
- * 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
- *
- * @param name may be TextView
- */
- private View createViewByPkgList(Context context, String name , AttributeSet attrs) {
- for (String prefix : sClassPrefixList) {
- try {
- View view = createView(context, prefix + name , attrs);
- if ( view != null ) {
- return view ;
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- return null ;
- }
- /**
- * Actually start creating the view
- *
- * @param name The name format is xxx.xxx.xxxView
- */
- private View createView(Context context, String name , AttributeSet attrs) {
- Constructor<? extends View > constructor = sConstructorMap.get( name );
- if ( null == constructor) {
- try {
- Class<? extends View > aClass = context.getClassLoader().loadClass( name ).asSubclass
- ( View .class);
- constructor = aClass.getConstructor(mConstructorSignature);
- sConstructorMap.put( name , constructor);
- } catch (Exception e) {
- }
- }
- if ( null != constructor) {
- try {
- return constructor.newInstance(context, attrs);
- } catch (Exception e) {
- }
- }
- return null ;
- }
- }
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. - // The system provides the ability to monitor the entire app activity life cycle
- public class SkinActivityLifecycle implements Application.ActivityLifecycleCallbacks {
- @Override
- public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
- // Get the corresponding layoutInflater, create skinLayoutFactory and set it in
- setFactory2(activity);
- }
- /**
- * Listen to the activity life cycle to set the Factory to intercept the system's view creation
- * Note that mFactorySet needs to be set to false
- * There is a flaw here: >28 Then this property cannot be changed using reflection. The system prohibits it.
- * You can consider directly reflecting to modify the value of the Factory. This system has no restrictions. There is no practice here.
- */
- private void setFactory2(Activity activity){
- LayoutInflater layoutInflater = LayoutInflater. from (activity);
- try {
- //Android layout loader uses mFactorySet to mark whether Factory has been set
- //If set, throw once
- //Set the mFactorySet tag to false
- Field field = LayoutInflater.class.getDeclaredField( "mFactorySet" );
- field.setAccessible( true );
- field.setBoolean(layoutInflater, false );
- } catch (Exception e) {
- e.printStackTrace();
- }
- SkinLayoutFactory skinLayoutFactory = new SkinLayoutFactory();
- LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutFactory);
- }
- }
- public class SkinManager {
- private static SkinManager instance;
- private Application application;
- private SkinActivityLifecycle skinActivityLifecycle;
- public static void init(Application application) {
- synchronized (SkinManager.class) {
- if ( null == instance) {
- instance = new SkinManager(application);
- }
- }
- }
- public static SkinManager getInstance() {
- return instance;
- }
- private SkinManager(Application application) {
- this.application = application;
- //Register Activity life cycle callback
- skinActivityLifecycle = new SkinActivityLifecycle();
- application.registerActivityLifecycleCallbacks(skinActivityLifecycle);
- }
- }
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 - public class SkinAttribute {
- static {
- mAttributes.add ( "background" );
- mAttributes.add ( "src" );
- mAttributes.add ( "textColor" );
- mAttributes.add ( "drawableLeft" );
- mAttributes.add ( "drawableTop" );
- mAttributes.add ( "drawableRight" );
- mAttributes.add ( "drawableBottom" );
- }
- // Filter view
- public void load ( View view , AttributeSet attrs) {
- // List of attributes that can be replaced by this view setting
- List<SkinPair> skinPairs = new ArrayList<>();
- for ( int i = 0; i < attrs.getAttributeCount(); i++) {
- //Get the attribute name
- String attributeName = attrs.getAttributeName(i);
- //Whether it meets the property name that needs to be filtered
- if (mAttributes. contains (attributeName)) {
- String attributeValue = attrs.getAttributeValue(i);
- // 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
- if (!attributeValue.startsWith( "@" )) {
- continue ;
- }
- //Resource id
- int resId = Integer .parseInt(attributeValue. substring (1));
- if (resId != 0) {
- // Properties that can be replaced
- SkinPair skinPair = new SkinPair(attributeName, resId);
- skinPairs.add (skinPair);
- }
- }
- }
- // The above has saved the properties that need to be modified in this view into skinPairs
- // Check if skinPairs is empty. If not, cache the property information of this view .
- if (!skinPairs.isEmpty() || view instanceof TextView) {
- SkinView skinView = new SkinView( view , skinPairs);
- // To modify the style
- skinView.applySkin();
- mSkinViews.add (skinView);
- }
- }
- /**
- * Traverse the view to set the style
- */
- public void applySkin() {
- for (SkinView mSkinView : mSkinViews) {
- mSkinView.applySkin();
- }
- }
- // The view and properties that need to be skinned
- static class SkinView {
- View view ;
- List<SkinPair> skinPairs;
- public SkinView( View view , List<SkinPair> skinPairs) {
- this.view = view ;
- this.skinPairs = skinPairs;
- }
- //Set the style. Here we search in the skin package. If we can't find it, we return the default one.
- public void applySkin() {
- for (SkinPair skinPair : skinPairs) {
- Drawable left = null , top = null , right = null , bottom = null ;
- switch (skinPair.attributeName) {
- case "background" :
- Object background = SkinResources.getInstance().getBackground(skinPair
- .resId);
- //Color
- if (background instanceof Integer ) {
- view .setBackgroundColor(( Integer ) background);
- } else {
- ViewCompat.setBackground( view , (Drawable) background);
- }
- break;
- case "src" :
- background = SkinResources.getInstance().getBackground(skinPair
- .resId);
- if (background instanceof Integer ) {
- ((ImageView) view ).setImageDrawable(new ColorDrawable(( Integer )
- background));
- } else {
- ((ImageView) view ).setImageDrawable((Drawable) background);
- }
- break;
- case "textColor" :
- ((TextView) view ).setTextColor(SkinResources.getInstance().getColorStateList
- (skinPair.resId));
- break;
- case "drawableLeft" :
- left = SkinResources.getInstance().getDrawable(skinPair.resId);
- break;
- case "drawableTop" :
- top = SkinResources.getInstance().getDrawable(skinPair.resId);
- break;
- case "drawableRight" :
- right = SkinResources.getInstance().getDrawable(skinPair.resId);
- break;
- case "drawableBottom" :
- bottom = SkinResources.getInstance().getDrawable(skinPair.resId);
- break;
- default :
- break;
- }
- if ( null != left || null != right || null != top || null != bottom) {
- ((TextView) view ).setCompoundDrawablesWithIntrinsicBounds( left , top , right ,
- bottom);
- }
- }
- }
- }
- // Used to save attribute name and id
- static class SkinPair {
- String attributeName;
- int resId;
- public SkinPair(String attributeName, int resId) {
- this.attributeName = attributeName;
- this.resId = resId;
- }
- }
- }
Next, we filter and cache where we create the view - public class SkinLayoutFactory implements LayoutInflater.Factory2, Observer {
- public View onCreateView(@Nullable View parent, @NonNull String name , @NonNull Context context, @NonNull AttributeSet attrs) {
- // Create the view
- View view = createViewFromTag(context, name , attrs);
- Log.e( "Skin" , "name = " + name + " , view = " + view );
- //Filter Views that match the attributes
- skinAttribute.load ( view , attrs);
- return view ;
- }
- }
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- public class SkinManager extends Observable {
- /**
- * Use skin packs
- *
- * @param path skin package address
- */
- public void loadSkin(String path) {
- if (TextUtils.isEmpty(path)) {
- // Pass in an empty string and use the default
- SkinPreference.getInstance().setSkin( "" );
- SkinResources.getInstance().reset();
- } else {
- try {
- AssetManager assetManager = AssetManager.class.newInstance();
- // Add resources to the resource manager
- Method addAssetPath = assetManager.getClass().getMethod( "addAssetPath" , String
- .class);
- addAssetPath.setAccessible( true );
- addAssetPath.invoke(assetManager, path);
- // System resources
- Resources resources = application.getResources();
- // External resource sResource
- Resources sResource= new Resources(assetManager, resources.getDisplayMetrics(),
- resources.getConfiguration());
- //Get the external Apk (skin package) package name
- PackageManager mPm = application.getPackageManager();
- PackageInfo info = mPm.getPackageArchiveInfo(path, PackageManager
- .GET_ACTIVITIES);
- String packageName = info.packageName;
- //Skin package resources are passed into the tool class SkinResources for easy subsequent search
- SkinResources.getInstance().applySkin(sResource, packageName);
- //Save the currently used skin package
- SkinPreference.getInstance().setSkin(path);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- //Notify observers
- setChanged();
- notifyObservers();
- }
- }
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 - // Find the resource ID in the skin package based on the resource ID in this app
- public int getIdentifier( int resId) {
- if (isDefaultSkin) {
- return resId;
- }
- //In the skin package, it is not necessarily the id of the current program
- //Get the corresponding id in the current name colorPrimary
- // So first get the current name and type and then look for the corresponding id in the skin package
- String resName = mAppResources.getResourceEntryName(resId);
- String resType = mAppResources.getResourceTypeName(resId);
- int skinId = mSkinResources.getIdentifier(resName, resType, mSkinPkgName);
- return skinId;
- }
- // Get the color based on the resource id
- public int getColor( int resId) {
- // If the default skin is displayed, return to the default
- if (isDefaultSkin) {
- return mAppResources.getColor(resId);
- }
- // Get the resource id in the skin package. The unified name resources in the two packages may have different ids
- int skinId = getIdentifier(resId);
- if (skinId == 0) {
- // Return the resources in the skin package
- return mAppResources.getColor(resId);
- }
- return mSkinResources.getColor(skinId);
- }
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 |