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处理。
要特别注意,适配暗黑模式,如果要适配pages.json中的tabbar和Navigationbar,需放置theme.json文件。
下面是pages.json中的globalStyle设置,在属性值中,通过@来引用theme.json中定义的值:
"globalStyle": {
"navigationBarTextStyle": "@navigationBarTextStyle",
"navigationBarBackgroundColor": "@navigationBarBackgroundColor",
"backgroundColor": "@backgroundColor",
"backgroundTextStyle": "@backgroundTextStyle"
},
下面是theme.json的样例。theme.json的位置放在pages.json同级目录下。
在light和dark节点下,分别命名一批同名的变量,并分别赋值。这些变量可以在pages.json里直接引用。
{
"light": {
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#007AFF",
"backgroundColor": "#efeff4",
"tabBarPagebackgroundColorContent": "#efeff4",
"backgroundTextStyle": "dark"
},
"dark": {
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#1F1F1F",
"backgroundColor": "#1F1F1F",
"tabBarPagebackgroundColorContent": "#1F1F1F",
"backgroundTextStyle": "light"
}
}
完整的theme.json教程详见:theme.json
theme.json 里的变量仅能用于 pages.json。uvue页面不能引用。
在web和小程序中,theme.json的dark部分生效的前提是:
darkmode:true在App中,可以通过manifest.json的app.defaultAppTheme配置应用默认主题,可取值为light、dark、auto,默认值为light。配置为auto时,appTheme会跟随OS主题变化。
{
"app": {
"defaultAppTheme": "auto"
}
}
如果应用为用户提供主题切换功能,可以在运行时通过uni.setAppTheme设置light、dark或auto。
完成上述基础配置后,页面和组件的主题样式可以根据平台及版本选择以下一种方案。
以下平台可以使用@media (prefers-color-scheme: light)和@media (prefers-color-scheme: dark)设置主题样式:
在支持上述能力的平台,推荐优先使用媒体查询适配主题。
媒体查询会根据hostTheme或appTheme自动匹配。主题变化时,匹配的样式也会自动更新,无需监听主题变化、维护响应式变量或动态切换class。详见@media媒体查询。
页面只需要使用固定的class,通过媒体查询分别定义亮色和暗色样式:
<template>
<view class="page">
<text class="title">根据当前主题显示不同颜色的文字</text>
</view>
</template>
<style>
@media (prefers-color-scheme: light) {
.page {
--text-color: #333333;
}
}
@media (prefers-color-scheme: dark) {
.page {
--text-color: #ffffff;
}
}
.title {
color: var(--text-color);
}
</style>
媒体查询和page选择器,可以在app.uvue里使用,从而实现所有页面的效果批量控制。除非页面配置了禁止全局样式影响。
如果App平台需要为用户提供light、dark、auto选项,直接调用uni.setAppTheme即可。appTheme变化后,媒体查询会自动更新匹配的样式。
以下App平台场景,由于不支持媒体查询,只能通过API使用监听主题变化,然后动态切换class:
在已支持媒体查询的平台,如果业务逻辑还需要读取当前主题状态,可以另外使用主题API获取和监听,但样式仍可使用媒体查询,无需动态切换class。
为了在同一套代码中兼容上述App平台场景,以下示例在各端统一使用动态class。为避免每个页面都监听主题变化,可以在app.uvue中获取并监听主题,将结果存放在store/index.uts中,供各页面使用。
如果应用只需要跟随上家,不独立设置主题,App平台需要先将app.defaultAppTheme配置为auto,然后可以这样处理:
// app.uvue
import { state } from '@/store/index.uts'
onLaunch(() => {
// #ifdef WEB || MP-WEIXIN
state.isDark = (uni.getAppBaseInfo().hostTheme == 'dark')
uni.onHostThemeChange((result) => {
state.isDark = (result.hostTheme == 'dark')
})
// #endif
// #ifdef APP
state.isDark = (uni.getDeviceInfo().osTheme == 'dark')
uni.onOsThemeChange((result: OsThemeChangeResult) => {
state.isDark = (result.osTheme == 'dark')
})
// #endif
})
store/index.uts的内容如下:
type State = {
// 是否为暗黑主题
isDark: boolean
}
export const state = reactive({
isDark: false
} as State)
如果App平台允许用户独立设置主题,则需要获取和监听appTheme:
// app.uvue
import { state } from '@/store/index.uts'
onLaunch(() => {
// #ifdef WEB || MP-WEIXIN
state.isDark = (uni.getAppBaseInfo().hostTheme == 'dark')
uni.onHostThemeChange((result) => {
state.isDark = (result.hostTheme == 'dark')
})
// #endif
// #ifdef APP
const appTheme = uni.getAppBaseInfo().appTheme
state.isDark = appTheme == 'auto'
? uni.getDeviceInfo().osTheme == 'dark'
: appTheme == 'dark'
uni.onAppThemeChange((result: AppThemeChangeResult) => {
state.isDark = (result.appTheme == 'dark')
})
// #endif
})
可以在app.uvue的全局样式中定义亮色和暗色class。除非页面或组件的样式隔离策略禁止全局样式影响,否则页面可以直接使用这些class。
.theme-light {
--text-color: #333333;
}
.theme-dark {
--text-color: #ffffff;
}
页面根节点根据state.isDark动态切换class:
<template>
<view :class="state.isDark ? 'theme-dark' : 'theme-light'">
<text class="title">根据当前主题显示不同颜色的文字</text>
</view>
</template>
<script setup lang="uts">
import { state } from '@/store/index.uts'
</script>
<style>
.title {
color: var(--text-color);
}
</style>
动态切换class,由于要执行script,性能没有媒体查询高。另外动态切换class的代码,和theme.json的执行可能不在同一帧,会造成theme.json先生效、class后生效,有闪烁感。推荐升级到5.25的蒸汽模式,使用媒体查询方案适配主题。
uni-app x的App和Web平台框架中自带的界面,均已适配暗黑模式(小程序平台由小程序宿主自行适配)
uni-app x的内置组件,在App和Web平台均支持css设置所有样式,这样就可以在所有样式控制中使用css变量。但小程序平台的内置组件,依赖其自身实现,有的组件需要通过属性控制样式,此时无法使用css变量。
app.uvue、页面和组件样式中使用@media (prefers-color-scheme: light)和@media (prefers-color-scheme: dark)定义不同主题下的样式,pages/CSS/prefers-color-scheme页面提供了完整示例。设置应用主题
uni.setAppTheme用于设置App当前主题。开发者仍需为不同主题定义相应的页面和组件样式。它的作用是:
@media (prefers-color-scheme: light)和@media (prefers-color-scheme: dark)匹配的样式当然组件作者也可以不监听onAppThemeChange,而是暴露主题切换API给开发者,由开发者监听主题切换,再调用组件的主题切换API。
uni-app x的UI相关的API(比如showModal),也会响应setAppTheme。
| Web | 微信小程序 | Android | iOS | HarmonyOS |
|---|---|---|---|---|
| x | x | 4.18 | 4.18 | 4.71 |
| 名称 | 类型 | 必填 | 兼容性 | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 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 | HarmonyOS |
|---|---|---|---|---|
| x | x | 4.18 | 4.18 | 4.71 |
| 名称 | 类型 | 必填 | 兼容性 |
|---|---|---|---|
| callback | (res: AppThemeChangeResult) => void | 是 |
| 名称 | 类型 | 必备 | 兼容性 | 描述 | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| appTheme | string | 是 | 应用主题 | ||||||||||
| |||||||||||||
| 类型 |
|---|
| number |
//callbackId 用于注销监听
val callbackId = uni.onAppThemeChange((res: AppThemeChangeResult) => {
console.log("onAppThemeChange", res.appTheme)
})
取消监听应用主题变化
| Web | Android | iOS | HarmonyOS |
|---|---|---|---|
| x | 4.18 | 4.18 | 4.71 |
| 名称 | 类型 | 必填 | 兼容性 |
|---|---|---|---|
| id | number | 是 |
val callbackId = uni.onAppThemeChange((res: AppThemeChangeResult) => {
console.log("onAppThemeChange", res.appTheme)
})
//...
//...
//注销监听
uni.offAppThemeChange(this.appThemeChangeId)
开启监听系统主题变化
| Web | 微信小程序 | Android | iOS | HarmonyOS |
|---|---|---|---|---|
| x | x | 4.18 | 4.18 | 4.71 |
| 名称 | 类型 | 必填 | 兼容性 |
|---|---|---|---|
| callback | (res: OsThemeChangeResult) => void | 是 |
| 名称 | 类型 | 必备 | 兼容性 | 描述 | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| osTheme | string | 是 | 系统主题 | ||||||||||
| |||||||||||||
| 类型 |
|---|
| number |
//callbackId 用于注销监听
val callbackId = uni.onOsThemeChange((res: OsThemeChangeResult)=> {
console.log("onOsThemeChange---", res.osTheme)
})
注意:
dark,更低版本无法获取、监听OS的主题。取消监听系统主题变化
| Web | 微信小程序 | Android | iOS | HarmonyOS |
|---|---|---|---|---|
| x | x | 4.18 | 4.18 | 4.71 |
| 名称 | 类型 | 必填 | 兼容性 |
|---|---|---|---|
| id | number | 是 |
val callbackId = uni.onOsThemeChange((res: OsThemeChangeResult)=> {
console.log("onOsThemeChange---", res.osTheme)
})
...
...
//注销监听
uni.offOsThemeChange(callbackId)
监听宿主题状态变化。
| Web | 微信小程序 | Android | iOS | HarmonyOS |
|---|---|---|---|---|
| 4.35 | 4.41 | x | x | 4.71 |
| 名称 | 类型 | 必填 | 兼容性 |
|---|---|---|---|
| callback | (result: OnHostThemeChangeCallbackResult) => void | 是 |
| 名称 | 类型 | 必备 | 兼容性 | 描述 | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| hostTheme | string | 是 | 主题名称 | ||||||||||
| |||||||||||||
| 类型 |
|---|
| number |
取消监听宿主题状态变化。
| Web | 微信小程序 | Android | iOS | HarmonyOS |
|---|---|---|---|---|
| 4.35 | 4.41 | x | x | 4.71 |
| 名称 | 类型 | 必填 | 兼容性 |
|---|---|---|---|
| id | number | 是 |
监听系统主题状态变化。 已废弃,在web、小程序上推荐使用 onHostThemeChange
| Web | 微信小程序 | Android | iOS | HarmonyOS |
|---|---|---|---|---|
| 4.0 | 4.41 | x | x | 4.71 |
| 名称 | 类型 | 必填 | 兼容性 |
|---|---|---|---|
| callback | (result: OnThemeChangeCallbackResult) => void | 是 |
| 名称 | 类型 | 必备 | 兼容性 | 描述 | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| theme | string | 是 | 主题名称 | ||||||||||
| |||||||||||||
取消监听系统主题状态变化。 已废弃,在web、小程序上推荐使用 offHostThemeChange
| Web | 微信小程序 | Android | iOS | HarmonyOS |
|---|---|---|---|---|
| 4.0 | 4.41 | x | x | 4.71 |
| 名称 | 类型 | 必填 | 兼容性 |
|---|---|---|---|
| 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">{{ data.osTheme }}</text>
</view>
<!-- #endif -->
<view class="uni-common-mt item-box">
<text>应用当前主题:</text>
<text id="theme">{{ data.appTheme }}</text>
</view>
<!-- #ifdef APP -->
<view>
<view class="uni-title uni-common-mt">
<text class="uni-title-text"> 修改appTheme主题(此处仅为演示API,本应用并未完整适配暗黑模式) </text>
</view>
</view>
<enum-data :items="data.items" title="appTheme" @change="radioChange"></enum-data>
<!-- #endif -->
</view>
</template>
<script setup lang="uts">
import { ItemType } from '@/components/enum-data/enum-data-types'
type Data = {
osThemeChangeId: number;
appThemeChangeId: number;
osTheme: string;
appTheme: string;
originalTheme: string;
current: number;
items: ItemType[];
}
const data = reactive({
osThemeChangeId: 0,
appThemeChangeId: 0,
osTheme: 'light',
appTheme: 'light',
originalTheme: 'light',
current: 0,
items: [
{ value: 0, name: 'light', checked: false },
{ value: 1, name: 'dark', checked: false },
{ value: 2, name: 'auto', checked: false }
] as ItemType[]
} as Data)
function bindOsThemeChange() : number {
return uni.onOsThemeChange((res : OsThemeChangeResult) => {
data.osTheme = res.osTheme
})
}
function bindAppThemeChange() : number {
// #ifdef APP
return uni.onAppThemeChange((res : AppThemeChangeResult) => {
data.appTheme = res.appTheme
})
// #endif
// #ifdef WEB || MP
return uni.onHostThemeChange((res : OnHostThemeChangeCallbackResult) => {
data.appTheme = 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(value : number) {
const theme = data.items[value].name
setAppTheme(theme)
}
onReady(() => {
uni.getSystemInfo({
success: (res : GetSystemInfoResult) => {
// #ifdef APP
data.osTheme = res.osTheme!
data.originalTheme = res.appTheme!
data.appTheme = res.appTheme == 'auto' ? res.osTheme! : res.appTheme!
data.current = data.items.findIndex((item : ItemType) : boolean => {
const currentItem = item.name == res.appTheme!
if (currentItem) {
item.checked = true
}
return currentItem
})
// #endif
// #ifdef WEB || MP
data.appTheme = res.hostTheme!
// #endif
}
})
// #ifdef APP
data.osThemeChangeId = bindOsThemeChange()
// #endif
data.appThemeChangeId = bindAppThemeChange()
})
onUnload(() => {
// #ifdef APP
uni.offAppThemeChange(data.appThemeChangeId)
uni.offOsThemeChange(data.osThemeChangeId)
// #endif
// #ifdef WEB || MP
uni.offHostThemeChange(data.appThemeChangeId)
// #endif
})
defineExpose({
data,
setAppTheme,
radioChange
})
</script>
<style>
.item-box {
display: flex;
flex-direction: row;
justify-content: space-between;
}
</style>
| 名称 | 类型 | 必备 | 描述 |
|---|---|---|---|
| errMsg | string | 是 | 错误信息 |