Skip to content

QMUI 换肤

cgspine edited this page Mar 9, 2020 · 2 revisions

QMUI版本要求: v2.0.0-alpha05+

官方 Android 10 Dark Mode 适配方案

Android 10 提供了 Dark Mode 适配提供的 API:

  1. 提供了 values-nightdrawable-night 资源目录,与我们做屏幕适配一样,App 会根据不同模式去不同文件夹取资源。
  2. configChange 里加入了 uiMode,因而我们可以通过 onConfigurationChanged 来监听夜间模式的打开和关闭,从而做一些自定义的处理。
  3. css 支持 prefers-color-scheme 媒体查询,从而支持 Webview 内容的夜间模式切换。

夜间模式只是配置的一种,其设计思路同横竖屏旋转等走相同的方案:

  1. Android 官方团队认为 UI 应该是需要时创建,并且可以随意销毁重建的,因此在默认情况下,夜间模式切换后, Activity 是会被销毁而后重建的,这样重新从资源文件夹里按当前配置而实现夜间模式,这就是第一个 API 存在的意义了。
  2. 而某些场景,例如视频播放,我们可能要自定义做一些处理,那我们就可以通过配置 configChanges, 不销毁 Activity 而走 onConfigurationChanged 通知,完全由业务方接管, 这便是第二个 API 存在的意义了。

Activity 销毁重建在一些比较轻量的 UI 上效果很好,但是如果 UI 比较重,或者 Activity 没有处理数据状态保存于恢复工作的话,重建 Activity 显得有点笨拙,甚至可能出现界面黑屏的现象,体验不是很好。

QMUI 换肤提供的 API

  1. QMUISkinManager: 存储肤色配置,并且派发当前肤色给它管理的ActivityFragmentDialogPopupWindow。 它通过 QMUISkinManager.of(name, context) 获取,是可以多实例的。 因而一个 App 可以在不同场景执行不同的换肤管理, 例如阅读产品阅读器的换肤和其它业务模块 uiMode 切换的区分管理。
  2. QMUISkinValueBuilder: 用于构建一个 View 实例的换肤配置(textColor、background、border、separator等)
  3. QMUISkinHelper: 一些辅助工具方法,最常用的为 QMUISkinHelper.setSkinValue(View, QMUISkinValueBuilder),将 QMUISkinValueBuilder 的配置应用到一个 View 实例。 如果使用 kotlin 语言,可以通过 View.skin { ... } 来配置 View 实例。
  4. QMUISkinLayoutInflaterFactory: 用于支持 xml 换肤配置项解析。
  5. IQMUISkinDispatchInterceptor: View 可以通过实现它,来拦截 skin 更改的派发。
  6. IQMUISkinHandlerView: View 可以通过实现它,来完全自定义不同 skin 的处理。
  7. IQMUISkinDefaultAttrProvider: View 可以通过实现它, 提供 View 默认的默认换肤配置,从组件层面提供换肤支持。

QMUI 换肤流程

1. 定义 attr 以及其实现 style

这一步需要我们与设计师协作,整理一套颜色、背景资源等供 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。

2. 向 QMUISkinManager 里添加 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);

3. 向 UI 界面注入 QMUISkinManager

1. 向 Activity 注入 QMUISkinManager

QMUIFragmentActivityQMUIActivity 默认注入了默认的 QMUISkinManager, 如果你需要更改 QMUISkinManager, 通过 setSkinManager 更改:

class MyActivity extend QMUIActivity{
     @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // setSkinManager(null); // 这样可以去除这个 Activity 的换肤支持
        setSkinManager(...);
    }
} 

QMUIFragmentActivityQMUIActivity 也默认使用 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);
        }
    }
}

2. 向 Fragment 注入 QMUISkinManager

如果 Activity 注入了 QMUISkinManager,那么它所管理的 Fragment 都会归这个 QMUISkinManager 管理。

如果你想以 Fragment 为单位管理来使用 QMUISkinManager,可以参照 Activity 来注入 QMUISkinManager

3. 向 QMUIDialog 注入 QMUISkinManager

 new QMUIDialog.CheckableDialogBuilder(getActivity())
    .setSkinManager(QMUISkinManager.of(name, context))
    .xxx
    .build()
    .show()

MessageDialogBuilderMenuDialogBuilder 等使用类似。

4. 向 QMUIBottomSheet 注入 QMUISkinManager

 new QMUIBottomSheet.BottomListSheetBuilder(getContext())
    .setSkinManager(QMUISkinManager.of(name, context))
    .xxx
    .build()
    .show()

BottomGridSheetBuilder 使用类似。

5. 向 QMUIPopup 注入 QMUISkinManager

QMUIPopups.popup(context, width)
    .skinManager(QMUISkinManager.of(name, context))
    .xxx
    .show(v);

QMUIFullScreenPopupQMUIQuickAction 使用类似。

4. 为 View 配置 skin

配置项 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

1. 通过 java 配置

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();

2. 通过 kotlin 配置

skin {
    background(R.attr.app_skin_common_background)
    border(R.attr.qmui_skin_support_color_separator)
    // more...
}

3. 通过 xml 配置

<YourView
  ...
   app:qmui_skin_border="?attr/qmui_skin_support_color_separator"
   app:qmui_skin_background="?attr/app_skin_common_background"/>

需要注意的是,QMUISkinValueBuilder 所配置的属性并不是所有 View 都支持的,例如 borderseparator 只支持 IQMUILayout 的实现者。 在不支持的情况下, QMUI 会给出 warn 信息,因此使用 QMUI 时最好调用 QMUILog.setDelegate() 来接收 QMUI 的一些日志信息。

5. 通知 skin 更改

QMUISkinManager.changeSkin(SKIN_2)

6. 适配 Dark Mode

首先我们要在 AndroidManifest 里将 uiMode 加入到 ActivityconfigChanges

<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);
    }
}

QMUI 换肤扩展

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 的左右按钮实际上就是用这种方式来走默认配置的。

2. 自定义 RuleHandler

在实现层面上,QMUISkinValueBuilder 实际上是设置换肤应该遵循的一些 rule 以及对应的 值。 QMUISkinManager 里存储了所有 rule 的处理器。

我们可以通过 QMUISkinValueBuilder.setRuleHandler(String name, IQMUISkinRuleHandler handler) 来覆盖默认的处理器, 或者提供新的处理器, 对于新的处理器, 可以通过 QMUISkinValueBuilder.custom(String name, int attr) 设置 rule 以及其对应的配置值。(或许叫 token 更合适?)

QMUI 组件换肤配置

QMUI 为许多组件提供了 skin 配置项,因而我们可以在 App 的各个 skin style 以及 AppTheme 里配置 QMUI 组件的肤色。 各个组件的配置项可查看 qmui_themes.xml 的的值,之后也会在 wiki 里组件文档里列举其配置项。