React Native 中的 style 和 layout

好久不見的 style。

自我檢測

  • 我知道怎麼在 APP 中加入 Icon
  • 我知道怎麼在 APP 中使用客製化 Font
  • 我知道怎麼利用 module 把 Stylesheet 抽出去變成全域樣式
  • 我知道什麼是 ViewTextButton 等等基本的 RN 元件
  • 我知道怎麼用 RN 中的 StyleSheetFlexPosition 來做排版
  • 我知道 RN 裡面不會寫 px 值,而是要透過 scale factor 來計算結果
  • 我知道怎麼透過 AppLoading 來處理第一次載入時的處理

block / inline

在 RN 裡面沒有所謂的 block / inline,所以你可以把所有東西都視為 block。

不過更精確一點來說是只有「Flex」,因為每一個 Component 預設就是 Flex-box,所以你才會看到像這樣的寫法:

1
2
3
4
5
6
7
8
9
10
11
12
13
export default function App () {
return (
<View style={styles.container}>
<Text>Flex item</Text>
</View>
)
}

const styles = StyleSheet.create({
container: {
flex: 1, // 直接用 flex 的屬性
}
})

Styles

想要在 RN 做樣式設定的話,可以用 style 這個 props 傳入「物件」來設定樣式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { StyleSheet, Text, View } from 'react-native';

export default function App() {
return (
<View style={styles.container}>
<Text>Some thing else.</Text>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
});

這邊透過 StyleSheet.create() 的用意是「如果用了不對的 key / value 時」,會直接噴 Error 並停止程式。

但其實只傳入一般的純物件也是 OK 的,像這樣:

1
2
3
4
5
6
7
8
9
10
11
12
import { StyleSheet, Text, View } from 'react-native';

export default function App() {
return (
<View style={{
flex: 1,
backgroundColor: '#fff'
}}>
<Text>Some thing else.</Text>
</View>
);
}

這樣有什麼差嗎?

有哦!在只傳入物件的情況下,如果我寫了不對的 key / value 是不會噴錯誤的。但如果是透過 StyleSheet.create() 來建立的物件,只要我有地方寫錯他就一定會編譯失敗並顯示錯誤

所以一般會建議用 StyleSheet.create() 的方式來撰寫,但如果你真的很懶的話用純物件的方式也不是不行啦。

RN 裡沒有繼承的概念

在經過 CSS 多年的摧殘下,如果我想讓文字粗體,可能會下意識這樣寫:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export default function App() {
return (
<View style={styles.container}>
<View style={styles.header}>
<Text>Hello React Native</Text>
<Text>Hello React Native</Text>
<Text>Hello React Native</Text>
</View>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
padding: 30,
},
header: {
backgroundColor: 'pink',
fontWeight: 'bold'
}
});

但這樣是沒用的,我一定要把這個 fontWeight 寫在 <Text> 身上才有用。

雖然有個例外情形,就是 <Text> 裡面再放一個 <Text>,像這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export default function App() {
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.parentText}>Parent Text And<Text>Child Text</Text></Text>
</View>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
padding: 30,
},
header: {
backgroundColor: 'pink',
},
parentText: {
fontWeight: 'bold'
}
});

這樣的話 Child Text 就也會吃到粗體的效果。

但這只是例外情形,你要知道大多數情形下還是不會有「繼承」的觀念的。

現在 <View> 就等於 flex-box,<Text> 就等於 flex-item,總之要知道這個在 RN 裡面最基本的運作模式。

關於尺寸的單位

因為要考慮「Scale Factor」的關係,所以實際的寬可能跟你想得不一樣。

參考這張圖:

scale-factor

一個 iPhone4 的像素是 320 x 480,但 Scale Factor 是「2x」,所以實際的解析度是 640 x 960(乘以二)

所以我如果把藍色方塊的寬度設為 150,實際在手機上就會是 300px

總之這邊是要讓你搞清楚妳設定的「值」代表什麼?

  • 如果 Scale factor 為 2,則每個單位就代表 2px
  • 如果 Scale factor 為 3,則每個單位就代表 3px

知道這些就好了,不用特別去背這些規格。如果真的碰到單位的問題,可以試著用 % 的方式來設值。

Flex

在 RN 裡面主要會用 Flex 的方式來排版(也是預設的排版方式),主要概念都跟 CSS 裡的 Flex box 差不多,所以這邊只會把我覺得幾個比較特別需要瞭解的觀念給記錄下來。

複習幾個 flex 相關屬性

  • flexBasis 設定相對於「主軸」的寬度,會覆蓋掉 width 的設定
  • flexGrou 根據「主軸」的剩餘空間來分配 ??? 到這個 item
  • flexShrink 根據「主軸」的溢出空間來壓縮 ??? 到這個 item
  • justifyContent 設定 item 的對齊方式(根據主軸)
  • alignItems 設定 item 的對齊方式(根據次軸)
  • flex: 1 這個其實是在說flexGrow: 1 的意思,只是個簡寫

附註一下 RN 跟 CSS 不同的幾個地方:

  • flexDirection 的預設值是 column
  • flexShrink 的預設值是 0

Direction

在 React Native 中,如果這樣設定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export default function App() {

return (
<View style={{
flex: 1,
backgroundColor: '#fff'
}}>
<View style={{
width: 100,
height: 100,
backgroundColor: 'dodgerblue'
}} />
<View style={{
width: 100,
height: 100,
backgroundColor: 'gold'
}} />
<View style={{
width: 100,
height: 100,
backgroundColor: 'tomato'
}} />
</View>
);
}

最後出來的東西會是三個「直向排列」的 item,跟你在寫 CSS 的時候還蠻不一樣的對吧?如果是 CSS 的話預設會是「橫向排列」。

簡單來說,這是因為在 RN 裡面 Flex 預設的方向是 column 而不是 row 的關係。

仔細想想的話也蠻合理的,畢竟我們在用手機的時後大多是以「直向」為主,所以把 Flex 預設成直向好像也挺直覺的!

alignContent

這個只會在有 flewWrap: wrap 的時候才有作用,記得這個就好了,舉例來說:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
export default function App() {

return (
<View style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
flexWrap: 'wrap', // 換行
alignContent: 'center',
backgroundColor: '#fff'
}}>
<View style={{
width: 100,
height: 100,
backgroundColor: 'dodgerblue'
}} />
<View style={{
width: 100,
height: 100,
backgroundColor: 'gold'
}} />
<View style={{
width: 100,
height: 100,
backgroundColor: 'tomato'
}} />
<View style={{
width: 100,
height: 100,
backgroundColor: 'dodgerblue'
}} />
<View style={{
width: 100,
height: 100,
backgroundColor: 'pink'
}} />
<View style={{
width: 100,
height: 100,
backgroundColor: 'black'
}} />
</View>
);
}

這時候的結果是長這樣:

align-content-01

但如果把 alignContent 拿掉的話就會變這樣:

align-content-02

簡單來說,如果你的 flex-box 有啟用「換行」,然後你又想「把裡面的 flex-item 全部放到正中央」,那就會需要 alignContent 而不是 alignItems

alignItems 是用來處理「沒有換行」的時候才會用的屬性。

Position

在 RN 裡面 Position 的預設值是 relative,所以我可以直接這樣寫:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
export default function App() {

return (
<View style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff'
}}>
<View style={{
width: 100,
height: 100,
backgroundColor: 'dodgerblue'
}} />
<View style={{
top: 20, // 直接用 top 來偏移
width: 100,
height: 100,
backgroundColor: 'gold'
}} />
<View style={{
width: 100,
height: 100,
backgroundColor: 'tomato'
}} />
</View>
);
}

輸出結果:

position

用法其實都跟 CSS 一樣,所以就不特別介紹怎麼用了。這邊只是想特別提一下預設值是 relative 這一點。

至於 absolute 的話也跟 CSS 一樣,記得加上 position: 'absolute' 來打開,然後判斷好「父層的參考點」就好了。

順道一提,在 RN 裡面 position 就只有這兩種而已,不會有什麼 fixedstickystatic 這種東西。

載入 Font

FOIT 跟 FOUT 是什麼?

附註:如果你想看範例的話可以到 這邊

這邊科普一下兩個載入字體的使用手法:

  • FOIT 先隱藏所有文字內容,等載入完成後才顯示(但其他的東西一開始會被顯示)
  • FOUT 先顯示預設字型,等載入後再更新成新的字型

簡單來說就是不同的設計哲學而已,還蠻有趣的。

第一種做法(hook)

這種作法是透過 hook 來做的,簡單來說就是:

  1. 先下載字型的 package,@expo-google-fonts/{googleFont}
  2. 接著用下面的方式來使用即可

附註:假設 <Home /> 裡面有用到我們指定的字型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import AppLoading from 'expo-app-loading';
import Home from './screens/Home';
import { useFonts, Nunito_400Regular, Nunito_700Bold } from "@expo-google-fonts/nunito";

export default function App() {
let [fontsLoaded] = useFonts({
Nunito_400Regular,
Nunito_700Bold
})

if (!fontsLoaded) {
return <AppLoading />
}

return (
<Home />
);
}

useFonts 會回傳一個陣列,陣列的第一個值是 boolean,代表這字型載入完了沒?

接著下面寫了一個 if 判斷,意思就是如果還在載入就顯示 <AppLoading />,等到載入完以後才顯示 <Home />

雖然還蠻好奇他是怎麼更新 fontsLoaded 這個 state 然後觸發 re-render 的,不過這確實是可行的!也是最簡潔的做法。

第二種做法(Font.loadAsync)

這應該算是比較舊的做法,不過也是能參考一下:

附註:Font.loadAsync 也可以載入外部資源,只要把 require 改成對應的 URI 就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import AppLoading from 'expo-app-loading';
import Home from './screens/Home';
import { useState } from 'react';
import * as Font from "expo-font";

export default function App() {
const [fontsLoaded, setFontsLoaded] = useState<boolean>(false);

const getFonts = () => {
return Font.loadAsync({
'Nunito_400Regular': require('./assets/fonts/Nunito-Regular.ttf'),
'Nunito_700Bold': require('./assets/fonts/Nunito-Bold.ttf'),
});
}

if (!fontsLoaded) {
return (
<AppLoading
startAsync={getFonts}
onFinish={() => setFontsLoaded(true)}
onError={(error) => console.log('error', error)}
/>
)
}

return (
<Home />
);
}

跟第一種作法差不多,只是 state 跟 call function 的部分得自己來:

  • getFonts 用來載入字型的 function
  • <AppLoading startAsync={getFonts}> 去 call 載入字型的 function
  • <AppLoading onFinish={() => setFontsLoaded(true)}> 載入完成後更新 state

設定 Icon

附註:官方文件(可查詢 icon 名稱)

關於 Icon 的部分,在我們用 expo init 專案的時候其實就會順便載入相關的套件了,所以我們只要直接 import 進來用就可以了,像這樣:

1
2
3
4
5
6
7
import { MaterialIcons } from '@expo/vector-icons';
export default function TodoList({ todos, removeTodo }) {

return (
<MaterialIcons name="delete" size={24} color="#333" />
)
}
React Navigation-Navigation Stack React Native 中的 Component 與 API
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×