Reflection | Open the girl heart model at station B, and explore the design and implementation of the APP skin replacement mechanism

The reflection blog series is my attempt at a new way of learning, please refer to here for the origin and catalog of the series .

overview

The skin changing function is not a magic trick, but has become very popular, especially after the Android Qlaunch of the dark mode , most mainstream applications in China provide at least two modes: daytime and nighttime .

For unfeeling users, this function is really tasteless, but from another perspective, this is also an attempt in the process of refining the ultimate user experience of the product , providing more options for users with different preferences in different scenarios.

Taking Bilibili as an example, in addition to providing the above two themes, it also provides a pink theme full of girlish hearts for free:

From the forward-looking point of view of the product, domestic exploration of the skinning function is ahead of foreign countries. The dark modeAndroid Q viewed in the abstract is nothing more than a new theme. Therefore, developers should focus on a higher level: provide a complete skinning solution for the product , not just adapt to the dark mode .

If you think about this point clearly, developers will not only focus on the technology itself—for the entire skinning system, it covers different concerns of multiple roles such as UI, product, development, testing, and operation and maintenance, and these concerns ultimately rely on R&D to assist in decision-making. Examples are as follows:

  • UI : Define different color attributes for different UI components, and these attributes finally represent different colors under different themes (the title in day mode is black, but in night mode, the title should be white).
  • Product : Define the business process of the skin-changing function, from simple skin-changing homepages, skin-changing interactions, to different displays under different themes, payment strategies, etc.
  • Development : Provide research and development capabilities for skinning functions.
  • Testing : Ensure the stability of the skinning function, such as automated testing and convenient color picking tools.
  • Operation and maintenance : to ensure the rapid positioning and timely resolution of online problems.

In addition, there are more technical points that can be considered in depth. For example, with more and more themes, the package size will inevitably APKincrease. Is it necessary to introduce download & installthe ability of remote dynamic loading ( )? With the perspective of different roles, we can plan the vision in advance, and the subsequent coding will be more convenient.

This article will Androidgive a general description of the entire skinning system of the application. Readers should put aside their obsession with the details of code implementation , think from the needs of different roles, get a glimpse of the whole leopard , and create robust and powerful technical support for the product.

1. Define the UI specification

What is the purpose of the skinning specification? For UI designers and developers, design and development should be based on a unified and complete specification:

For UI designers, under different themes of the APP, the color of the control is no longer a single value, but should be defined by a common one. As shown in the figure above, the color of the key"title" should be black during the day, #000000and white in the dark mode. #FFFFFFSimilarly, the "secondary title", "main background color", and "dividing line color" should correspond to different values ​​under different themes.

When designing, the designer only needs to fill in the corresponding elements for each element on the page key, and complete the UI design clearly according to the specification:

ColorKey day mode dark mode Remark
skinPrimaryTextColor #000000 #FFFFFF title font color
skinSecondaryTextColor #CCCCCC #CCCCCC Secondary title font color
skinMainBgColor #FFFFFF #333333 main page background color
skinSecondaryBgColor #EEEEEE #000000 Secondary background, divider background color
and more...
skinProgressBarColor #000000 #FFFFFF progress bar color

This is more obvious for developers to improve efficiency. Developers no longer need to care about the value of specific colors, but only need to fill the corresponding color colorinto the layout:

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World"
    android:textColor="@color/skinPrimaryTextColor" />

2. Build product thinking: skin package

How to measure a developer's ability - fast and stable delivery of complex functions?

If you simply recognize this concept, then the realization of the skinning function is simple. Taking the title color skinPrimaryTextColoras an example, I only need to declare two colorresources:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="skinPrimaryTextColor">#000000</color>
    <color name="skinPrimaryTextColor_Dark">#FFFFFF</color>
</resources>

The author successfully got rid of the complicated coding implementation, and ActivityI only need 2 lines of code in it:

public void initView() {
    
    
  if (isLightMode) {
    
        // 日间模式
     tv.setTextColor(R.color.skinPrimaryTextColor);
  } else {
    
                  // 夜间模式
     tv.setTextColor(R.color.skinPrimaryTextColor_Dark);
  }
}

This kind of implementation is not useless. From the perspective of the difficulty of implementation, at least it can protect the few hairpins of developers.

Of course, this solution has "optimization space", such as providing encapsulation tools and methods that seem to get rid of endless if-else:

/**
 * 获取当前皮肤下真正的color资源,所有color的获取都必须通过该方法。
 **/
@ColorRes
public static int getColorRes(@ColorRes int colorRes) {
    
    
  // 伪代码
  if (isLightMode) {
    
         // 日间模式
     return colorRes;    // skinPrimaryTextColor
  } else {
    
                   // 夜间模式
     return colorRes + "_Dark";   // skinPrimaryTextColor_Dark
  }
}

// 代码中使用该方法,设置标题和次级标题颜色
tv.setTextColor(SkinUtil.getColorRes(R.color.skinPrimaryTextColor));
tvSubTitle.setTextColor(SkinUtil.getColorRes(R.color.skinSecondaryTextColor));

Obviously, return colorRes + "_Dark"this line of code intis invalid as the return value of the type, and readers don't need to pay attention to the specific implementation, because this kind of encapsulation still does not get rid of the essence of clumsy if-else implementation.

It can be foreseen that as the number of themes gradually increases, the code related to skinning becomes more and more bloated. The most critical problem is that the relevant colors of all controls are strongly coupled to the skinning-related code itself, and each UI container (//custom) needs to be manually set Activitywith Fragmentadditional Viewcode Java.

In addition, when the number colorof skins reaches a certain scale, the huge resources will inevitably affect apkthe volume, so the dynamic loading of theme resources is imperative. When users install the application, there is only one theme by default, and other themes are downloaded and installed on demand , such as Taobao:

At this point, the concept of skin packs came into being. Developers need to treat the color resources of a single theme as a skin pack , and load and replace resources with different skin packs under different themes:

<!--日间模式皮肤包的colors.xml-->
<resources>
    <color name="skinPrimaryTextColor">#000000</color>
    ...
</resources>

<!--深色模式皮肤包的colors.xml-->
<resources>
    <color name="skinPrimaryTextColor">#FFFFFF</color>
    ...
</resources>

ViewIn this way, for business code, developers no longer need to pay attention to the specific theme, but only need to specify the color in a conventional way, and the system will fill it according to the current color resource pair:

<!--当前切换到什么主题,系统就用对应的颜色值进行填充-->
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World"
    android:textColor="@color/skinPrimaryTextColor" />

Going back to the original question in this section, product thinking is also an indispensable ability for an excellent developer: first list different implementation solutions according to requirements, make corresponding trade-offs, and finally start coding.

3. Integration ideas

So far, everything is still in the stage of requirements proposal and design. With the clarification of requirements, technical difficulties are listed in front of developers one by one.

1. Dynamic refresh mechanism

The first problem developers face: how to implement the dynamic refresh function after skinning.

Taking the WeChat registration page as an example, after manually switching to the dark mode, WeChat refreshes the page:

Readers can't help but ask, what is the meaning of dynamic refresh , can't rebuild the current page or restart the APP?

Of course it is feasible, but it is unreasonable , because page reconstruction means the loss of page state, and users cannot accept that the filled information on a form page is reset; and if this problem is to be compensated, it is also a huge amount of work to rebuild the additional state of each page ( ), from the perspective of implementation Activity.onSaveInstanceState().

Therefore, dynamic refresh is imperative—whether the user switches the skin package in the app or manually switches the dark mode of the system, how do we send this notification to ensure that all pages are refreshed accordingly?

2. Save the activity of all pages

Readers know that we can Application.registerActivityLifecycleCallbacks()observe all life cycles in the application through methods Activity, which also means that we can hold all Activity:

public class MyApp extends Application {
    
    

    // 当前应用内的所有Activity
    private List<Activity> mPages = new ArrayList();

    @Override
    public void onCreate() {
    
    
        super.onCreate();
        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
    
    
            @Override
            public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
    
    
              mPages.add(activity);
            }

            @Override
            public void onActivityDestroyed(@NonNull Activity activity) {
    
    
              mPages.remove(activity);
            }

            // ...省略其它生命周期
        });
    }
}

With all Activitythe references, the developer can try to Viewupdate all the pages on the first time when they receive the notification of skinning.

3. Cost issues

But a huge mystery came into view. For the control, the concept of updating and skinning does not exist .

What does that mean? When the skinning notification arrives, I can't update TextViewthe text color, nor can I update Viewthe background color - they are just system controls, and they execute the most basic logic. To put it bluntly, developers can't code at all.

Some students said, can I directly re-render the entire Viewtree of the entire page? ViewYes, but it goes back to the original question, that is, all Viewits own state is also reset (for example EditText, the text is cleared), taking a step back, even if this is acceptable, then Viewthe re-rendering of the entire tree will greatly affect performance.

So, how to save the cost of dynamic page refresh as much as possible ?

The developer hopes that when the skinning occurs, only the specified properties of the specified control will be dynamically updated , for example, TextViewonly focus on the update backgroundand textColor, ViewGrouponly focus on background, other properties do not need to be reset and modified, and make full use of every performance of the device:

public interface SkinSupportable {
    
    
  void updateSkin();
}

class SkinCompatTextView extends TextView implements SkinSupportable {
    
    

  public void updateSkin() {
    
    
    // 使用当前最新的资源更新 background 和 textColor
  }
}

class SkinCompatFrameLayout extends FrameLayout implements SkinSupportable {
    
    

  public void updateSkin() {
    
    
    // 使用当前最新的资源更新 background
  }
}

As shown in the code, SkinSupportableit is an interface. The class that implements this interface means that it supports dynamic refresh. When the skin change occurs, we only need to get the current one, and let all the implementation classes execute the method to refresh themselves Activityby traversing Viewthe tree . Then the entire page will complete the refresh of the skin change, and will not affect other current attributes of itself.SkinSupportableupdateSkinView

Of course, this also means that developers need to perform a round of coverage encapsulation of conventional controls and provide corresponding dependencies:

implementation 'skin.support:skin-support:1.0.0'                   // 基础控件支持,比如SkinCompatTextView、SkinCompatFrameLayout等
implementation 'skin.support:skin-support-cardview:1.0.0'          // 三方控件支持,比如SkinCompatCardView
implementation 'skin.support:skin-support-constraint-layout:1.0.0' // 三方控件支持,比如SkinCompatConstraintLayout

From a long-term perspective, for the designers of the skinning library, the development cost of the library itself is actually not high by encapsulating the controls one by one and providing composable dependencies.

4. One hair can move the whole body

But the developers responsible for business development complained endlessly.

According to the current design, don't xml文件all the controls in the project need to be replaced again?

<!--使用前-->
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World"
    android:textColor="@color/skinPrimaryTextColor" />

<!--需要替换为-->
<skin.support.SkinCompatTextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World"
    android:textColor="@color/skinPrimaryTextColor" />

From another point of view, this is an additional cost. If one day wants to remove or replace the skinning library, it is tantamount to a new reconstruction.

Therefore, designers need to try to avoid designs that affect the whole body by pulling a hair , and it is best to let developers feel the dynamic update of the skinning library without perception .

5. Starting point: LayoutInflater.Factory2

For LayoutInflaterreaders who don't understand, you can refer to this article of the author .

Readers who understand should know that in the process of LayoutInflaterparsing xmland instantiating files , the basic controls are intercepted and created into corresponding ones through their own interfaces , which not only avoids the impact of reflection creation on performance, but also ensures downward compatibility:ViewLayoutInflaterFactory2AppCompatXXXViewView

switch (name) {
    
    
    // 解析xml,基础组件都通过new方式进行创建
    case "TextView":
        view = new AppCompatTextView(context, attrs);
        break;
    case "ImageView":
        view = new AppCompatImageView(context, attrs);
        break;
    case "Button":
        view = new AppCompatButton(context, attrs);
        break;
    case "EditText":
        view = new AppCompatEditText(context, attrs);
        break;
    // ...
    default:
    // 其他通过反射创建
}

In a nutshell:

Therefore, LayoutInflaterthe implementation idea itself provides us with a very good starting point. We only need to intercept this logic and delegate the instantiation of the control to the skinning library:

As shown in the figure, we use SkinCompatViewInflaterinterception to replace LayoutInflaterthe logic of the system itself. As CardViewan example, when parsing tags, CardViewthe generated logic is delegated to the following dependency library. If the corresponding dependency is added to the project, then the corresponding one can be generated SkinCompatCardView, which naturally supports the dynamic skinning function.

Of course, the realization of all these logics originates from adding corresponding dependencies to the project, and then initializes when the APP starts:

implementation 'skin.support:skin-support:1.0.0'   
implementation 'skin.support:skin-support-cardview:1.0.0'
// implementation 'skin.support:skin-support-constraint-layout:1.0.0'  // 未添加ConstraintLayout换肤支持
// App.onCreate()
SkinCompatManager.withApplication(this)
                .addInflater(new SkinAppCompatViewInflater())   // 基础控件换肤
                .addInflater(new SkinCardViewInflater())        // cardView
                //.addInflater(new SkinConstraintViewInflater())   // 未添加ConstraintLayout换肤支持
                .init();     

Take ConstraintLayoutas an example, when there is no corresponding dependency (), it will be constructed by reflection by default to generate the corresponding label itself ConstraintLayout. Because it is not implemented SkinSupportable, it will naturally not be updated.

In this way, the designer of the library provides enough flexibility for the skinning library, which not only avoids drastic modifications to existing projects, but also ensures extremely low usage and migration costs. If I want to remove or replace the skinning library, I only need to delete the dependencies in and the code initialized build.gradlein Application.

4. In-depth discussion

Next, the author will conduct an in-depth discussion on more details of the skinning library itself.

1. Skin pack loading strategy

The strategy pattern is also very well reflected in the design process of the skinning library.

For different skin packages, their loading and installation strategies should be different , for example:

  • 1. Each APPhas a default skin pack (usually day mode), and the strategy needs to be loaded immediately after installation;
  • 2. If the skin package is remote, and the user clicks to switch the skin, it needs to be pulled from the remote. After the download is successful, install and load;
  • 3. The skin package is downloaded and installed successfully, and then it should be loaded from the local SD card;
  • 4. Other custom loading strategies, such as encryption of remote skin packages, decryption after local loading, etc.

Therefore, the designer should abstract the loading and installation of skin packs into an SkinLoaderStrategyinterface, so that developers can configure on-demand more conveniently and flexibly.

In addition, since the loading behavior itself is likely to be a time-consuming operation, the scheduling of threads should be well controlled, and the SkinLoaderListenerloading progress and results should be notified in a timely manner by defining callbacks:

/**
 * 皮肤包加载策略.
 */
public interface SkinLoaderStrategy {
    
    
    /**
     * 加载皮肤包.
     */
    String loadSkinInBackground(Context context, String skinName, SkinLoaderListener listener);
}

/**
 * 皮肤包加载监听.
 */
public interface SkinLoaderListener {
    
    
    /**
     * 开始加载.
     */
    void onStart();

    /**
     * 加载成功.
     */
    void onSuccess();

    /**
     * 加载失败.
     */
    void onFailed(String errMsg);
}

2. Further save performance

In the above, the author mentioned that because it holds all the references, after the skinning library is skinned, it can try to update Activityall the pages .View

In fact, "update all pages" is usually unnecessary. A more reasonable way is to provide a configurable item. When the skin change is successful, only the foreground is refreshed by default, and other pages are updated after execution, which can greatly reduce the performance impact caused by Activityrendering onResume.

ViewIn addition, it is also a time-consuming operation to refresh the traversal tree repeatedly for each skin change . You can store the realized pages in a collection to which the page belongs while creating the tree . LayoutInflaterWhen the skin change occurs, you only need to update the collection.ViewSkinSupportableViewView

ActivityFinally, all the sums in the above text can Viewbe held by weak references to reduce the possibility of memory leaks.

3. Provide the ability to change the skin of image resources

Since colorresources can support skinning, drawableresources should of course also provide support, so that the display of the page can be more diversified. Usually, this scene is applied to the background image of the page. For this, readers can refer to the skinning function effect of Taobao APP:

ResourceKey day mode dark mode Remark
skinPrimaryTextColor #000000 #FFFFFF title font color
skinSecondaryTextColor #CCCCCC #CCCCCC Secondary title font color
skinMainBgDrawable A picture B picture Page main background image
skinProgressBarDrawable C animation 3D animation Loading box animation
and more...

summary

The summary is not a summary, there is more content that can be expanded, such as:

  • 1. How do classes Androidin the system Resourcesrealize the replacement of resources, and what processing is done in the skinning library?
  • 2. LayoutInflaterIt is clearly stated in the source code that one LayoutInflatercan only be set once setFactory2(), otherwise an exception will be thrown. Then, when is the skinning library injected Factory2, and why is it designed this way?
  • 3. How to further expand the functions of the skinning library according to the needs, such as providing support for a single page without skinning, and providing support for multiple pages using different skin packages?
  • 4. How to provide more tools that can be used in the testing phase and operation and maintenance phase?
  • 5. As of the time of writing, the 2021 Google IO Conference has proposed a new UI design concept Material You , which raised the concept of themeAPP to the entire operating system. Does it have any new impact on the existing skinning function?

There is no end to realization. What developers can do is to provide products with the possibility to show more value through continuous multi-faceted reflection, so as to go further and complete their own professional ability.

grateful

The construction of the design ideas in this article refers to Android-skin-support , which is GitHubcurrently starthe largest number of skin-changing libraries on the Internet. Thanks to the author ximsfei for providing such an excellent design for developers.

I would also like to thank Bilibili , Nuggets , Taobao , and WeChat for providing a variety of skinning functions for this article.

Thanks again.


about me

Hello, I am Qingmei Xun . If you think the article is valuable to you, welcome ❤️, and welcome to follow my blog or GitHub .

If you feel that the article is still lacking, please follow me to urge me to write a better article—what if I improve someday?

Guess you like

Origin blog.csdn.net/mq2553299/article/details/117162379