這是我在2023第十五屆 iThome 鐵人賽發表的系列文章。https://ithelp.ithome.com.tw/users/20136637/ironman/6408
假設設計 APP UI 時以 iphone 14 pro 為框架,那麼尺寸就是 393px*852px
,但這尺寸包括了各種狀態欄、導航欄、工具列的高度,所以實際內容區塊的高度會更小。

從設計稿可以看出,實際內容區塊的高度僅有 683px,但還要再加上 padding: 16
,所以高度是 683+(16*2) = 715px

如果所有設備使用同樣的數值,那麼一定會發生UI跑版、尺寸不合適的情況,因為:
- 在不同尺寸的屏幕底下顯示的比例會有所不同
- 每個裝置的系統字體縮放比例不一定相同
所以我們需要針對不同設備的尺寸和字體縮放比例去對 App 中的樣式進行調整。
獲取屏幕尺寸
在調整之前,我們需要先知道怎麼樣獲取設備屏幕的尺寸。
Dimension API
RN 有提供 Dimension API,使用 Dimensions.get()
即可獲取屏幕尺寸:
screen
: 包括狀態欄和底部導航欄高度。window
: 在 Android 不包括狀態欄(如果不是半透明)和底部導航欄使用的高度,iOS 上則和 screen 無區別。
const { width, height } = Dimensions.get('window')
const { width, height } = Dimensions.get('screen')
大部分時候我們需要知道的是可視範圍尺寸,所以會使用 Dimensions.get('window')
獲取寬度和高度:
import React from 'react'
import { Dimensions, Text } from 'react-native'
const windowWidth = Dimensions.get('window').width
const windowHeight = Dimensions.get('window').height
export const HomeScreen = (): JSX.Element => {
return (
<>
<Text>windowWidth: {windowWidth}</Text>
<Text>windowHeight: {windowHeight}</Text>
</>
)
}

但如果你是使用折疊屏,或者從直向轉為橫向,那麼 Dimension API 並不會重新獲取屏幕的尺寸,這時候就需要透過監聽或者使用 useWindowDimensions
來動態獲取當前的屏幕尺寸。

監聽屏幕尺寸變化
使用 Dimensions.addEventListener('change', ({ window, screen }) => {})
監聽 Dimensions 物件,當 Dimensions 物件中的屬性更改時就會觸發:
const windowWidth = Dimensions.get('window').width
const windowHeight = Dimensions.get('window').height
const [windowSize, setWindowSize] = useState({
width: windowWidth,
height: windowHeight,
})
useEffect(() => {
const subscription = Dimensions.addEventListener(
'change',
({ window }) => {
setWindowSize({
width: window.width,
height: window.height,
})
})
return () => subscription?.remove()
}, [])


useWindowDimensions
當然還有更方便的方法,直接使用 useWindowDimensions
這個 hook 就可以了:
import { useWindowDimensions } from "react-native"
const { width, height } = useWindowDimensions()
字體縮放比
Dimensions 除了 width, height 之外,還有 scale 和 fontScale
fontScale 1 代表的是字體為正常大小:

假設我將系統字體設置為最大 fontScale 就變為 2 了,代表字體縮放為正常的 2 倍,原本的文字也變得十分巨大:


限制字體縮放
如果希望無論系統字體設置多大 App 中的文字都保持正常大小,那麼可以修改
Text 組件預設的 maxFontSizeMultiplier
屬性:
import { Text, TextInput } from 'react-native'
Text.defaultProps = Text.defaultProps || {}
Text.defaultProps.maxFontSizeMultiplier = 1
TextInput.defaultProps = Text.defaultProps || {}
TextInput.defaultProps.maxFontSizeMultiplier = 1
如此一來即使系統字體大小設置為最大(看狀態欄可知),fontScale 也還是 2,但顯示的文字變回和正常一樣大:

根據不同屏幕尺寸縮放內容
設計稿上這個卡片組件為 361px*188px
,在 iphone 14 pro 上一切正常,但在 pixel 7 上直接大跑版,卡片右側被卡掉了。


import React from "react"
import { Card, Text, Button } from "react-native-paper"
export const HomeScreen = (): JSX.Element => {
return (
<Card
contentStyle={{
width: 361,
height: 188,
backgroundColor: 'white',
borderWidth: 1,
borderRadius: 4
}}
>
<Card.Content>
<Text>Home Screen</Text>
</Card.Content>
</Card>
)
}
這種時候我們就需要根據實際設備屏幕尺寸與設計稿屏幕尺寸的比例去縮放內容:
- horizontalScale:根據設備的屏幕寬度返回所提供尺寸的線性縮放結果。
- verticalScale:根據設備的屏幕高度返回所提供尺寸的線性縮放結果。
- moderateScale:不想線性縮放時使用,還可以藉由傳入不同的 factor 調整縮放結果。
// helpers/scaling.ts
import { Dimensions } from "react-native"
const baseWidth = 393
const baseHeight = 852
const { width, height } = Dimensions.get('window')
const [shortDimension, longDimension] = width < height
? [width, height]
: [height, width]
export const horizontalScale = (size: number) => shortDimension / baseWidth * size
export const verticalScale = (size: number) => longDimension / baseHeight * size
export const moderateScale = (size: number, factor = 0.5) => size + (horizontalScale(size) - size) * factor
使用方式如下:
<Card
contentStyle={{
width: horizontalScale(361),
height: verticalScale(188),
backgroundColor: 'white',
borderWidth: 1,
borderRadius: 4
}}
>
<Card.Content>
<Text>Home Screen</Text>
</Card.Content>
</Card>
