-
Notifications
You must be signed in to change notification settings - Fork 2.7k
QMUI 换肤
QMUI版本要求: v2.0.0-alpha05+
Android 10 提供了 Dark Mode 适配提供的 API:
- 提供了
values-night
、drawable-night
资源目录,与我们做屏幕适配一样,App 会根据不同模式去不同文件夹取资源。 - 在
configChange
里加入了uiMode
,因而我们可以通过onConfigurationChanged
来监听夜间模式的打开和关闭,从而做一些自定义的处理。 - css 支持
prefers-color-scheme
媒体查询,从而支持 Webview 内容的夜间模式切换。
夜间模式只是配置的一种,其设计思路同横竖屏旋转等走相同的方案:
- Android 官方团队认为 UI 应该是需要时创建,并且可以随意销毁重建的,因此在默认情况下,夜间模式切换后,
Activity
是会被销毁而后重建的,这样重新从资源文件夹里按当前配置而实现夜间模式,这就是第一个 API 存在的意义了。 - 而某些场景,例如视频播放,我们可能要自定义做一些处理,那我们就可以通过配置
configChanges
, 不销毁Activity
而走onConfigurationChanged
通知,完全由业务方接管, 这便是第二个 API 存在的意义了。
Activity
销毁重建在一些比较轻量的 UI 上效果很好,但是如果 UI 比较重,或者 Activity
没有处理数据状态保存于恢复工作的话,重建 Activity
显得有点笨拙,甚至可能出现界面黑屏的现象,体验不是很好。
-
QMUISkinManager
: 存储肤色配置,并且派发当前肤色给它管理的Activity
、Fragment
、Dialog
、PopupWindow
。 它通过QMUISkinManager.of(name, context)
获取,是可以多实例的。 因而一个 App 可以在不同场景执行不同的换肤管理, 例如阅读产品阅读器的换肤和其它业务模块 uiMode 切换的区分管理。 -
QMUISkinValueBuilder
: 用于构建一个 View 实例的换肤配置(textColor、background、border、separator等) -
QMUISkinHelper
: 一些辅助工具方法,最常用的为QMUISkinHelper.setSkinValue(View, QMUISkinValueBuilder)
,将QMUISkinValueBuilder
的配置应用到一个 View 实例。 如果使用 kotlin 语言,可以通过View.skin { ... }
来配置 View 实例。 -
QMUISkinLayoutInflaterFactory
: 用于支持 xml 换肤配置项解析。 -
IQMUISkinDispatchInterceptor
:View
可以通过实现它,来拦截 skin 更改的派发。 -
IQMUISkinHandlerView
:View
可以通过实现它,来完全自定义不同 skin 的处理。 -
IQMUISkinDefaultAttrProvider
:View
可以通过实现它, 提供View
默认的默认换肤配置,从组件层面提供换肤支持。
这一步需要我们与设计师协作,整理一套颜色、背景资源等供 App 使用。之后我们在 xml 里以 attr 的形式给它命名,例如:
<attr name="app_common_color_01" format="color" />
<attr name="app_common_color_02" format="color" />
...
<attr name="app_common_bg_01" format="reference" />
然后用多套 style 实现上述定义的 attr 值:
<style name="app_skin_1" parent="AppTheme">
<item name="app_common_color_01">#fff</item>
<item name="app_common_color_02">#000</item>
...
<item name="app_common_bg_01">@drawable/xxx</item>
</style>
<style name="app_skin_2" parent="app_skin_1">
<item name="app_common_color_01">#ccc</item>
...
</style>
<style name="app_skin_3" parent="app_skin_1">
<item name="app_common_color_01">#000</item>
...
</style>
style 是支持继承的, 以上述为例,app_skin_3
继承自 app_skin_1
, 在通过 attr 寻找其值时,如果在 app_skin_3
没找到,那么它就会去 app_skin_1
寻找。 因此我们可以把 App 的 theme 作为我们的一个 skin, 其它 skin 都继承自这个 skin。
public static final int SKIN_1 = 1;
public static final int SKIN_2 = 2;
public static final int SKIN_3 = 3;
QMUISkinManager skinManager = QMUISkinManager.defaultInstance(context);
skinManager.addSkin(SKIN_1, R.style.app_skin_1);
skinManager.addSkin(SKIN_2, R.style.app_skin_2);
skinManager.addSkin(SKIN_3, R.style.app_skin_3);
QMUIFragmentActivity
与 QMUIActivity
默认注入了默认的 QMUISkinManager
, 如果你需要更改 QMUISkinManager
, 通过 setSkinManager
更改:
class MyActivity extend QMUIActivity{
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setSkinManager(null); // 这样可以去除这个 Activity 的换肤支持
setSkinManager(...);
}
}
QMUIFragmentActivity
与 QMUIActivity
也默认使用 QMUISkinLayoutInflaterFactory
来解析 xml,你也可以通过重写 useQMUISkinLayoutInflaterFactory()
来决定是否要使用它。
如果因为某些原因无法使用 QMUIActivity
,那么你需要自己处理 QMUISkinManager
的注册:
class YourActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
// 使用 QMUISkinLayoutInflaterFactory
LayoutInflater layoutInflater = LayoutInflater.from(this);
LayoutInflaterCompat.setFactory2(layoutInflater,
new QMUISkinLayoutInflaterFactory(this, layoutInflater));
super.onCreate(savedInstanceState);
// 注入 QMUISkinManager
mSkinManager = QMUISkinManager.defaultInstance(this);
}
@Override
public void onStart() {
super.onStart();
if(mSkinManager != null){
mSkinManager.register(this);
}
}
@Override
protected void onStop() {
super.onStop();
if(mSkinManager != null){
mSkinManager.unRegister(this);
}
}
}
如果 Activity
注入了 QMUISkinManager
,那么它所管理的 Fragment
都会归这个 QMUISkinManager
管理。
如果你想以 Fragment
为单位管理来使用 QMUISkinManager
,可以参照 Activity
来注入 QMUISkinManager
。
new QMUIDialog.CheckableDialogBuilder(getActivity())
.setSkinManager(QMUISkinManager.of(name, context))
.xxx
.build()
.show()
MessageDialogBuilder
、MenuDialogBuilder
等使用类似。
new QMUIBottomSheet.BottomListSheetBuilder(getContext())
.setSkinManager(QMUISkinManager.of(name, context))
.xxx
.build()
.show()
BottomGridSheetBuilder
使用类似。
QMUIPopups.popup(context, width)
.skinManager(QMUISkinManager.of(name, context))
.xxx
.show(v);
QMUIFullScreenPopup
、QMUIQuickAction
使用类似。
配置项 | QMUISkinValueBuilder 方法名 | xml属性名 | 备注 |
---|---|---|---|
背景 | background | qmui_skin_background | |
字体颜色 | textColor | qmui_skin_text_color | 支持 TextView, QMUIQQFaceView, QMUIProgressBar |
hint字体颜色 | hintColor | qmui_skin_hint_color | 支持 TextView, TextInputLayout |
进度颜色 | progressColor | qmui_skin_progress_color | 支持 QMUIProgressBar,QMUISlider |
src | src | qmui_skin_src | 只支持 ImageView |
边框 | border | qmui_skin_border | 支持 IQMUILayout 的实现者、QMUIRoundButton、QMUISlider.DefaultThumbView |
分隔线 | topSeparator, rightSeparator, bottomSeparator, leftSeparator | qmui_skin_separator_top, qmui_skin_separator_right, qmui_skin_separator_bottom, qmui_skin_separator_left | 支持 IQMUILayout 的实现者 |
透明度 | alpha | qmui_skin_alpha | |
着色 | tintColor | qmui_skin_tint_color | 支持 ImageView,QMUILoadingView,QMUIPullRefreshLayout.RefreshView |
背景着色 | bgTintColor | qmui_skin_bg_tint_color | 支持 TintableBackgroundView 的实现者 |
下划线 | underline | qmui_skin_underline | 只支持 QMUIQQFaceView |
「更多」背景 | moreBgColor | qmui_skin_more_bg_color | 只支持 QMUIQQFaceView |
「更多」字体颜色 | moreTextColor | qmui_skin_more_text_color | 只支持 QMUIQQFaceView |
CompoundDrawable着色 | textCompoundTintColor | qmui_skin_text_compound_tint_color | 只支持 TextView |
CompoundDrawable src | textCompoundTopSrc,textCompoundRightSrc,textCompoundBottomSrc,textCompoundLeftSrc | qmui_skin_text_compound_src_left, qmui_skin_text_compound_src_top, qmui_skin_text_compound_src_right, qmui_skin_text_compound_src_bottom | 只支持 TextView |
QMUISkinValueBuilder builder = QMUISkinValueBuilder.acquire();
builder.background(R.attr.app_skin_common_background);
builder.border(R.attr.qmui_skin_support_color_separator);
// more ....
QMUISkinHelper.setSkinValue(view, builder);
builder.release();
skin {
background(R.attr.app_skin_common_background)
border(R.attr.qmui_skin_support_color_separator)
// more...
}
<YourView
...
app:qmui_skin_border="?attr/qmui_skin_support_color_separator"
app:qmui_skin_background="?attr/app_skin_common_background"/>
需要注意的是,QMUISkinValueBuilder
所配置的属性并不是所有 View
都支持的,例如 border
、separator
只支持 IQMUILayout
的实现者。 在不支持的情况下, QMUI
会给出 warn 信息,因此使用 QMUI
时最好调用 QMUILog.setDelegate()
来接收 QMUI
的一些日志信息。
QMUISkinManager.changeSkin(SKIN_2)
首先我们要在 AndroidManifest
里将 uiMode 加入到 Activity
的 configChanges
里
<activity
android:name=".YourActivity"
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|screenLayout|uiMode"
android:windowSoftInputMode="stateAlwaysHidden|adjustResize">
</activity>
然后在 Application.onConfigurationChanged
里根据当前 uiMode 更改 skin
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if((newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES){
// 假设 SKIN_2 为 Dark Mode 下的 skin
QDSkinManager.changeSkin(SKIN_2);
}else if(QDSkinManager.getCurrentSkin() == QDSkinManager.SKIN_DARK){
QDSkinManager.changeSkin(SKIN_1);
}
}
一些通用组件,例如 TopBar, 换肤配置一般都是相同的,没必要每次实例化的时候都配置一次。因而 QMUI 提供了 IQMUISkinDefaultAttrProvider
, 用于提供默认配置项, 以 QMUITopBar
为例:
class QMUITopBar implements IQMUISkinDefaultAttrProvider {
private static SimpleArrayMap<String, Integer> sDefaultSkinAttrs;
static {
sDefaultSkinAttrs = new SimpleArrayMap<>(4);
sDefaultSkinAttrs.put(QMUISkinValueBuilder.BOTTOM_SEPARATOR, R.attr.qmui_skin_support_topbar_separator_color);
sDefaultSkinAttrs.put(QMUISkinValueBuilder.BACKGROUND, R.attr.qmui_skin_support_topbar_bg);
}
@Override
public SimpleArrayMap<String, Integer> getDefaultSkinAttrs() {
return sDefaultSkinAttrs;
}
}
因而, 只需要我们的 skin style 里提供 `qmui_skin_support_topbar_bg`、`qmui_skin_support_topbar_separator_color` 等的值,所有的 `QMUITopBar` 实例都会走这个配置。
如果你有某一个 `QMUITopBar` 实例需要不同的配置,那么通过 `QMUISkinValueBuilder` 覆盖就可。
如果无法重写这个某些 View,也可以通过 QMUISkinHelper.setSkinDefaultProvider()
来设置默认配置, QMUITopBar
的左右按钮实际上就是用这种方式来走默认配置的。
在实现层面上,QMUISkinValueBuilder
实际上是设置换肤应该遵循的一些 rule 以及对应的 值。 QMUISkinManager
里存储了所有 rule 的处理器。
我们可以通过 QMUISkinValueBuilder.setRuleHandler(String name, IQMUISkinRuleHandler handler)
来覆盖默认的处理器, 或者提供新的处理器, 对于新的处理器, 可以通过 QMUISkinValueBuilder.custom(String name, int attr)
设置 rule 以及其对应的配置值。(或许叫 token 更合适?)
QMUI 为许多组件提供了 skin 配置项,因而我们可以在 App 的各个 skin style 以及 AppTheme 里配置 QMUI 组件的肤色。 各个组件的配置项可查看 qmui_themes.xml 的的值,之后也会在 wiki 里组件文档里列举其配置项。