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 Q
launch 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 APK
increase. Is it necessary to introduce download & install
the 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 Android
give 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, #000000
and white in the dark mode. #FFFFFF
Similarly, 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 color
into 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 skinPrimaryTextColor
as an example, I only need to declare two color
resources:
<?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 Activity
I 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 codeint
is 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 Activity
with Fragment
additional View
code Java
.
In addition, when the number color
of skins reaches a certain scale, the huge resources will inevitably affect apk
the 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>
View
In 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 Activity
the references, the developer can try to View
update 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 TextView
the text color, nor can I update View
the 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 View
tree of the entire page? View
Yes, but it goes back to the original question, that is, all View
its own state is also reset (for example EditText
, the text is cleared), taking a step back, even if this is acceptable, then View
the 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, TextView
only focus on the update background
and textColor
, ViewGroup
only 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, SkinSupportable
it 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 Activity
by traversing View
the tree . Then the entire page will complete the refresh of the skin change, and will not affect other current attributes of itself.SkinSupportable
updateSkin
View
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
LayoutInflater
readers who don't understand, you can refer to this article of the author .
Readers who understand should know that in the process of LayoutInflater
parsing xml
and 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:View
LayoutInflater
Factory2
AppCompatXXXView
View
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, LayoutInflater
the 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 SkinCompatViewInflater
interception to replace LayoutInflater
the logic of the system itself. As CardView
an example, when parsing tags, CardView
the 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 ConstraintLayout
as 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.gradle
in 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
APP
has 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 SkinLoaderStrategy
interface, 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 SkinLoaderListener
loading 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 Activity
all 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 Activity
rendering onResume
.
View
In 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 . LayoutInflater
When the skin change occurs, you only need to update the collection.View
SkinSupportable
View
View
Activity
Finally, all the sums in the above text can View
be held by weak references to reduce the possibility of memory leaks.
3. Provide the ability to change the skin of image resources
Since color
resources can support skinning, drawable
resources 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
Android
in the systemResources
realize the replacement of resources, and what processing is done in the skinning library? - 2.
LayoutInflater
It is clearly stated in the source code that oneLayoutInflater
can only be set oncesetFactory2()
, otherwise an exception will be thrown. Then, when is the skinning library injectedFactory2
, 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 theme
APP
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 GitHub
currently star
the 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?