iOS 13+、Android 10+ 提供了暗黑模式/深色模式,之前的模式称为light,暗黑称为dark。
同时也要注意,低于上述版本的手机,系统层没有暗黑模式概念。
在uni-app x中,有3种主题概念:OSTheme、hostTheme、appTheme。每种主题在不同平台支持度不同,获取、设置和监听变化的方式也不同。
| 主题概念 | 描述 | App | Web | 小程序 | 获取方式 | 设置方式 | 监听变化 |
|---|---|---|---|---|---|---|---|
| osTheme | 手机OS的当前主题 | √ | x | x | uni.getDeviceInfo | - | uni.onOsThemeChange |
| hostTheme | 浏览器或小程序宿主的当前主题 | x | √ | √ | uni.getAppBaseInfo | - | uni.onHostThemeChange |
| appTheme | App当前主题 | √ | X | x | uni.getAppBaseInfo | uni.setAppTheme | uni.onAppThemeChange |
Web和小程序注意:
一般情况下,独立设置主题的场景常见于App平台,所以App平台新增了appTheme的概念。appTheme有几个用途:
开发者做主题适配时需要先明确需求,这3种做法,需要做的事情都不一样:
开发者做主题适配时需处理的内容范围,涉及manifest.json、theme.json、pages.json、app.uvue,以及自己的uvue页面。
web 端、小程序需要配置 manifest.json 中 web、mp-weixin 根节点的 "darkmode": true。配置后如果不生效请重新编译运行
{
"mp-weixin": {
"darkmode": true
},
"web": {
"darkmode": true
}
}
pages.json的亮黑设置,需要通过theme.json处理。
要特别注意,适配暗黑模式,在项目根目录下放置theme.json文件是必不可少的环节。
该文件除了处理tabbar和导航栏之外,非常重要的是globalStyle里的页面style的backgroundColorContent属性。
尤其是在小程序下,前端页面设置的背景色生效时间较晚,在页面刚创建并开始动画的时候,页面的原生背景色是浅色,然后前端设置页面背景色为深色,就会出现闪白现象。
所以适配暗黑,就必须要在项目下新建theme.json文件,并且在pages.json的globalStyle里,把页面在dark模式下的背景色统一掉。
然后每个页面的根view或scroll-view,反而不用设背景色,使用globalStyle的backgroundColorContent的配置就好了。
下面是pages.json中的globalStyle设置,在属性值中,通过@来引用theme.json中定义的值:
"globalStyle": {
"navigationBarTextStyle": "@navigationBarTextStyle",
"navigationBarBackgroundColor": "@navigationBarBackgroundColor",
"backgroundColorContent": "@backgroundColorContent",
"backgroundColor": "@backgroundColor",
"backgroundTextStyle": "@backgroundTextStyle"
},
下面是theme.json的样例。theme.json的位置放在pages.json同级目录下。
在light和dark节点下,分别命名一批同名的变量,并分别赋值。这些变量可以在pages.json里直接引用。
{
"light": {
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#007AFF",
"backgroundColor": "#efeff4",
"backgroundColorContent": "#efeff4",
"tabBarPagebackgroundColorContent": "#efeff4",
"backgroundTextStyle": "dark"
},
"dark": {
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#1F1F1F",
"backgroundColor": "#1F1F1F",
"backgroundColorContent": "#646464",
"tabBarPagebackgroundColorContent": "#1F1F1F",
"backgroundTextStyle": "light"
}
}
完整的theme.json教程详见:theme.json
theme.json 里的变量仅能用于 pages.json。uvue页面不能引用。
在web和小程序中,theme.json的dark部分生效的前提是:
darkmode:true在App中,由于appTheme默认值是light,此时theme.json的dark内容不会生效。想要让theme.json的dark内容生效,需要写如下代码:
uni.setAppTheme({"theme": "dark"}) // 显式设置为dark主题。如果给用户提供了独立的应用主题设置,在用户选择暗黑时应执行此代码
uni.setAppTheme({"theme": "auto"}) //跟随OS的主题而变化,设置为auto后,并且osTheme为dark,那么appTheme就会变成dark,此时theme.json里的dark设置才能生效
app.uvue在主题适配中有2个角色:
如下分别讲述。
web和小程序平台可以使用媒体查询来设置,但App平台暂不支持媒体查询。
所以跨端的写法是,在app.uvue里,通过uni.getAppBaseInfo、uni.getDeviceInfo获取自身或上家的主题设置,保存到vue的响应式变量中,模板的class绑定响应式变量实现动态切换class。
如果应用只需要跟随上家,不独立设置主题,那么写法是这样:
// app.uvue
import { state } from '@/store/index.uts'
onLaunch(() => {
console.log('App Launch')
// #ifdef WEB || MP-WEIXIN
state.isDark = (uni.getAppBaseInfo().hostTheme == 'dark')
uni.onHostThemeChange((result) => {
state.isDark = (result.hostTheme == 'dark');
});
// #endif
// 在app平台,跟随osTheme而变化的,就要获取和监听osTheme。写法如下:
// #ifdef APP
uni.setAppTheme({"theme": "auto"}) //默认值是light,必须显示设置为auto才能根据系统自动切换
state.isDark = (uni.getDeviceInfo().osTheme == 'dark')
uni.onOsThemeChange((result:OsThemeChangeResult) => {
state.isDark = (result.osTheme == 'dark');
});
// #endif
})
为了避免每个页面都监听主题change,在app.uvue里的监听结果,存放在一个独立的 store/index.uts 文件中,每个页面引用这个文件,获取当前主题状态、
/store/index.uts 的内容如下:
type State = {
// 是否暗黑主题
isDark: boolean
}
export const state = reactive({
isDark:false
} as State)
实际应用中,state上可以挂载很多东西,此处仅以isDark为例。
如果App平台的主题需要独立设置,即在界面中提供给用户选项,那么App平台的监听对象就不再是osTheme了,而是要改成监听onAppThemeChange,写法是这样:
import { state } from '@/store/index.uts'
onLaunch(() => {
console.log('App Launch')
// #ifdef WEB || MP-WEIXIN
state.isDark = (uni.getAppBaseInfo().hostTheme == 'dark')
uni.onHostThemeChange((result) => {
state.isDark = (result.hostTheme == 'dark');
});
// #endif
// 如果app独立设置主题,就要获取和监听appTheme
// #ifdef APP
state.isDark = (uni.getAppBaseInfo().appTheme == 'dark')
uni.onAppThemeChange((result:AppThemeChangeResult) => {
state.isDark = (result.appTheme == 'dark');
});
// #endif
})
注意:有些平台,os主题变化时会重启App,有些小程序宿主主题变化时会重启小程序,有些则不会。在会重启的场景下,监听上家主题变化其实没有意义。
在全局样式里设置一批css变量,是适配页面主题的好方式。
比如设置如下2个全局class,theme-light和theme-dark,后续在页面中就可以使用它们。(除非页面和组件在样式隔离策略中禁止了全局样式影响)
.theme-light {
--background-color: #f8f8f8;
--text-color: #333333;
}
.theme-dark {
--background-color: #1a1a1a;
--text-color: #ffffff;
}
在根节点的class中,根据state.isDark设置class,让全局样式的theme-dark或theme-light生效。
这2个class又影响了2个css变量 --background-color 和 --text-color 的值。
<template>
<view :class="state.isDark ? 'theme-dark' : 'theme-light'"> <!--根view不需要设背景色,因为页面已经设置过背景色了-->
<text class="title">使用css变量设置color的文字</text>
</view>
</template>
<script setup lang="uts">
import { state } from '@/store/index.uts'
</script>
<style>
.title {
color: var(--text-color); /* 该css变量的值,根据theme-dark和theme-light谁生效而变化*/
}
</style>
如需在App的界面中给用户提供主题选择,可使用如下代码:
<template>
<view :class="state.isDark ? 'theme-dark' : 'theme-light'">
<text class="title">使用css变量设置color的文字,在根view的class中根据state.isDark设置class,影响了不同的css变量值</text>
<text>"state.isDark:" {{state.isDark}}</text>
<!-- #ifdef APP -->
<text>设置AppTheme</text>
<radio-group @change="radioChange">
<radio class="" v-for="(item, index) in appThemeitems" :key="item"
:class="index < appThemeitems.length - 1 ? 'uni-list-cell-line' : ''" :value="item" :checked="index === current">
{{ item }}
</radio>
</radio-group>
<!-- #endif -->
</view>
</template>
<script setup lang="uts">
import { state } from '@/store/index.uts'
const current = ref(0)
const appThemeitems = ref(["light","dark","auto"] as string[])
function radioChange(e : UniRadioGroupChangeEvent) {
const theme = e.detail.value
uni.setAppTheme({
theme: theme as 'light' | 'dark' | 'auto',
success: function () {
console.log("设置appTheme为", theme, "成功")
},
fail: function (e : IAppThemeFail) {
console.log("设置appTheme为", theme, "失败,原因:", e.errMsg)
}
})
uni.showToast({
title: '当前选中:' + theme,
icon: 'none'
})
}
onReady(() => {
current.value = appThemeitems.value.indexOf(uni.getAppBaseInfo().appTheme)
})
</script>
<style>
.title {
color: var(--text-color, #005500);
}
</style>
注意此时app.uvue里监听和获取的都应该是appTheme。
uni-app x的App和Web平台框架中自带的界面,均已适配暗黑模式(小程序平台由小程序宿主自行适配)
uni-app x的内置组件,在App和Web平台均支持css设置所有样式,这样就可以在所有样式控制中使用css变量。但小程序平台的内置组件,依赖其自身实现,有的组件需要通过属性控制样式,此时无法使用css变量。
app.uvue的onLaunch中调用了checkSystemTheme(),该方法来自于/store/index.uts,获取当前的主题设置存放在响应式state.isDarkMode中。
然后在组件components/boolean-data/boolean-data.vue中,设置computed()的isDarkMode,在template中通过响应式变量isDarkMode动态切换class。设置应用主题
uni.setAppTheme,并不会帮助开发者自动实现整个应用的亮/暗主题切换,但是必须写。它的作用是:
当然组件作者也可以不监听onAppThemeChange,而是暴露主题切换API给开发者,由开发者监听主题切换,再调用组件的主题切换API。
uni-app x的UI相关的API(比如showModal),也会响应setAppTheme。
| Web | 微信小程序 | Android | iOS | iOS uni-app x UTS 插件 | HarmonyOS | HarmonyOS(Vapor) |
|---|---|---|---|---|---|---|
| x | x | 4.18 | 4.18 | 4.18 | 4.71 | 5.0 |
| 名称 | 类型 | 必填 | 默认值 | 兼容性 | 描述 | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| options | SetAppThemeOptions | 是 | - | ||||||||||||||||||||||||||||||||||||||||||||||||||
| |||||||||||||||||||||||||||||||||||||||||||||||||||||
| 名称 | 类型 | 必备 | 默认值 | 兼容性 | 描述 |
|---|---|---|---|---|---|
| theme | string | 是 | - |
| 名称 | 类型 | 必备 | 默认值 | 兼容性 | 描述 | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| errCode | number | 是 | - | 错误码 - 702001 参数错误 - 2002000 未知错误 | ||||||||||
| ||||||||||||||
| errSubject | string | 是 | - | 统一错误主题(模块)名称 | ||||||||||
| data | any | 否 | - | 错误信息中包含的数据 | ||||||||||
| cause | Error | 否 | - | - | 源错误信息,可以包含多个错误,详见SourceError | |||||||||
| errMsg | string | 是 | - | |||||||||||
uni.setAppTheme({
theme: "auto",
success: function() {
console.log("设置appTheme为 auto 成功")
},
fail: function(e: IAppThemeFail) {
console.log("设置appTheme为 auto 失败,原因:", e.errMsg)
}
})
开启监听应用主题变化
版本历史调整
| Web | 微信小程序 | Android | iOS | iOS uni-app x UTS 插件 | HarmonyOS | HarmonyOS(Vapor) |
|---|---|---|---|---|---|---|
| x | x | 4.18 | 4.18 | 4.18 | 4.71 | 5.0 |
| 名称 | 类型 | 必填 | 默认值 | 兼容性 | 描述 |
|---|---|---|---|---|---|
| callback | (res: AppThemeChangeResult) => void | 是 | - |
| 名称 | 类型 | 必备 | 默认值 | 兼容性 | 描述 | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| appTheme | string | 是 | - | 应用主题 | ||||||||||
| ||||||||||||||
| 类型 |
|---|
| number |
//callbackId 用于注销监听
val callbackId = uni.onAppThemeChange((res: AppThemeChangeResult) => {
console.log("onAppThemeChange", res.appTheme)
})
取消监听应用主题变化
| Web | 微信小程序 | Android | iOS | iOS uni-app x UTS 插件 | HarmonyOS | HarmonyOS(Vapor) |
|---|---|---|---|---|---|---|
| x | - | 4.18 | 4.18 | 4.18 | 4.71 | 5.0 |
| 名称 | 类型 | 必填 | 默认值 | 兼容性 | 描述 |
|---|---|---|---|---|---|
| id | number | 是 | - |
val callbackId = uni.onAppThemeChange((res: AppThemeChangeResult) => {
console.log("onAppThemeChange", res.appTheme)
})
//...
//...
//注销监听
uni.offAppThemeChange(this.appThemeChangeId)
开启监听系统主题变化
| Web | 微信小程序 | Android | iOS | iOS uni-app x UTS 插件 | HarmonyOS | HarmonyOS(Vapor) |
|---|---|---|---|---|---|---|
| x | x | 4.18 | 4.18 | 4.18 | 4.71 | 5.0 |
| 名称 | 类型 | 必填 | 默认值 | 兼容性 | 描述 |
|---|---|---|---|---|---|
| callback | (res: OsThemeChangeResult) => void | 是 | - |
| 名称 | 类型 | 必备 | 默认值 | 兼容性 | 描述 | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| osTheme | string | 是 | - | 系统主题 | ||||||||||
| ||||||||||||||
| 类型 |
|---|
| number |
//callbackId 用于注销监听
val callbackId = uni.onOsThemeChange((res: OsThemeChangeResult)=> {
console.log("onOsThemeChange---", res.osTheme)
})
注意:
dark,更低版本无法获取、监听OS的主题。取消监听系统主题变化
| Web | 微信小程序 | Android | iOS | iOS uni-app x UTS 插件 | HarmonyOS | HarmonyOS(Vapor) |
|---|---|---|---|---|---|---|
| x | x | 4.18 | 4.18 | 4.18 | 4.71 | 5.0 |
| 名称 | 类型 | 必填 | 默认值 | 兼容性 | 描述 |
|---|---|---|---|---|---|
| id | number | 是 | - |
val callbackId = uni.onOsThemeChange((res: OsThemeChangeResult)=> {
console.log("onOsThemeChange---", res.osTheme)
})
...
...
//注销监听
uni.offOsThemeChange(callbackId)
监听宿主题状态变化。
| Web | 微信小程序 | Android | iOS | HarmonyOS | HarmonyOS(Vapor) |
|---|---|---|---|---|---|
| 4.35 | 4.41 | x | x | 4.71 | 5.0 |
| 名称 | 类型 | 必填 | 默认值 | 兼容性 | 描述 |
|---|---|---|---|---|---|
| callback | (result: OnHostThemeChangeCallbackResult) => void | 是 | - |
| 名称 | 类型 | 必备 | 默认值 | 兼容性 | 描述 | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| hostTheme | string | 是 | - | 主题名称 | ||||||||||
| ||||||||||||||
| 类型 |
|---|
| number |
取消监听宿主题状态变化。
| Web | 微信小程序 | Android | iOS | HarmonyOS | HarmonyOS(Vapor) |
|---|---|---|---|---|---|
| 4.35 | 4.41 | x | x | 4.71 | 5.0 |
| 名称 | 类型 | 必填 | 默认值 | 兼容性 | 描述 |
|---|---|---|---|---|---|
| id | number | 是 | - |
监听系统主题状态变化。 已废弃,在web、小程序上推荐使用 onHostThemeChange
| Web | 微信小程序 | Android | iOS | HarmonyOS | HarmonyOS(Vapor) |
|---|---|---|---|---|---|
| 4.0 | 4.41 | x | x | 4.71 | 5.0 |
| 名称 | 类型 | 必填 | 默认值 | 兼容性 | 描述 |
|---|---|---|---|---|---|
| callback | (result: OnThemeChangeCallbackResult) => void | 是 | - |
| 名称 | 类型 | 必备 | 默认值 | 兼容性 | 描述 | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| theme | string | 是 | - | 主题名称 | ||||||||||
| ||||||||||||||
取消监听系统主题状态变化。 已废弃,在web、小程序上推荐使用 offHostThemeChange
| Web | 微信小程序 | Android | iOS | HarmonyOS | HarmonyOS(Vapor) |
|---|---|---|---|---|---|
| 4.0 | 4.41 | x | x | 4.71 | 5.0 |
| 名称 | 类型 | 必填 | 默认值 | 兼容性 | 描述 |
|---|---|---|---|---|---|
| callback | (result: OnThemeChangeCallbackResult) => void | 是 | - | - |
| 名称 | 类型 | 必备 | 默认值 | 兼容性 | 描述 | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| theme | string | 是 | - | 主题名称 | ||||||||||
| ||||||||||||||
示例为hello uni-app x alpha分支,与最新HBuilderX Alpha版同步。与最新正式版同步的master分支示例另见
示例
<template>
<view class="uni-padding-wrap">
<!-- #ifdef APP -->
<view class="uni-common-mt item-box">
<text>osTheme:</text>
<text id="theme">{{ osTheme }}</text>
</view>
<!-- #endif -->
<view class="uni-common-mt item-box">
<text>应用当前主题:</text>
<text id="theme">{{ appTheme }}</text>
</view>
<!-- #ifdef APP -->
<view>
<view class="uni-title uni-common-mt">
<text class="uni-title-text"> 修改appTheme主题(此处仅为演示API,本应用并未完整适配暗黑模式) </text>
</view>
</view>
<view class="uni-list uni-common-pl">
<radio-group @change="radioChange" class="radio-group">
<radio class="uni-list-cell uni-list-cell-pd radio" v-for="(item, index) in items" :key="item"
:class="index < items.length - 1 ? 'uni-list-cell-line' : ''" :value="item" :checked="index === current">
{{ item }}
</radio>
</radio-group>
</view>
<!-- #endif -->
</view>
</template>
<script setup lang="uts">
const osThemeChangeId = ref(0)
const appThemeChangeId = ref(0)
const osTheme = ref("light" as string)
const appTheme = ref("light" as string)
const originalTheme = ref("light" as string)
const current = ref(0)
const items = ref([
"light",
"dark",
"auto"
] as string[])
function bindOsThemeChange() : number {
//注册osTheme变化监听
return uni.onOsThemeChange((res : OsThemeChangeResult) => {
osTheme.value = res.osTheme
})
}
function bindAppThemeChange() : number {
// #ifdef APP
//注册appTheme变化监听
return uni.onAppThemeChange((res : AppThemeChangeResult) => {
appTheme.value = res.appTheme
})
// #endif
// #ifdef WEB || MP
return uni.onHostThemeChange((res : OnHostThemeChangeCallbackResult) => {
appTheme.value = res.hostTheme
})
// #endif
}
function setAppTheme(value : string) {
uni.setAppTheme({
theme: value as 'light' | 'dark' | 'auto',
success: function () {
console.log("设置appTheme为", value, "成功")
},
fail: function (e : IAppThemeFail) {
console.log("设置appTheme为", value, "失败,原因:", e.errMsg)
}
})
}
function radioChange(e : UniRadioGroupChangeEvent) {
const theme = e.detail.value
setAppTheme(theme)
uni.showToast({
icon: 'none',
title: '当前选中:' + theme,
})
}
onReady(() => {
uni.getSystemInfo({
success: (res : GetSystemInfoResult) => {
// #ifdef APP
osTheme.value = res.osTheme!
originalTheme.value = res.appTheme!
appTheme.value = res.appTheme == "auto" ? res.osTheme! : res.appTheme!
current.value = items.value.indexOf(res.appTheme!)
// #endif
// #ifdef WEB || MP
appTheme.value = res.hostTheme!
// #endif
}
})
// #ifdef APP
osThemeChangeId.value = bindOsThemeChange()
// #endif
appThemeChangeId.value = bindAppThemeChange()
})
onUnload(() => {
//注销监听
// #ifdef APP
uni.offAppThemeChange(appThemeChangeId.value)
uni.offOsThemeChange(osThemeChangeId.value)
// #endif
// #ifdef WEB || MP
uni.offHostThemeChange(appThemeChangeId.value)
// #endif
//暂时屏蔽 避免5.1安卓设备自动化测试不过
// uni.showToast({
// "position": "bottom",
// "title": "已停止监听主题切换"
// })
})
</script>
<style>
.item-box {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.uni-list-cell {
justify-content: flex-start;
}
</style>
| 名称 | 类型 | 必备 | 默认值 | 兼容性 | 描述 |
|---|---|---|---|---|---|
| errMsg | string | 是 | - | 错误信息 |