這是我在2023第十五屆 iThome 鐵人賽發表的系列文章。https://ithelp.ithome.com.tw/users/20136637/ironman/6408
在 RN 中有兩種常見的測量組件尺寸方式:
- onLayout
- measure
onLayout
有些時候會需要知道當前視圖的實際尺寸為何就可以用到 onLayout
。
當組件佈局發生變化時(例如組件的尺寸、位置等改變),React Native 會調用 onLayout 函數,就能獲得組件的新尺寸。
RN 內建的以下幾個組件都具有 onLayout 屬性:
- Image
- Pressable
- ScrollView
- Text
- TextInput
- TouchableWithoutFeedback
- View
觸發時機
- 當組件首次被渲染並且佈局完成後。
- 佈局發生了變化,例如其子組件的尺寸變化、組件被添加或移除等情況。
- 從豎屏切換到橫屏。
基本使用
onLayout?: ((event: LayoutChangeEvent) => void) | undefined
LayoutChangeEvent
中有 width, height, x, y 四個值,分別代表:
- width: 視圖寬度
- height: 視圖高度
- x: 視圖和相對父視圖的水平位置
- y: 視圖和相對父視圖的垂直位置
import { View, Image, Text, type LayoutChangeEvent } from "react-native"
export const App = () => {
const onLayout = (event: LayoutChangeEvent) => {
const { width, height, x, y } = event.nativeEvent.layout
console.log(width, height, x, y); // 320 320 80 349.3333435058594
}
return (
<View style={{ flex: 1 }}>
<View style={{ backgroundColor: 'white', padding: 10 }} onLayout={onLayout}>
<Image source={{ uri: 'https://placehold.co/300x300.png' }} width={300} height={300} />
</View>
</View>
)
}
寬度與高度的計算
以剛剛的例子 console.log(width, height, x, y)
輸出 320 320 80 349.3333435058594
- width: 320 代表圖片寬度
300
+ 水平 padding 10*2 =20
- height 同理
- 這邊父視圖是全屏,所以 x: 80, y: 349.333 就代表 View 在距離原點(左上角)的距離
改變下方這些樣式都會影響 onLayout 重新計算 View 的大小和位置
- padding
- borderWidth
- left, top, right, bottom
- height, width, maxHeight, maxWidth
- aspectRatio
封裝成hook
可以將獲取組件大小的邏輯抽象出來成一個 custom hook:
// hooks/useComponentSize.tsx
import { useState, useCallback } from 'react'
import { View, Image, type LayoutChangeEvent } from 'react-native'
export const useComponentSize = () => {
const [size, setSize] = useState({ width: 0, height: 0 })
const onLayout = useCallback((event: LayoutChangeEvent) => {
const { width, height } = event.nativeEvent.layout
setSize({ width, height })
}, [])
return [size, onLayout] as const
}
// App.tsx
export const App = () => {
const [size, onLayout] = useComponentSize()
return (
<View style={{ backgroundColor: 'white', padding: 10 }} onLayout={onLayout}>
<Image source={{ uri: 'https://placehold.co/300x300.png' }} width={300} height={300} />
</View>
)
}
measure
measure 跟 onLayout 一樣可以用於獲取組件尺寸和位置,不同的是 measure 會需要使用 ref 來獲得組件的引用,然後再使用 ref.measure 方法來獲取組件的尺寸和位置。
ref.current.measure((x, y, width, height, pageX, pageY) => { // ... })
width
,height
: 元素的寬高pageX
,pageY
: 相對於整個頁面的位置x
,y
: 相對於父組件的位置
import { useRef, useEffect } from "react"
import { View, Image } from "react-native"
export const ViewPage = () => {
const viewRef = useRef(null)
useEffect(() => {
handleMeasure()
}, [viewRef])
const handleMeasure = () => {
if (viewRef.current) {
viewRef.current.measure((x, y, width, height, pageX, pageY) => {
console.log(width, height, pageX, pageY)
})
}
}
return (
<View ref={viewRef} style={{ backgroundColor: 'white', padding: 10 }}>
<Image source={{ uri: 'https://placehold.co/300x300.png' }} width={300} height={300} />
</View>
)
}
判斷內容長度是否需要滾動
如果希望頁面內容高度在小於設備屏幕高度時無需滾動,但在頁面內容高度小於設備屏幕高度時要可以滾動的話,就可以使用 onLayout 來和當前設備屏幕高度做比較:
- 獲取設備屏幕高度:useWindowDimensions
- 獲取頁面內容高度:onLayout
- 當頁面內容高度超過設備屏幕高度時才可以滾動
enabled={contentHeight > height}
import React, { useState } from 'react'
import { Platform, useWindowDimensions } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler'
export const ContentLayout = () => {
const { height } = useWindowDimensions()
const [contentHeight, setContentHeight] = useState(0)
const onLayout = (event: any) => {
const { height } = event.nativeEvent.layout
setContentHeight(height)
}
return (
<ScrollView
contentContainerStyle={{ flexGrow: 1 }}
enabled={contentHeight > height}
onLayout={onLayout}
>
{children}
</ScrollView>
)
}
總結
onLayout
- 用法:onLayout 屬性
- 使用時機:需監聽組件的佈局變化,實時獲取組件的大小和位置
measure
- 用法:ref
- 使用時機:在指定時機獲取組件尺寸和位置,比起 onLayout 還多回傳 pageX, pageY