Android设备在 10 之后提供了对于暗黑模式的支持, iOS也在 iOS13之后提供了同样的支持。React Native 在 0.62 版本中增加了对于暗黑模式功能的支持,用来提升App用户体验。暗黑模式在 0.62 之前及 0.62+的版本如何适配呢?本文就来详细说明在RN环境下暗黑模式的实践方案。
暗黑模式是基于Native端实现。如何在RN端进行功能实施呢?核心实现是:通过直接获取和动态监听两种方式获取 Native 端的主题变化即可。
使用 Native Modules 的同步方法在 JS 端获取当前 theme 值,JS 端方法调用能直接得到 Native 同步方法的返回值,而非一个 Promise。
iOS: 使用 RCTEXPORTSYNCHRONOUSTYPEDMETHOD() 替换 RCTEXPORTMETHOD()(v0.51.0 及以上版本支持Commit)
Android: 在 @ReactMethod annotation 后面添加 (isBlockingSynchronousMethod = true) (v0.42.0 及以上版本支持Commit)
同步方法的缺点是无法在 Debug Remotely 时调用,所以必须在 Debug Remotely 时,提供默认值。
RN事件监听Theme变化可以使用系统提供的EventEmitter来实现
iOS: RCTEventEmitter
Android: RCTDeviceEventEmitter
由于官方在0.62之后才提供了暗黑模式的支持,所以在0.62版本之前, 需要借助第三方库来实现:
我们提供 IBUThemeContext & IBUThemeProvider 两个类供产线获取主题。 Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。IBUThemeContext 是 Context 在 Theme 上的一个应用, IBUThemeProvider 负责同步 Theme 值,并将其传递给 IBUThemeContext.Provider。
// IBUThemeContext
export const IBUThemeContext = React.createContext('light');
//IBUThemeProvider
export class IBUThemeProvider extends Component {
// 引入文件时同步获取一次 theme
static theme = isInChromeDebugMode ? 'dark' : IBUTheme.getTheme();
constructor(props) {
super(props);
// 实例创建时, 再次同步一次theme
const theme = isInChromeDebugMode ? 'dark' : IBUTheme.getTheme();
IBUThemeProvider.theme = theme;
this.state = {
theme,
};
}
render() {
const { theme } = this.state;
const { children } = this.props;
return (
{children}
)
}
}
将IBUThemeProvider 嵌入App 的根节点, 组件树便能通过如下两种方法,获取theme值:
通过IBUThemeProvider.theme 读取全局theme。声明了static contextType=IBUThemeContext 的类中使用 this.context,获取theme值。
在 0.62版本之后,官方支持了 dark mode:
从源码中可以看到,围绕 dark mode 官方给出了3种获取 theme 的方式
(1)Appearance
// The Appearance module exposes information about the user's appearance
// preferences, such as their preferred color scheme (light or dark).
import { Appearance } from 'react-native';
const colorScheme = Appearance.getColorScheme(); light | dark
if (colorScheme === 'dark') {
// Use dark color scheme
}
(2)useColorScheme
import { useColorScheme } from 'react-native';
const MyComponent = () => {
const colorScheme = useColorScheme(); // light | dark | null
};
(3)AppearanceListener
import { Appearance } from 'react-native';
this.themeListener = (colorScheme) => {} // colorScheme: light | dark
Appearance.addChangeListener(this.themeListener);
Appearance.removeChangeListener(this.themeListener);
注意:配色方案的渲染逻辑或样式都应尝试在每个render上调用此函数,而不是缓存值。 例如,您可以使用useColorScheme ,因为它提供并订阅了配色方案更新,或者您可以使用内联样式(Inline Styles,为了提高代码可维护性,不建议将style直接写在render中, 下文会介绍使用 DynamicStyle 方式解决该问题),而不是在 StyleSheet 中设置值。
export class IBUColor {
static red(theme, alpha);
static green(theme, alpha);
}
所有方法均接受 theme 和 alpha 两个可选参数, 方法会先根据 theme 选择对应颜色的 hex 字符串色值,如果 theme 值为空, 则 fallback 到 IBUThemeProvider.theme , 之后再根据 alpha 值计算颜色的的 alpha hex 值,并拼接到 hex 字符串色值之后。如 alpha 为空,则不拼接 hex 色值。最后将对应的 hex 色值字符串返回。
RN端图片之前已经作了统一的静态资源管理.。方式如下:
export const images = {
button: require('./images/button.png'),
logo: require('./images/logo.png'),
}
我们使用 lazy getters 解决 Light/Dark 图片展示的问题, 使用 lazy getters,稍作改造后,即能完美适配:
export const images = {
get button() {
const theme = IBUThemeProvider.theme;
return theme === 'dark' ? require('./images/button_dark.png') : require('./images/button.png');
},
get logo() {
const theme = IBUThemeProvider.theme;
return theme === 'dark' ? require('./images/logo_dark.png') : require('./images/logo.png');
}
}
ReactNative 导出的 StyleSheet 只会在文件引入时,初始化一次,不会随着 App DarkTheme 的变化而变化这就导致系统主题发生变化时,RN 无法更新 styles。为此我们提出 DynamicStyleSheet 来解决该问题。
export function DynamicStyleSheet(callback) {
const cache = {
light: undefined,
dark: undefined,
};
return (theme) => {
const currentTheme = theme || ThemeProvider.theme;
let style = cache[currentTheme];
if (!style) {
style = StyleSheet.create(callback());
cache[currentTheme] = style;
}
return style;
};
}
DynamicStyleSheet 是一个 Function,它接受一个返回值是 style 的 Function 作为参数,并且返回一个 Function。这种 Function 也被称High Order Function。
StyleSheet 创建 style 的代码被包在参数的 Function 中,这样可以保证每次取值都会取到当前的 theme 对应的 style。每次 render 前, 将返回的 Function 执行一次,并将这个 Function 的返回值作为真正的 style 使用。
IBUDynamicStyleSheet 内部对 light 和 dark 下的 style 作了缓存,这样大部分情况下 style 仍然只会被创建一次, theme 发生变化时 style 被创建两次, theme 发生多次变化时,style 最多只被创建两次。
采用DynamicStyleSheet这种方式,代码改动量不仅小, 而且性能损失少, 达到实时切换Theme的目的。
App 开启dark theme
export default class App extends Component{
render(){
return (
// ...
)
}
}
Class Component 接入
class MyClass extends React.Component {
//需要声明contextType, 否则该组件可能不会随theme变化而重新绘制
static contextType = IBUThemeContext;
constructor(props, context) {
super(props, context)
// context can be accessed now, https://github.com/facebook/react/issues/6598
const theme = this.context;
// ....
}
// ...
render() {
const theme = this.context; // 'light'|'dark'
/* render something based on the value of IBUThemeContext */
const styles = dynamicStyles(theme);
return(
{/* render something else */}
)
}
}
const dynamicStyles = IBUDynamicStyleSheet(() => ({
icon: {
backgroundColor: IBUColor.quaternaryGray(),
height: 20,
},
}));
Functional Component接入
export const MyComponent = () => {
const theme = React.useContext(IBUThemeContext); // 'light'|'dark'
const styles = dynamicStyles(theme);
return (
{/* render something else */}
)
}
const dynamicStyles = IBUDynamicStyleSheet(() => ({
icon: {
backgroundColor: IBUColor.quaternaryGray(),
height: 20,
},
}));