Reflection series blog is my attempt to learn a new way, the series of origin and table of contents please refer to here.

An overview of the

Skin peels aren’t just weird or clever, but they’ve become ubiquitous, especially since Android Q introduced dark color, and most of the country’s major apps offer at least daytime and night modes.

For insensitive users, this function is useless, but from another point of view, it is also an attempt of the product in the process of carving the user’s ultimate experience, providing more choices for users with different preferences in different situations.

In the case of Bilibili, in addition to the above two themes, there is also a free pink theme full of girly heart:

From the prospective of the product, in the exploration of change skin function is a foreign domestic leading, abstract to look at the Android Q dark pattern, also is a new topic, as a result, developers should put the Angle on the higher level, for the product to provide a perfect solution, rather than merely adapter dark mode.

With this in mind, developers need to look beyond the technology itself — for the whole skin system, there are many different aspects of the UI, product, development, testing, operations, etc., all of which ultimately rely on R&D to help make decisions, for example:

  • UI: Define different color attributes for different UI components, which ultimately represent different colors for different themes (black title in day mode, white title in night mode).
  • Product: The business process that defines the skin function, from a simple skin homepage, skin interaction, to different presentations under different themes, payment strategies, and so on.
  • Development: provide research and development ability of skin changing function.
  • Testing: To ensure the stability of skin changing functions, such as automated testing and convenient color extraction tools.
  • Operation and maintenance: to ensure the quick location and timely solution of online problems.

In addition, there are more technical points to consider further, for example, is it necessary to introduce remote dynamic load (download & Install) capability as more topics inevitably lead to larger APK packages? With the perspective of different characters, we can plan the vision ahead of time, and the subsequent coding will be much easier to follow.

This article will be a general description of the Android application skin system, readers should put aside the details of the code implementation, from the needs of different roles to think, see a glimpse of the whole leopard, to create a strong technical support for the product.

Define the UI specification

What is the purpose of the peels code? For UI designers and developers, design and development should be based on unified and complete specifications. Take gold digging APP as an example:

For UI designers, the color of the control should not be a single value for different themes of the APP, but should be defined by a common key, as shown in the figure above. The color of the “title” should be black #000000 during the day and white #FFFFFF in dark mode. Similarly, “Subtitle,” “main background color,” and “line color” should all have different values for different themes.

During the design, the designer only needs to fill the corresponding key for each element of the page and complete the UI design clearly according to the specification:

The color Key Daytime mode Dark mode note
skinPrimaryTextColor # 000000 #FFFFFF Title font color
skinSecondaryTextColor #CCCCCC #CCCCCC Subheading font color
skinMainBgColor #FFFFFF # 333333 Page main background color
skinSecondaryBgColor #EEEEEE # 000000 Secondary background, separator background color
More…
skinProgressBarColor # 000000 #FFFFFF Progress bar color

This is even more efficient for developers, who no longer need to care about the value of a specific color, just fill the corresponding color into the layout:

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

Second, build product thinking: skin package

How do you measure a developer’s ability to deliver complex features quickly and consistently?

If only the simple recognition of this concept, then the realization of the skin function is simple, to the title color skinPrimaryTextColor for example, I only need to declare two color resources:


      
<resources>
    <color name="skinPrimaryTextColor"># 000000</color>
    <color name="skinPrimaryTextColor_Dark">#FFFFFF</color>
</resources>
Copy the code

I managed to get rid of the complicated coding implementation, and in the Activity I only needed 2 lines of code:

public void initView(a) {
  if (isLightMode) {    // Daytime mode
     tv.setTextColor(R.color.skinPrimaryTextColor);
  } else {              // Night modetv.setTextColor(R.color.skinPrimaryTextColor_Dark); }}Copy the code

This implementation isn’t all bad, and in terms of implementation difficulty, it protects at least a few of the developer’s pockets.

Of course, there is “room for optimization” in this scenario, such as providing encapsulated utility methods that seem to get rid of endless if-else:

/** * Get the real color resource under the current skin. All colors must be obtained through this method. * * /
@ColorRes
public static int getColorRes(@ColorRes int colorRes) {
  / / pseudo code
  if (isLightMode) {     // Daytime mode
     return colorRes;    // skinPrimaryTextColor
  } else {               // Night mode
     return colorRes + "_Dark";   // skinPrimaryTextColor_Dark}}// Use this method in your code to set the title and subtitle colors
tv.setTextColor(SkinUtil.getColorRes(R.color.skinPrimaryTextColor));
tvSubTitle.setTextColor(SkinUtil.getColorRes(R.color.skinSecondaryTextColor));
Copy the code

Obviously, the line of return colorRes + “_Dark” is not valid as a return value of type int, so the reader need not worry about the implementation, because the encapsulation is still in the nature of the cumbersome if-else implementation.

It can be foreseen that with the gradual increase in the number of topics, the code related to skin is becoming more and more bloated. The most critical problem is that the related colors of all controls are strongly coupled to the code related to skin itself, and each UI container (Activity/Fragment/ custom View) needs to be manually set with additional Java code.

In addition, when the number of skin reaches a certain scale, the huge size of color resources is bound to affect the APK volume, so the dynamic loading of theme resources is imperative, the default user installation application only one theme, other themes download and install on demand, such as Taobao:

This is where the concept of skin packs comes in. Developers need to treat the color resources of a single theme as one skin pack, and load and replace different skin packs under different themes:

<! -- Color.xml for day mode skin pack -->
<resources>
    <color name="skinPrimaryTextColor"># 000000</color>.</resources>

<! -- Color.xml of the dark mode skin package -->
<resources>
    <color name="skinPrimaryTextColor">#FFFFFF</color>.</resources>
Copy the code

In this way, for the business code, the developer no longer needs to focus on the specific theme, just specify the color in the normal way, and the system will fill the View according to the current color resource:

<! -- The system will fill with the corresponding color value of the current theme -->
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World"
    android:textColor="@color/skinPrimaryTextColor" />
Copy the code

Going back to the original question in this section, productization thinking is also an essential skill for a good developer: start by listing different implementations based on requirements, making trade-offs, and then start coding.

Third, integrate ideas

So far, everything is still in the requirement proposal and design stage, as the requirements are clear, the technical difficulties are listed in front of the developers.

1. Dynamic refresh mechanism

The first problem developers face is how to implement dynamic refresh after skin change.

Take the wechat registration page as an example. After manually switching to dark mode, wechat refreshed the page:

Readers can’t help but ask what’s the point of a dynamic refresh, not letting the current page be rebuilt or the APP restart?

Of course, it is feasible, but not reasonable, because page reconstruction means the loss of page state, users can not accept a form page filled information reset; On each page if you want to make up for this problem, rebuild the additional state of preservation (Activity. The onSaveInstanceState ()), in the implementation point of view, is a huge quantities.

Therefore, dynamic refresh is imperative — whether users switch skin packs in the app, or manually switch the system’s dark mode, how do we send this notification to ensure that all pages are refreshed?

2. Save the Activity for all pages

Readers know, we can through the Application. RegisterActivityLifecycleCallbacks () method to observe all the Activity in the Application of life cycle, this also means that we can hold all the Activity:

public class MyApp extends Application {

    // All activities in the current application
    private List<Activity> mPages = new ArrayList();

    @Override
    public void onCreate(a) {
        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);
            }

            / /... Omit other life cycles}); }}Copy the code

With all Activity references, developers can try to get all views on all pages updated and skinned in the first place when they are notified.

3. Cost

But then the big puzzle comes into view, for the control, the concept of update skin itself does not exist.

What does that mean? When the skin notification arrived, I couldn’t tell the TextView to update the text color, nor could I tell the View to update the background color — they were just controls of the system that performed basic logic that, in plain English, the developer couldn’t code at all.

Can I just re-render the whole View tree of the whole page and all the views? Yes, but it goes back to the original problem, which is that the state of all views themselves is reset (i.e. the EditText text is cleared). To say the least, even if this were acceptable, the entire View tree would have to be rerendered to significantly affect performance.

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

TextView is only interested in updating the background and textColor, ViewGroup is only interested in updating the background, and other properties do not need to be reset or modified to maximize the performance of the device:

public interface SkinSupportable {
  void updateSkin(a);
}

class SkinCompatTextView extends TextView implements SkinSupportable {

  public void updateSkin(a) {
    // Update background and textColor with the latest resource}}class SkinCompatFrameLayout extends FrameLayout implements SkinSupportable {

  public void updateSkin(a) {
    // Update background with the latest resource}}Copy the code

As the code shows, SkinSupportable is an interface, and classes that implement this interface mean that they all support dynamic refreshing. When a skin change occurs, we just need to grab the current Activity and walk through the View tree, Have all the Implementation classes of SkinSupportable perform the updateSkin method to refresh themselves, and the entire page is skinned without affecting the View’s current properties.

Of course, this also means that developers need to wrap regular controls in a round of coverage and provide corresponding dependencies:

implementation 'skin. Support: skin - support: 1.0.0'                   // Support for basic controls such as SkinCompatTextView, SkinFramelayout etc
implementation 'skin. Support: skin - support - cardview: 1.0.0'          // Tripartite control support, such as skincardView
implementation 'skin. Support: skin - support - the constraint - layout: 1.0.0' / / three control support, such as SkinCompatConstraintLayout
Copy the code

In the long run, the development cost of the library itself is not high for the designers of the skin library, which provides the dependency of combinable choice for the control encapsulation.

4. The whole body is involved

But the developers in charge of business development complained.

As currently designed, wouldn’t all the controls in the project’s XML file need to be replaced?

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

<! -- need to be replaced with -->
<skin.support.SkinCompatTextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World"
    android:textColor="@color/skinPrimaryTextColor" />
Copy the code

From another point of view, this is an additional cost, if one day to remove or replace the skin library, it is no different than a new reconstruction.

Therefore, designers should try to avoid similar design that affects the whole body, and it is better to let developers feel the dynamic update of skin library without feeling.

5. Start: LayoutInflater.Factory2

For those unfamiliar with LayoutInflaters, please refer to this article by the author.

Readers of LayoutInflaters should know that layoutInflaters intercept basic controls and create appxxxViews as they parse XML files and instantiate views through their Factory2 interface. Avoiding the performance impact of reflection creating a View and ensuring downward compatibility:

switch (name) {
    // Parsing XML, base components are created in new mode
    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:
    // Others are created by reflection
}
Copy the code

In one picture:

Therefore, the implementation of LayoutInflater itself provides a good place to start. We simply intercept this logic and delegate the instantiation of the control to the peeler library:

As shown in the figure, we use Skinviewinflater to intercept and replace the logic of system LayoutInflater itself. Take CardView for example, when parsing tags, we delegate the logic generated by CardView to the following dependency library. If the corresponding dependency is added in the project, Then you can generate a corresponding SkinCompatCardView, which naturally supports dynamic peels.

Of course, the implementation of all this logic starts with the project adding the corresponding dependency and then initializing it 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' // No ConstraintLayout support added
Copy the code
// App.onCreate()
SkinCompatManager.withApplication(this)
                .addInflater(new SkinAppCompatViewInflater())   // Base control skin
                .addInflater(new SkinCardViewInflater())        // cardView
                / /. AddInflater (new SkinConstraintViewInflater ()) / / not add ConstraintLayout skinning support
                .init();     
Copy the code

ConstraintLayout, for example, is constructed by default through reflection when there is no corresponding dependency (), and ConstraintLayout corresponding to the tag itself is generated. Since SkinSupportable is not implemented, it will not be skinned.

In this way, the designers of the library have provided enough flexibility for the peeler library to avoid drastic changes to existing projects while keeping the usage and migration costs very low. If I want to remove or replace the peeler library, ALL I need to do is remove the dependencies in build.gradle and the code initialized in the Application.

Fourth, in-depth discussion

Next the author will be for the skin library itself more details for in-depth discussion.

1. Skin package loading strategy

The strategy mode is also very well reflected in the design process of skin changing library.

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

  • 1, eachAPPEach has a default skin pack (usually in daytime mode) that policies need to be loaded immediately after installation;
  • 2. If the skin package is remote, the user clicks to switch the skin, needs to pull it from the remote, and install and load it after successful download;
  • 3. After the skin package is successfully downloaded and installed, it should be loaded from the local SD card;
  • 4. Other customized loading strategies, such as remote skin package encryption, decryption after local loading, etc.

Therefore, designers should abstract the loading and installation of skin packs into a SkinLoaderStrategy interface for more convenient and flexible on-demand configuration by developers.

In addition, since the loading behavior itself is likely to be a time-consuming operation, the thread scheduling should be controlled well, and the loading progress and results should be timely notified by defining the SkinLoaderListener callback:

/** * Skin package loading policy. */
public interface SkinLoaderStrategy {
    /** * Loads the skin pack. */
    String loadSkinInBackground(Context context, String skinName, SkinLoaderListener listener);
}

/** * Skin pack load listener. */
public interface SkinLoaderListener {
    /** * start loading. */
    void onStart(a);

    /** * Successfully loaded. */
    void onSuccess(a);

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

2, further save performance

As I mentioned above, because I hold all Activity references, the skin library can try to update all views on all pages after the skin is skinned.

In fact, “update all page actions” is usually not necessary. It is more reasonable to provide a configurable option that only refreshes the foreground Activity by default when the skin is successful, and the rest of the page is updated after the onResume execution. This can greatly reduce the performance impact of rendering.

In addition, it is also a time-consuming operation to iterate through the View tree for refreshing each peel. By creating a View tree in LayoutInflater, the SkinSupportable View can be stored in a collection of pages to which the SkinSupportable View belongs. When the peel occurs, All you need to do is update the View in the collection.

Finally, the Activity and View in the above text can be held by weak references to reduce the possibility of memory leaks.

3. Provide skin changing ability of picture resources

Since color resources can support skin, drawable resources should naturally also provide support, so that the display of the page can be more diversified, usually this scene is applied to the background of the page, for which readers can refer to the effect of skin function of Taobao APP:

Resources are the Key Daytime mode Dark mode note
skinPrimaryTextColor # 000000 #FFFFFF Title font color
skinSecondaryTextColor #CCCCCC #CCCCCC Subheading font color
skinMainBgDrawable A picture B pictures Page main background image
skinProgressBarDrawable C animation D animation Loading box animation
More…

summary

The summary is not a summary. There are more things that can be expanded on, such as:

  • 1.AndroidIn the systemResourcesHow does the class implement resource replacement, and what is done in the skin library?
  • 2,LayoutInflaterThe source code clearly states that oneLayoutInflaterCan only be set oncesetFactory2()Otherwise, an exception will be thrown. Then, when is the skin library carried outFactory2Why is it designed that way?
  • 3. How to further expand the function of skin library according to requirements, such as providing support for single page without skin, and providing support for multiple pages with different skin packs?
  • 4. How to provide more tools that can be used in the testing and operation stages?
  • 5. At the time of writing, a new UI design concept was presented at Google IO 2021Material YouThat will beThe themeFrom the concept ofAPPDoes it have any new impact on the existing skin changing function when it is extended to the entire operating system?

There is no end to the realization. What developers can do is to provide the possibility to show more value for their products through continuous multi-directional reflection, so as to further complete the phased leap of their own professional ability.

Thank you

The design of this article is based on android-skin-Support, which has the largest number of star skin remover on GitHub. Thanks to the author Ximsfei for providing such an excellent design for developers.

Also thanks to bilibili, Nuggets, Taobao, wechat, many excellent applications for this article to provide a variety of skin changing function display.

Thanks again.


About me

If you think this article is valuable to you, welcome to ❤️, and also welcome to follow my blog or GitHub.

If you feel that the article is not enough, please also urge me to write a better article by paying attention to it — in case I make progress one day?

  • My Android learning system
  • About the article error correction
  • About paying for knowledge
  • About the Reflections series