這是我在2023第十五屆 iThome 鐵人賽發表的系列文章。https://ithelp.ithome.com.tw/users/20136637/ironman/6408
React 是 component-based,遵循 CSS-in-JS 方法來設定組件樣式。CSS in JS 的主要優點之一是允許將樣式與組件直接綁定,能避免樣式命名衝突並且不用再單獨維護 CSS 檔案,但缺點是存在性能問題,因為它需要在運行時創建樣式物件。
React Native 內建 StyleSheet,使用 StyleSheet 建立樣式會得到一個 ID 而不是樣式物件,重新渲染會引用 ID,便可避免重複創建相同的樣式物件造成的性能問題。
接下來會介紹在 React Native 中設定組件樣式的基本方式。
Inline Style
可以直接在組件的 style 屬性中定義樣式,inline style 類似於 CSS 屬性,不過屬性名稱會使用小駝峰。
定義一個長寬為 100% 的 View:
<View style={{ width: '100%', height: '100%' }}>
// ...
</View>
如果需要合併樣式可以轉為陣列,比如想將已經定義好的樣式物件和 backgroundColor: 'skyblue'
合併,就可以使用陣列將它們合併 style={[styles.root, { backgroundColor: 'skyblue' }]}
。
注意:後定義的樣式會覆蓋先定義的樣式,如果 styles.root 中設置
backgroundColor: 'black'
,那最終背景色會是skyblue
<View style={[styles.root, { backgroundColor: 'skyblue' }]}>
// ...
</View>
inline style 也可以做計算,比如希望在系統為暗色主題時將背景色改為黑色就可以這樣寫:isDarkMode && { backgroundColor: 'black' }
<View style={[styles.root, isDarkMode && { backgroundColor: 'black' }]}>
// ...
</View>
優點:
- 簡單、不需要另外維護樣式檔案。
- 在需要計算、動態調整樣式時很方便。
缺點:
- 對於大型和複雜的樣式,inline style 會使程式碼可讀性變差且變得難以維護。
- 每次渲染時都會創建樣式物件,導致性能下降。
StyleSheet
StyleSheet 是類似 CSS StyleSheets 的抽象,使用 StyleSheet 建立樣式會得到一個 ID 而不是樣式物件,寫法如下:
// App.tsx
import React from 'react';
import { StyleSheet, View } from 'react-native'
export const App = (): JSX.Element => {
return (
<View style={styles.root}>
// ...
</View>
)
}
const styles = StyleSheet.create({
root: {
width: '100%',
height: '100%'
}
})
官方建議將 StyleSheet 和組件寫在一起會更簡潔,所以除非是需要共用的樣式,不然直接和組件寫在同一個檔案中就好。
As a component grows in complexity, it is often cleaner to use StyleSheet.create to define several styles in one place.
https://reactnative.dev/docs/style
優點:
- 將樣式從物件中抽離,提升程式碼易讀性和維護性。
- 方便全局共用相同的樣式。
- 使用 StyleSheet 建立樣式會得到一個 ID 而不是樣式物件,有助於提高性能。
缺點:
- 不適合動態調整樣式。
傳遞參數給 StyleSheet
有些時候會需要動態改變樣式,該如何使用 StyleSheet 接收參數呢?
比如 App 背景顏色要根據系統主題變化,但獲取系統主題色的方式是使用 hook,hook 只能在函數組件中使用,所以必須在組件中將系統主題色作為參數傳遞過來給 StyleSheet:
// styles/basee.ts
import { StyleSheet, StatusBar } from "react-native"
import { Colors } from 'react-native/Libraries/NewAppScreen'
interface BaseStylesProps {
isDarkMode?: boolean
}
export const baseStyles = (props: BaseStylesProps) => StyleSheet.create({
root: {
backgroundColor: props.isDarkMode ? Colors.darker : Colors.lighter,
width: '100%',
height: '100%',
marginTop: StatusBar.currentHeight ?? 0,
padding: 16
}
})
// App.tsx
import React from 'react'
import { View, useColorScheme } from 'react-native'
import { baseStyles } from '@/styles'
export const App = (): JSX.Element => {
const isDarkMode = useColorScheme() === 'dark'
return (
<View style={baseStyles({ isDarkMode }).root}>
// ...
</View>
)
}
但這樣就失去使用 StyleSheet 的意義了,所以盡量不要傳遞參數給 StyleSheet,需要計算的樣式直接用 inline style 就好。
實現偽類、偽元素
文章最開始有提到 React Native 中不允許寫偽類、偽元素,但是有別的方式可以去實現。這邊就簡單分享 :before
, :after
, :hover
, :active
的間接實現方式。
:before :after
:before
和 :after
可以使用額外的 View 或 Text 組件來模擬。
import React from 'react'
import { View, Text, StyleSheet } from 'react-native'
const App = () => {
return (
<View style={styles.container}>
<Text style={styles.beforeAfterContent}>Before Content</Text>
<Text style={styles.text}>Main Content</Text>
<Text style={styles.beforeAfterContent}>After Content</Text>
</View>
)
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center'
},
beforeAfterContent: {
marginRight: 10,
color: 'red'
},
text: {
color: 'black',
fontSize: 16
}
})
export default App
:hover :active
可以使用 Pressable 組件來模擬,Pressable 組件自帶 pressed 屬性,藉以判斷是否點擊組件。
import React from 'react'
import { View, Text, Pressable, StyleSheet } from 'react-native'
const App = () => {
return (
<Pressable
style={({ pressed }) => [
{
backgroundColor: pressed ? "rgb(210, 230, 255)" : "white",
},
]}
>
{({ pressed }) => (
<Text style={styles.text}>{pressed ? "Pressed!" : "Press Me"}</Text>
)}
</Pressable>
)
}
const styles = StyleSheet.create({
text: {
color: 'black',
fontSize: 18
}
})
export default App
其他的偽類偽元素也是差不多的實現方式。
修改 Android 預設文字顏色
如果使用 Text 組件時沒有設置文字顏色,在亮色主題下會是黑色,但如果切換成深色主題,文字就會變為空(透明)因此無法正常顯示文字:
<Text>TEST</Text>
這是因為 Android 應用預設使用 DayNight 配置,為了同時支持亮色和深色主題。
https://developer.android.com/develop/ui/views/theming/darktheme
可以在 android/app/src/main/res/values/styles.xml
中看到 AppTheme 為 DayNight:
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
...
</style>
如果要解決這個問題,有幾種簡單的解決方式:
1.將 <style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
改為 <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
強制使用亮色主題。
2.在 android/app/src/main/res/values/styles.xml
中設置文字顏色
<resources>
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="android:textColor">#000000</item>
...
</style>
...
</resources>
3.使用 Apperance 獲取主題模式,根據模式動態修改文字顏色。
import { useColorScheme, Text } from 'react-native'
const App = () => {
const isDarkMode = useColorScheme() === 'dark'
return (
<Text style={{ color: isDarkMode ? '#ffffff' : '#000000' }}>DEMO</Text>
)
}
補充:mixins
在 CSS 中設置 padding, margin 寫法如下:
paading: 10px 20px 40px 5px;
margin: 20px 10px;
在 RN 中寫法就會變得十分冗長:
{
paddingTop: 10,
paddingRight: 20,
paddingBottom: 40,
paddingLeft: 5,
marginHorizontal: 10,
marginVertical: 20
}
如果希望能在 RN 中和 CSS 一樣用一行就能定義 padding, margin 的話,可以寫一個函數來簡化這段樣式:
const dimensions = (t: number, r = t, b = t, l = r, prop: string): Record<string, number> => {
let styles: Record<string, number> = {}
styles[`${prop}Top`] = t
styles[`${prop}Right`] = r
styles[`${prop}Bottom`] = b
styles[`${prop}Left`] = l
return styles
}
const margin = (t: number, r: number, b?: number, l?: number) => {
return dimensions(t, r, b, l, 'margin')
}
const padding = (t: number, r: number, b?: number, l?: number) => {
return dimensions(t, r, b, l, 'padding')
}
export const mixins = {
padding,
margin
}
<View style={styles.container}>
// ...
</View>
const styles = StyleSheet.create({
container: {
...mixins.padding(8, 16)
}
})
總結
- RN 中 CSS 屬性使用小駝峰命名,如
justifyContent
,alignItems
… - RN 中的默認單位為 dp,因此若設置
width: 200
則為 200dp - 部分屬性使用無單位的值,比如
borderRadius
,padding
,margin
,fontSize
…等 - 無須計算的樣式建議使用 StyleSheet 而不是 inline style
- Android 和 iOS 設置陰影為不同屬性,Android 使用
elevation
,iOS 則使用shadowOffset
,shadowOpacity
,shadowRadius
- iOS 及 Android API 28 以上可使用
shadowColor
設置陰影顏色,Android API 低於 28 則用elevation
- React Native 中不允許使用偽類、偽元素,需要另外實現,或者可以使用 styled-components、emotion。
- 如果要在 React Native 中使用媒體樣式,需要藉助第三方庫,如 react-native-media-query