React Native初心者だけど、React Native Calendarをダークモードに対応させるために色々頑張った話。
顔面神経麻痺ケアアプリで使っているのだが、テーマを動的に変化させることができないのだ。
RNCの再レンダリング
まず最初にやったのは、RNCを再レンダリングする方法。
RNCはレンダリングした時点のテーマで表示されるので、アピアランスが切り替わるたびにRNCを再レンダリングするというゴリ押しな方法。
import React, { useState } from 'react';
import { View, Appearance } from 'react-native';
import { useTheme, Button } from 'react-native-paper';
import { CalendarList } from 'react-native-calendars';
const HomeScreen = ({ navigation }) => {
const theme = useTheme();
const [calendarKey, setCalendarKey] = useState(0);
const reloadCalendar = () => {
setTimeout(() => {setCalendarKey(calendarKey + 1)},100);
};
const changeAppearance = Appearance.addChangeListener(reloadCalendar);
// changeAppearance.remove();
return (
<View>
<CalendarList
key={calendarKey}
horizontal={true}
pagingEnabled={true}
theme={{
calendarBackground: theme.colors.dynamic.surface,
textSectionTitleColor: theme.colors.dynamic.onSurface,
textSectionTitleDisabledColor: theme.colors.dynamic.onSurfaceDisabled,
monthTextColor: theme.colors.dynamic.onSurface,
arrowColor: theme.colors.dynamic.primary,
}}
/>
<Button onPress={reloadCalendar}>今日</Button>
</View>
);
};
export default HomeScreen;
タイミングがシビアで、普通にやるとOSがダークモードになるとRNCがライトに、OSがライトモードになるとRNCがダークになってしまうことがある。
そこでsetTimeout
を使って0.1秒遅延させるというもはやクソコードだが、他に方法が思いつかないのだ。
DynamicColorIOS
iOSの場合DynamicColorIOS
を使うという方法があるらしい。light
、dark
、highContrastLight
など複数の色を指定しておくと、OS側が勝手に必要な色を選んで表示してくれる。
Appスイッチャーでも色が切り替わるし、切り替え時には美しいディゾルブトランジションが見れる。
少し実験したらRNCでも使える。
なんだこれ最高じゃないか!?
このアプリではApp.js
でMaterial Design 3を上書きする形で2種類のカラースキームを作って切り替えている。
iOS用にもう1つ作らなければならないのは正直面倒だが、DynamicColorIOS
はやってみる価値がある。
import React from 'react';
import { useColorScheme, Platform, DynamicColorIOS } from 'react-native';
import { MD3LightTheme, MD3DarkTheme, adaptNavigationTheme, Provider as PaperProvider } from 'react-native-paper';
import { NavigationContainer, DarkTheme as NavigationDarkTheme, DefaultTheme as NavigationDefaultTheme, } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import HomeScreen from './screens/HomeScreen';
const Stack = createNativeStackNavigator();
export default function App() {
const colorScheme = useColorScheme();
const { LightTheme, DarkTheme } = adaptNavigationTheme({
reactNavigationLight: NavigationDefaultTheme,
reactNavigationDark: NavigationDarkTheme,
});
const customLightColors = {
"primary": "rgb(0, 102, 137)",
"onPrimary": "rgb(255, 255, 255)",
"primaryContainer": "rgb(195, 232, 255)",
"onPrimaryContainer": "rgb(0, 30, 44)",
"secondary": "rgb(0, 106, 106)",
"onSecondary": "rgb(255, 255, 255)",
"secondaryContainer": "rgb(111, 247, 246)",
"onSecondaryContainer": "rgb(0, 32, 32)",
// ...
// 省略
// ...
"border": "rgb(192, 199, 205)",
"card": "#ebf2f8",
"notification": "rgb(186, 26, 26)",
"text": "rgb(25, 28, 30)",
}
const customDarkColors = {
"primary": "rgb(76, 218, 218)",
"onPrimary": "rgb(0, 55, 55)",
"primaryContainer": "rgb(0, 79, 80)",
"onPrimaryContainer": "rgb(111, 247, 246)",
"secondary": "rgb(120, 209, 255)",
"onSecondary": "rgb(0, 53, 73)",
"secondaryContainer": "rgb(0, 76, 104)",
"onSecondaryContainer": "rgb(195, 232, 255)",
// ...
// 省略
// ...
"border": "rgb(65, 72, 77)",
"card": "#001e2c",
"notification": "rgb(255, 180, 171)",
"text": "rgb(225, 226, 229)",
}
const iosColors = Platform.OS === 'ios' ? {
"primary": DynamicColorIOS({
light: customLightColors.primary,
dark: customDarkColors.primary,
}),
"onPrimary": DynamicColorIOS({
light: customLightColors.onPrimary,
dark: customDarkColors.onPrimary,
}),
"primaryContainer": DynamicColorIOS({
light: customLightColors.primaryContainer,
dark: customDarkColors.primaryContainer,
}),
"onPrimaryContainer": DynamicColorIOS({
light: customLightColors.onPrimaryContainer,
dark: customDarkColors.onPrimaryContainer,
}),
"secondary": DynamicColorIOS({
light: customLightColors.secondary,
dark: customDarkColors.secondary,
}),
"onSecondary": DynamicColorIOS({
light: customLightColors.onSecondary,
dark: customDarkColors.onSecondary,
}),
"secondaryContainer": DynamicColorIOS({
light: customLightColors.secondaryContainer,
dark: customDarkColors.secondaryContainer,
}),
"onSecondaryContainer": DynamicColorIOS({
light: customLightColors.onSecondaryContainer,
dark: customDarkColors.onSecondaryContainer,
}),
// ...
// 省略
// ...
"border": DynamicColorIOS({
light: customLightColors.border,
dark: customDarkColors.border,
}),
"card": DynamicColorIOS({
light: customLightColors.card,
dark: customDarkColors.card,
}),
"notification": DynamicColorIOS({
light: customLightColors.notification,
dark: customDarkColors.notification,
}),
"text": DynamicColorIOS({
light: customLightColors.text,
dark: customDarkColors.text,
}),
} : {};
const theme = Platform.OS === 'ios' && colorScheme === 'dark' ? {
...MD3DarkTheme,
...DarkTheme,
myOwnProperty: true,
colors: {
...MD3DarkTheme.colors,
...DarkTheme.colors,
...iosColors,
},
} : Platform.OS === 'ios' ? {
...MD3LightTheme,
...LightTheme,
myOwnProperty: true,
colors: {
...MD3LightTheme.colors,
...LightTheme.colors,
...iosColors,
},
} : colorScheme === 'dark' ? {
...MD3DarkTheme,
...DarkTheme,
myOwnProperty: true,
colors: {
...MD3DarkTheme.colors,
...DarkTheme.colors,
...customDarkColors,
},
} : {
...MD3LightTheme,
...LightTheme,
myOwnProperty: true,
colors: {
...MD3LightTheme.colors,
...LightTheme.colors,
...customLightColors,
},
};
return (
<PaperProvider theme={theme}>
<NavigationContainer theme={theme}>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
</Stack.Navigator>
</NavigationContainer>
</PaperProvider>
);
}
エラー吐いて動かない。
なんで?・・・
React Native Paperの問題
色々やってみたが、どうやらReact Native PaperがDynamicColorIOS
に対応していないと結論づけることにした。
こうなってくるともうMaterial Design 3を上書きする方法は使えない。
独自のプロパティでテーマを拡張するしかなくなってしまった。
import React from 'react';
import { useColorScheme, Platform, DynamicColorIOS } from 'react-native';
import { MD3LightTheme, MD3DarkTheme, adaptNavigationTheme, Provider as PaperProvider } from 'react-native-paper';
import { NavigationContainer, DarkTheme as NavigationDarkTheme, DefaultTheme as NavigationDefaultTheme, } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import HomeScreen from './screens/HomeScreen';
const Stack = createNativeStackNavigator();
export default function App() {
const colorScheme = useColorScheme();
const { LightTheme, DarkTheme } = adaptNavigationTheme({
reactNavigationLight: NavigationDefaultTheme,
reactNavigationDark: NavigationDarkTheme,
});
const customLightColors = {
"primary": "rgb(0, 102, 137)",
"onPrimary": "rgb(255, 255, 255)",
"primaryContainer": "rgb(195, 232, 255)",
"onPrimaryContainer": "rgb(0, 30, 44)",
"secondary": "rgb(0, 106, 106)",
"onSecondary": "rgb(255, 255, 255)",
"secondaryContainer": "rgb(111, 247, 246)",
"onSecondaryContainer": "rgb(0, 32, 32)",
// ...
// 省略
// ...
"border": "rgb(192, 199, 205)",
"card": "#ebf2f8",
"notification": "rgb(186, 26, 26)",
"text": "rgb(25, 28, 30)",
}
const customDarkColors = {
"primary": "rgb(76, 218, 218)",
"onPrimary": "rgb(0, 55, 55)",
"primaryContainer": "rgb(0, 79, 80)",
"onPrimaryContainer": "rgb(111, 247, 246)",
"secondary": "rgb(120, 209, 255)",
"onSecondary": "rgb(0, 53, 73)",
"secondaryContainer": "rgb(0, 76, 104)",
"onSecondaryContainer": "rgb(195, 232, 255)",
// ...
// 省略
// ...
"border": "rgb(65, 72, 77)",
"card": "#001e2c",
"notification": "rgb(255, 180, 171)",
"text": "rgb(225, 226, 229)",
}
const iosColors = Platform.OS === 'ios' ? {
"dynamic": {
"primary": DynamicColorIOS({
light: customLightColors.primary,
dark: customDarkColors.primary,
}),
"onPrimary": DynamicColorIOS({
light: customLightColors.onPrimary,
dark: customDarkColors.onPrimary,
}),
"primaryContainer": DynamicColorIOS({
light: customLightColors.primaryContainer,
dark: customDarkColors.primaryContainer,
}),
"onPrimaryContainer": DynamicColorIOS({
light: customLightColors.onPrimaryContainer,
dark: customDarkColors.onPrimaryContainer,
}),
"secondary": DynamicColorIOS({
light: customLightColors.secondary,
dark: customDarkColors.secondary,
}),
"onSecondary": DynamicColorIOS({
light: customLightColors.onSecondary,
dark: customDarkColors.onSecondary,
}),
"secondaryContainer": DynamicColorIOS({
light: customLightColors.secondaryContainer,
dark: customDarkColors.secondaryContainer,
}),
"onSecondaryContainer": DynamicColorIOS({
light: customLightColors.onSecondaryContainer,
dark: customDarkColors.onSecondaryContainer,
}),
// ...
// 省略
// ...
},
"border": DynamicColorIOS({
light: customLightColors.border,
dark: customDarkColors.border,
}),
"card": DynamicColorIOS({
light: customLightColors.card,
dark: customDarkColors.card,
}),
"notification": DynamicColorIOS({
light: customLightColors.notification,
dark: customDarkColors.notification,
}),
"text": DynamicColorIOS({
light: customLightColors.text,
dark: customDarkColors.text,
}),
} : {};
const theme = Platform.OS === 'ios' && colorScheme === 'dark' ? {
...MD3DarkTheme,
...DarkTheme,
myOwnProperty: true,
colors: {
...MD3DarkTheme.colors,
...DarkTheme.colors,
...customDarkColors,
...iosColors,
},
} : Platform.OS === 'ios' ? {
...MD3LightTheme,
...LightTheme,
myOwnProperty: true,
colors: {
...MD3LightTheme.colors,
...LightTheme.colors,
...customLightColors,
...iosColors,
},
} : colorScheme === 'dark' ? {
...MD3DarkTheme,
...DarkTheme,
myOwnProperty: true,
colors: {
...MD3DarkTheme.colors,
...DarkTheme.colors,
...customDarkColors,
"dynamic": {
...customDarkColors,
}
},
} : {
...MD3LightTheme,
...LightTheme,
myOwnProperty: true,
colors: {
...MD3LightTheme.colors,
...LightTheme.colors,
...customLightColors,
"dynamic": {
...customLightColors,
}
},
};
return (
<PaperProvider theme={theme}>
<NavigationContainer theme={theme}>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
</Stack.Navigator>
</NavigationContainer>
</PaperProvider>
);
}
これでstyle = {{ backgroundColor: theme.colors.dynamic.primary }}
みたいな感じで使えるようになる。
非対応のReact Native Paperコンポーネント(CardとかDialogとか)では、従来通りtheme.colors.dynamic.primary
を使えばOK。
というわけで、全てのスクリーンでスタイルを書き換える地獄の作業が始まるのであった・・・
参考
- Changing theme dynamically · Issue #982 · wix/react-native-calendars
- Appearance addChangeListener handler is called when app goes to background with wrong color scheme · Issue #28525 · facebook/react-native
- DynamicColorIOS · React Native
- Theming | React Native Paper
- Theming with React Navigation | React Native Paper
- React でレンダリングを遅延させたい – Diary
コメント