首发于公众号 前端混合开发,欢迎关注。

几乎每个网站和移动应用现在都包含一个暗黑模式或者默认颜色方案的替代方案。颜色方案为用户提供了选择他们希望应用设计看起来如何的选项。当这样的选项不可用时,用户可能会感到失望。

色彩方案也帮助我们作为开发者,为每个用户的偏好设计 - 也就是为喜欢浅色模式和喜欢深色模式的用户设计。

在这篇文章中,我们不会深入探讨设计和颜色选择。相反,我们只会关注如何在React Native应用程序中实现主题切换器。这意味着在亮色、暗色和系统模式之间切换或切换,即移动设备的颜色方案。

使用Expo设置你的项目

如果你更喜欢使用Expo开发React Native应用,并且将按照这个教程进行设置,你需要在你的 app.json 文件中做一点小改动。

在你的 app.json 文件中,添加下面的行;

{
  "expo": {
    "userInterfaceStyle": "automatic",
    "ios": {
      "userInterfaceStyle": "automatic"
    },
    "android": {
      "userInterfaceStyle": "automatic"
    }
  }
}

默认样式是 light ,所以我们的配色方案总会返回一个 light 主题。你可以将其更改为 automatic ,如上所示,以适应深色和浅色主题。这使我们能够动态获取设备的配色方案。

理解 useColorScheme 钩子

在我们开始构建我们的主题切换器之前,熟悉我们将在实施过程中频繁遇到和使用的一个钩子 useColorScheme 是非常重要的。

useColorScheme 是一个React Native Hook,使我们能够订阅不同颜色方案的更新。本质上,它提供了访问设备颜色方案的功能,这可能是浅色或深色方案。它返回一个值,显示用户当前偏好的颜色方案。

考虑下面的代码:

/* App.js */

import React from 'react';
import {Text, StyleSheet, useColorScheme, View} from 'react-native';

const App = () => {
  const colorScheme = useColorScheme();
  return (
    <View style={styles.container}>
      <Text>Current Color Scheme: {colorScheme}</Text>
    </View>
  );
};
const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
});
export default App;

输出将显示当前的颜色方案。例如,我的输出显示了一个深色方案,因为那是我偏好的系统主题。

image.png

如果你的设备处于亮色模式,那么你的输出将显示亮色配色方案。

根据配色方案设置文本样式

通过 useColorScheme 返回的值,我们可以设计用户选择深色或浅色模式的情况。让我们来看下面的代码片段:

import React from 'react';
import {Text, StyleSheet, useColorScheme, View} from 'react-native';

const App = () => {
  const colorScheme = useColorScheme();

  return (
    <View style={styles.container}>
      <Text
        style={{
          color: colorScheme === 'light' ? '#000' : '#fff',
        }}>
        Current Color Scheme: {colorScheme}
      </Text>
    </View>
  );
};
const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
});
export default App;

在上述代码中,我们根据颜色方案来设置文本的样式。当用户选择暗黑模式时,我们会用白色来设置文本,使其在暗黑模式下可见。相反,当用户处于亮色模式时,我们会用黑色来设置文本。

我们也可以使用 react-native/Libraries/NewAppScreen 提供的 Color 对象来设置我们文本的样式。 NewAppScreen 是React Native为我们提供的默认组件,作为创建我们屏幕的起点。它的功能就像一个模板,可以按照下面的方式使用:

/* App.js */

import React from 'react';
import {Text, StyleSheet, useColorScheme, View} from 'react-native';
import {Colors} from 'react-native/Libraries/NewAppScreen';

const App = () => {
  const colorScheme = useColorScheme();
  const color = colorScheme === 'light' ? Colors.darker : Colors.lighter;

  return (
    <View style={styles.container}>
      <Text style={{color: color}}>Current Color Scheme: {colorScheme}</Text>
    </View>
  );
};
const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
});
export default App;

在上述代码中,我们导入了 NewAppScreen 模块。使用 Color 对象,它有一组预定义的值或 Colors ,我们将 lighter 颜色设定为用户处于暗黑模式时的文本颜色,而当用户选择亮色模式时,我们将 darker 颜色设定为文本颜色。

image.png

如果我们将设备主题更改为亮色模式,我们的输出将会是这样的:

image.png

动态切换亮暗主题

到目前为止,我们已经探索了如何检查我们设备的当前模式或颜色方案。我们也简要地看到了如何使用 useColorScheme 钩子返回的值来相应地设计我们的应用程序。

在这个部分,我们将研究如何动态切换主题以及如何保持我们主题的当前状态。

首先,让我们安装 async-storage 包。这个包允许我们在设备的本地存储中保存JSON字符串。它的工作方式与Web上的本地存储、会话存储和Cookie类似:

/* npm */
npm install @react-native-async-storage/async-storage

/* yarn */
yarn add @react-native-async-storage/async-storage

/* expo */
npx expo install @react-native-async-storage/async-storage

在你的 App.js 文件中,复制并粘贴下面的代码:

/* App.js */

import React from 'react';
import Home from './src/Home';
import {ThemeProvider} from './context/ThemeContext';

const App = () => {
  return (
    <ThemeProvider>
      <Home />
    </ThemeProvider>
  );
};
export default App;

在上面的代码中,我们导入了两个组件 —— ThemeContextHome。我们的 ThemeContext 组件将包含主题的上下文和当前状态,而 Home 将是我们的主页。

我们将 Home 页面包裹在 ThemeContext 中,因为我们希望主题可以被应用程序的其他部分访问。

创建用于主题管理的 context

接下来,创建一个名为 context 的文件夹。在该文件夹内,创建一个名为 ThemeContext.js 的文件,并输入以下代码:

/* context/ThemeContext.js */

import React, {createContext, useState, useEffect} from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';

const ThemeContext = createContext();

export const ThemeProvider = ({children}) => {
  const [theme, setTheme] = useState('light');

  useEffect(() => {
    // Load saved theme from storage
    const getTheme = async () => {
      try {
        const savedTheme = await AsyncStorage.getItem('theme');
        if (savedTheme) {
          setTheme(savedTheme);
        }
      } catch (error) {
        console.log('Error loading theme:', error);
      }
    };
    getTheme();
  }, []);

  const toggleTheme = newTheme => {
    setTheme(newTheme);
    AsyncStorage.setItem('theme', newTheme)
  };

  return (
    <ThemeContext.Provider value={{theme, toggleTheme}}>
      {children}
    </ThemeContext.Provider>
  );
};
export default ThemeContext;

我们管理我们的状态,并在 ThemeContext.js 文件中使用 async-storage 持久化我们当前的状态值。由于传递给本地存储的字符串除非被修改或删除,否则保持不变,我们总是可以检索最后保存的值,并将其设置为我们当前的主题状态,如上述代码所示。

最后,我们将主题状态和 toggleTheme 函数传递给我们的 ThemeContext.Provider 。这使得它可以被应用程序的其余部分访问,以便我们可以调用 toggleTheme 函数来执行我们的切换。

使用 button 组件检索和切换主题

接下来,让我们创建一个 Home.js 文件,并复制以下代码:

/* Home */

import React, {useContext} from 'react';
import {View, Text, StyleSheet, TouchableOpacity} from 'react-native';
import ThemeContext from '../context/ThemeContext';

const Home = () => {
  const {theme, toggleTheme} = useContext(ThemeContext);

  const handleToggleTheme = () => {
    const newTheme = theme === 'light' ? 'dark' : 'light';
    toggleTheme(newTheme);
  };

  const styles = StyleSheet.create({
    container: {
      flex: 1,
      alignItems: 'center',
      justifyContent: 'center',
      backgroundColor: theme === 'dark' ? 'black' : 'white',
    },
    text: {
      color: theme === 'dark' ? 'white' : 'black',
    },
    button: {
      color: theme === 'dark' ? 'black' : 'white',
    },
  });

  return (
    <View style={styles.container}>
      <Text style={styles.text}>Home page</Text>
      <TouchableOpacity
        onPress={handleToggleTheme}
        style={{
          marginTop: 10,
          paddingVertical: 5,
          paddingHorizontal: 10,
          backgroundColor: theme === 'dark' ? '#fff' : '#000',
        }}>
        <Text style={styles.button}>
          Switch to {theme === 'light' ? 'Dark' : 'Light'} Theme
        </Text>
      </TouchableOpacity>
    </View>
  );
};
export default Home;

在上述代码中,我们正在获取我们传递给Context API的 theme 值和 toggleTheme 函数。使用返回的值,我们可以根据 theme 值设计我们的页面样式。我们还将 toggleTheme 函数传递给我们的 button 组件。

自动将应用主题适应系统设置

我们现在已经看到了如何在主题之间切换,特别是在亮色和暗色模式之间。最后一步是检测系统的颜色方案,并根据此准确地切换应用程序的主题。

为了实现这一点,我们将修改上面已有的代码。在你的 ThemeContext.js 文件中,复制并粘贴以下代码:

/* ThemeContext */

import React, {createContext, useState, useEffect} from 'react';
import {useColorScheme} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';

const ThemeContext = createContext();

export const ThemeProvider = ({children}) => {
  const colorScheme = useColorScheme();
  const [theme, setTheme] = useState(colorScheme || 'light');

  useEffect(() => {
    // Load saved theme from storage
    const getTheme = async () => {
      try {
        const savedTheme = await AsyncStorage.getItem('theme');
        if (savedTheme) {
          setTheme(savedTheme);
        }
      } catch (error) {
        console.log('Error loading theme:', error);
      }
    };
    getTheme();
  }, []);

  useEffect(() => {
    // set theme to system selected theme
    if (colorScheme) {
      setTheme(colorScheme);
    }
  }, [colorScheme]);

  const toggleTheme = newTheme => {
    setTheme(newTheme);
    // Save selected theme to storage
    AsyncStorage.setItem('theme', newTheme);
  };

  return (
    <ThemeContext.Provider value={{theme, toggleTheme}}>
      {children}
    </ThemeContext.Provider>
  );
};
export default ThemeContext;

在上述代码中,当我们选择一个主题时,我们也会将其保存到我们的本地存储中。这意味着当我们的应用程序加载时,我们可以检查本地存储中是否存在一个现有的主题。如果存在,那么我们将设置该主题为我们的首选主题。否则,我们将不设置任何主题。

生成的React Native主题切换应用将如下所示:

在某些情况下,我们可能想要给用户提供多种主题选项,例如:

  • Dark theme 深色主题
  • Light theme 浅色主题
  • System theme 系统主题

例如,一些用户可能在他们的移动设备上使用浅色主题,但他们可能更喜欢并希望使用某个应用程序的深色主题。在这种情况下,我们不想为应用程序使用系统的浅色主题。相反,我们希望指定一个深色主题。

要做到这一点,将下面的代码复制到你的 ThemeContext.js 文件中:

/* ThemeContext.js */

  ....

  const toggleTheme = (newTheme) => {
    setTheme(newTheme);
    AsyncStorage.setItem('theme', newTheme); // Save selected theme to storage
  };

  const useSystemTheme = () => {
    setTheme(colorScheme);
    AsyncStorage.setItem('theme', colorScheme);
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme, useSystemTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};
export default ThemeContext;

第一个功能允许我们在深色和浅色主题之间切换,正如我们在其他示例中看到的那样。第二个功能允许我们使用系统或设备的默认主题 - 我们将我们的主题设置为设备在该点的主题。

然后,在你的 Home.js 文件中,我们将创建三个按钮来指定每个主题。请参阅下面的代码:

/*Home.js*/

....imports....

const Home = () => {
  const systemTheme = useColorScheme();
  const { theme, toggleTheme, useSystemTheme } = useContext(ThemeContext);

const styles = StyleSheet.create({
  ......
  });

  return (
    <View style={styles.container}>
      <Text style={styles.text}>Current Theme: {theme}</Text>
      <Text style={styles.text}>System Theme: {systemTheme}</Text>
      <TouchableOpacity
        onPress={() => toggleTheme('light')}
        style={{
          marginTop: 10,
          paddingVertical: 5,
          paddingHorizontal: 10,
          backgroundColor: theme === 'dark' ? '#fff' : '#000',
        }}
      >
        <Text style={styles.button}>Light Theme</Text>
      </TouchableOpacity>
      <TouchableOpacity
        onPress={() => toggleTheme('dark')}
        style={{
          marginTop: 20,
          paddingVertical: 5,
          paddingHorizontal: 10,
          backgroundColor: theme === 'dark' ? '#fff' : '#000',
        }}
      >
        <Text style={styles.button}>Dark Theme</Text>
      </TouchableOpacity>
      <TouchableOpacity
        onPress={() => useSystemTheme()}
        style={{
          marginTop: 20,
          paddingVertical: 5,
          paddingHorizontal: 10,
          backgroundColor: theme === 'dark' ? '#fff' : '#000',
        }}
      >
        <Text style={styles.button}>System Theme</Text>
      </TouchableOpacity>
    </View>
  );
};
export default Home;

然后,在你的 Home.js 文件中,我们将创建三个按钮来指定每个主题。请参阅下面的代码:

/*Home.js*/

....imports....

const Home = () => {
  const systemTheme = useColorScheme();
  const { theme, toggleTheme, useSystemTheme } = useContext(ThemeContext);

const styles = StyleSheet.create({
  ......
  });

  return (
    <View style={styles.container}>
      <Text style={styles.text}>Current Theme: {theme}</Text>
      <Text style={styles.text}>System Theme: {systemTheme}</Text>
      <TouchableOpacity
        onPress={() => toggleTheme('light')}
        style={{
          marginTop: 10,
          paddingVertical: 5,
          paddingHorizontal: 10,
          backgroundColor: theme === 'dark' ? '#fff' : '#000',
        }}
      >
        <Text style={styles.button}>Light Theme</Text>
      </TouchableOpacity>
      <TouchableOpacity
        onPress={() => toggleTheme('dark')}
        style={{
          marginTop: 20,
          paddingVertical: 5,
          paddingHorizontal: 10,
          backgroundColor: theme === 'dark' ? '#fff' : '#000',
        }}
      >
        <Text style={styles.button}>Dark Theme</Text>
      </TouchableOpacity>
      <TouchableOpacity
        onPress={() => useSystemTheme()}
        style={{
          marginTop: 20,
          paddingVertical: 5,
          paddingHorizontal: 10,
          backgroundColor: theme === 'dark' ? '#fff' : '#000',
        }}
      >
        <Text style={styles.button}>System Theme</Text>
      </TouchableOpacity>
    </View>
  );
};
export default
 Home;

通过这三个按钮,我们可以选择我们想要的任何主题。上述代码还允许我们查看我们应用程序的当前主题——换句话说,我们当前选择的任何主题——以及我们系统的主题,这可能与我们当前的主题不同。

有了这个,我们可以选择自动使用我们系统的主题,或者为我们的应用选择一个不同的主题:

image.png

如你所见,即使在关闭和重新打开应用程序后,我们选择的主题仍会保持不变。

总结

在这篇文章中,我们已经看到如何实现一个切换功能,保持每个选择的状态,并根据用户的系统选择切换主题。

在今天的应用开发中,切换主题是一项常见功能。让用户选择他们喜欢的主题,并在用户关闭应用后仍然保持,这可以提升用户体验,使你的应用更具吸引力。

完整的项目代码已在GitLab上公开。如果你还有任何疑问,欢迎在下方留言。否则,我们下篇文章见,再见!

交流

首发于公众号 大迁世界,欢迎关注。📝 每周一篇实用的前端文章 🛠️ 分享值得关注的开发工具 ❓ 有疑问?我来回答

本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试完整考点、资料以及我的系列文章。


王大冶
68.1k 声望105k 粉丝