開始執筆 Follow Mobile 已經過去一個月了,想想也該沉澱點什麼東西了。
這篇文章首先來講講 Follow Mobile 的顏色體系。
開始之前需要知道的是 Follow Mobile 是使用 React Native 開發的並且使用了 Expo 框架。
準備條件#
由於 React Native 並沒有官方支持 web 中 className 的寫法,為了適應 web 中方便快捷的 TailwindCSS 原子類名,我們需要借助 NativeWind 工具。這是一個能讓 React Native app 中也使用一部分 TailwindCSS 能力的編譯器。通過 babel plugin 對 React Native 中的基礎組件進行包裝,在 runtime 中對 className 進行翻譯到 React Native style 對象來實現類似效果。
NativewindCSS 內部也借助 TailwindCSS 進行翻譯,在配置 TailwindCSS 時基本和 Web 中一致。
我們安裝 NativewindCSS。
pnpm install nativewind tailwindcss
配置好 Babel 和 Metro。
// babel.config.js
module.exports = function (api) {
api.cache(true)
return {
presets: [
['babel-preset-expo', { jsxImportSource: 'nativewind' }],
'nativewind/babel',
],
}
}
const { withNativeWind } = require('nativewind/metro')
module.exports = withNativeWind(config, { input: './src/global.css' })
創建 PostCSS 樣式入口。
/* src/global.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
在 dev server 啟動之後,會自動生成 nativewind-env.d.ts 文件以提供類型支持。至此,準備工作已經完成。
選擇顏色體系#
一個 app 起步階段,或許一套完善的設計規範是不可少的。顏色定義對於一個 app 來說也是重中之重。在開始開發 app 時,我想要打造一個很 apple 味道的 app。
可惜,React Native 畢竟是一個跨段開發框架,並沒有提供太多的 native 組件和樣式。多數我們只能去模擬 native 樣式或者借助社區的 native 模塊。對於組件,這是後話了,這裡我先說說顏色。
apple 有一套非常規範的顏色體系,在 Color | Apple Developer Documentation。使用這套定義,搬到 NativeWind 中。
可能在文檔中並不好看到所有顏色的值,apple 提供了 figma,在這裡還可以看到大部分的 native 組件使用的顏色。
https://www.figma.com/community/file/1385659531316001292/ios-18-and-ipados-18
那麼這裡,我已經把相關顏色都提取出來了。供你參考:
const lightPalette = {
red: '255 59 48',
orange: '255 149 0',
yellow: '255 204 0',
green: '52 199 89',
mint: '0 199 190',
teal: '48 176 190',
cyan: '50 173 200',
blue: '0 122 255',
indigo: '88 86 214',
purple: '175 82 222',
pink: '255 45 85',
brown: '162 132 94',
gray: '142 142 147',
gray2: '172 172 178',
gray3: '199 199 204',
gray4: '209 209 214',
gray5: '229 229 234',
gray6: '242 242 247',
}
const darkPalette = {
red: '255 69 58',
orange: '255 175 113',
yellow: '255 214 10',
green: '48 209 88',
mint: '99 230 226',
teal: '64 200 244',
cyan: '100 210 255',
blue: '10 132 255',
indigo: '94 92 230',
purple: '191 90 242',
pink: '255 55 95',
brown: '172 142 104',
gray: '142 142 147',
gray2: '99 99 102',
gray3: '72 72 74',
gray4: '58 58 60',
gray5: '44 44 46',
gray6: '28 28 30',
}
export const lightVariants = {
// UIKit Colors
placeholderText: '199 199 204',
separator: '84 84 86 0.34',
opaqueSeparator: '84 84 86 0.34',
nonOpaqueSeparator: '198 198 200',
link: '0 122 255',
systemBackground: '255 255 255',
secondarySystemBackground: '242 242 247',
tertiarySystemBackground: '255 255 255',
// Grouped
systemGroupedBackground: '242 242 247',
secondarySystemGroupedBackground: '255 255 255',
tertiarySystemGroupedBackground: '242 242 247',
// System Colors
systemFill: '120 120 128 0.2',
secondarySystemFill: '120 120 128 0.16',
tertiarySystemFill: '120 120 128 0.12',
quaternarySystemFill: '120 120 128 0.08',
// Text Colors
label: '0 0 0',
text: '0 0 0',
secondaryLabel: '60 60 67 0.6',
tertiaryLabel: '60 60 67 0.3',
quaternaryLabel: '60 60 67 0.18',
}
export const darkVariants = {
// UIKit Colors
placeholderText: '122 122 122',
separator: '56 56 58 0.6',
opaqueSeparator: '56 56 58 0.6',
nonOpaqueSeparator: '84 84 86',
link: '10 132 255',
systemBackground: '0 0 0',
secondarySystemBackground: '28 28 30',
tertiarySystemBackground: '44 44 46',
// Grouped
systemGroupedBackground: '0 0 0',
secondarySystemGroupedBackground: '28 28 30',
tertiarySystemGroupedBackground: '44 44 46',
// System Colors
systemFill: '120 120 128 0.36',
secondarySystemFill: '120 120 128 0.32',
tertiarySystemFill: '120 120 128 0.24',
quaternarySystemFill: '120 120 128 0.19',
// Text Colors
label: '255 255 255',
text: '255 255 255',
secondaryLabel: '235 235 245 0.6',
tertiaryLabel: '235 235 245 0.3',
quaternaryLabel: '235 235 245 0.18',
}
分別對應亮色和暗色下的普通顏色和系統變量顏色。
使用 NativeWind 變量注入#
NativeWind 有個特徵可以實現類似 Web 中的 CSS variable。
https://www.nativewind.dev/api/vars
例如:
<View style={vars({ '--brand-color': 'red'})}>
{ // style: { color: 'red' } }
<Text className="text-[--brand-color]" />
</View>
借助這個特徵,我們可以把上面的顏色定義都用這個方式從頂層傳入。
// @ts-expect-error
const IS_DOM = typeof ReactNativeWebView !== 'undefined'
const varPrefix = '--color'
const buildVars = (_vars: Record<string, string>) => {
const cssVars = {} as Record<`${typeof varPrefix}-${string}`, string>
for (const [key, value] of Object.entries(_vars)) {
cssVars[`${varPrefix}-${key}`] = value
}
return IS_DOM ? cssVars : vars(cssVars)
}
上面這個函數為了兼容 react-native-web 如果你沒有需求可省略。
const mergedLightColors = {
...lightVariants,
...lightPalette,
}
const mergedDarkColors = {
...darkVariants,
...darkPalette,
}
const mergedColors = {
light: mergedLightColors,
dark: mergedDarkColors,
}
export const colorVariants = {
light: buildVars(lightVariants),
dark: buildVars(darkVariants),
}
export const palette = {
// iOS color palette https://developer.apple.com/design/human-interface-guidelines/color
light: buildVars(lightPalette),
dark: buildVars(darkPalette),
}
export const getCurrentColors = () => {
const colorScheme = Appearance.getColorScheme() || 'light'
return StyleSheet.compose(
colorVariants[colorScheme],
palette[colorScheme],
) as StyleProp<ViewStyle>
}
然後在頂層包一層 View。例如:
export const RootProviders = ({ children }: { children: ReactNode }) => {
useColorScheme() // 為了對亮色/暗色進行監聽
const currentThemeColors = getCurrentColors()!
return <View style={[styles.flex, currentThemeColors]}>{children}</View>
}
這樣,在子代任何組件都可以直接使用相關的變量了。但是使用仍然不方便。我們還需要配置下 TailwindCSS 的 colors。
由於上面我們都用了前綴 --color
,我可以這樣寫一個 tailwindcss config 的包裝函數。
import { Config } from 'tailwindcss'
const configColors = {
// Palette colors
red: 'rgb(var(--color-red) / <alpha-value>)',
orange: 'rgb(var(--color-orange) / <alpha-value>)',
yellow: 'rgb(var(--color-yellow) / <alpha-value>)',
green: 'rgb(var(--color-green) / <alpha-value>)',
mint: 'rgb(var(--color-mint) / <alpha-value>)',
teal: 'rgb(var(--color-teal) / <alpha-value>)',
cyan: 'rgb(var(--color-cyan) / <alpha-value>)',
blue: 'rgb(var(--color-blue) / <alpha-value>)',
indigo: 'rgb(var(--color-indigo) / <alpha-value>)',
purple: 'rgb(var(--color-purple) / <alpha-value>)',
pink: 'rgb(var(--color-pink) / <alpha-value>)',
brown: 'rgb(var(--color-brown) / <alpha-value>)',
gray: {
DEFAULT: 'rgb(var(--color-gray) / <alpha-value>)',
2: 'rgb(var(--color-gray2) / <alpha-value>)',
3: 'rgb(var(--color-gray3) / <alpha-value>)',
4: 'rgb(var(--color-gray4) / <alpha-value>)',
5: 'rgb(var(--color-gray5) / <alpha-value>)',
6: 'rgb(var(--color-gray6) / <alpha-value>)',
},
// System colors
'placeholder-text': 'rgb(var(--color-placeholderText) / <alpha-value>)',
separator: 'rgb(var(--color-separator) / <alpha-value>)',
'opaque-separator': 'rgba(var(--color-opaqueSeparator))',
'non-opaque-separator': 'rgba(var(--color-nonOpaqueSeparator))',
link: 'rgb(var(--color-link) / <alpha-value>)',
// Backgrounds
'system-background': 'rgb(var(--color-systemBackground) / <alpha-value>)',
'secondary-system-background':
'rgb(var(--color-secondarySystemBackground) / <alpha-value>)',
'tertiary-system-background':
'rgb(var(--color-tertiarySystemBackground) / <alpha-value>)',
'system-grouped-background':
'rgb(var(--color-systemGroupedBackground) / <alpha-value>)',
'secondary-system-grouped-background':
'rgb(var(--color-secondarySystemGroupedBackground) / <alpha-value>)',
'tertiary-system-grouped-background':
'rgb(var(--color-tertiarySystemGroupedBackground) / <alpha-value>)',
// System fills
'system-fill': 'rgba(var(--color-systemFill))',
'secondary-system-fill': 'rgba(var(--color-secondarySystemFill))',
'tertiary-system-fill': 'rgba(var(--color-tertiarySystemFill))',
'quaternary-system-fill': 'rgba(var(--color-quaternarySystemFill))',
// Text colors
label: 'rgb(var(--color-text) / <alpha-value>)',
text: 'rgb(var(--color-text) / <alpha-value>)',
'secondary-label': 'rgba(var(--color-secondaryLabel))',
'tertiary-label': 'rgba(var(--color-tertiaryLabel))',
'quaternary-label': 'rgba(var(--color-quaternaryLabel))',
}
export const withUIKit = (config: Config) => {
config.theme = config.theme || {}
config.theme.extend = config.theme.extend || {}
config.theme.extend.colors = config.theme.extend.colors || {}
config.theme.extend.colors = {
...config.theme.extend.colors,
...configColors,
}
return config
}
然後直接在 tailwind.config.ts 中使用。
export default withUIKit(config)
這樣,在 tailwindcss 中就可以直接使用這些顏色了。
使用#
在組件中,可以直接使用這樣的方式去設置顏色:
<View className={'bg-secondary-system-grouped-background'} />
但是總有時候我們不能直接使用類名,而是需要實際的變量。比如在做顏色過渡動畫的時候。
我們來寫一個 hook 去獲取當前主題時的對應顏色。
export const useColor = (color: keyof typeof mergedLightColors) => {
const { colorScheme } = useColorScheme()
const colors = mergedColors[colorScheme || 'light']
return useMemo(() => rgbStringToRgb(colors[color]), [color, colors])
}
使用方式:
const redColor = useColor('red')
後記#
此方案已從 Follow Mobile 項目中抽取為通用庫,歡迎使用。
此文由 Mix Space 同步更新至 xLog 原始鏈接為 https://innei.in/posts/tech/react-native-uikit-colors