React Native 暗黑模式适配实践

Android设备在 10 之后提供了对于暗黑模式的支持, iOS也在 iOS13之后提供了同样的支持。React Native 在 0.62 版本中增加了对于暗黑模式功能的支持,用来提升App用户体验。暗黑模式在 0.62 之前及 0.62+的版本如何适配呢?本文就来详细说明在RN环境下暗黑模式的实践方案。

暗黑模式是基于Native端实现。如何在RN端进行功能实施呢?核心实现是:通过直接获取和动态监听两种方式获取 Native 端的主题变化即可。

1)从 Native 端获取当前的 theme 值

使用 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 时,提供默认值。


2)theme 值变化监听

RN事件监听Theme变化可以使用系统提供的EventEmitter来实现

iOS: RCTEventEmitter

Android: RCTDeviceEventEmitter

 

3)RN业务方调用 theme (0.62之前)

由于官方在0.62之后才提供了暗黑模式的支持,所以在0.62版本之前, 需要借助第三方库来实现:

react-native-dark-mode

我们提供 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值。

4)RN业务方调用 theme (0.62+)

在 0.62版本之后,官方支持了 dark mode: 

React Native 暗黑模式适配实践_第1张图片

从源码中可以看到,围绕 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 中设置值。

5)颜色适配

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 色值字符串返回。

6)图片适配

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

7)DynamicStyle

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的目的。

8)Examples

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

 

你可能感兴趣的:(Android,iOS,React,Native,实践进阶)