react-native-reanimated measure() 详解:同步测量视图位置与尺寸
【免费下载链接】react-native-reanimatedReact Native's Animated library reimplemented项目地址: https://gitcode.com/GitHub_Trending/re/react-native-reanimated
导读
measure()是 react-native-reanimated 提供的原生方法之一,用于在 UI 线程上同步获取某个视图在屏幕上的位置(x、y、pageX、pageY)与尺寸(width、height)。本篇文章基于仓库 版本 2.x 官方文档,结合当前仓库的源码实现,全面讲解measure()的用法、返回值、注意事项、常见坑位与实战示例。读完本文,你将掌握如何在动画 worklet 中安全地同步读取视图测量值,并理解其底层实现原理。
1. measure() 是什么
measure()是一个同步函数,用于在当前屏幕视口(viewport)内确定给定视图的位置、宽度和高度,并返回一个包含测量结果的对象;如果该视图无法被测量,则返回null。
从源码结构看,measure在仓库中位于 packages/react-native-reanimated/src/platformFunctions/ 目录下,分为两个平台实现:
- measure.native.ts:原生(Android / iOS / macOS)实现,运行在 UI 运行时(worklet)中;
- measure.ts:Web 实现,基于 DOM 的
getBoundingClientRect等方法。
两者通过 src/index.ts 统一对外导出,因此无论是原生平台还是 Web 平台,开发者都使用同一套 API 签名。
小提示:如果你只是需要尽可能早地拿到测量值,且不需要
pageX与pageY,官方文档建议优先使用 React Native 的onLayout属性,因为它更轻量、触发时机更早。
2. 函数签名与参数
type Measure = <TRef extends InstanceOrElement>( animatedRef: AnimatedRef<TRef> ) => MeasuredDimensions | null;签名来自 measure.native.ts。
2.1 animatedRef 参数
measure()的唯一参数是animatedRef,它是useAnimatedRef的返回值。useAnimatedRef是 Reanimated 对标准 React ref 的扩展,关键区别在于:它能够在 UI 线程上交付视图标签(view tag),这正是measure()能在 worklet 内部直接同步读取测量值的前提。
从 useAnimatedRef.ts 的实现可以看到,它内部还会处理可滚动组件的特殊情况——如果组件带有getScrollableNode或getNativeScrollRef方法(如ScrollView、FlatList),会优先返回其可滚动节点,确保测量的是实际滚动内容对应的原生视图。
2.2 返回值 MeasuredDimensions
返回对象类型为MeasuredDimensions,在 commonTypes.ts 中定义,包含六个字段:
| 字段 | 含义 | 说明 |
|---|---|---|
x | X 坐标 | 相对于父组件的横坐标 |
y | Y 坐标 | 相对于父组件的纵坐标 |
width | 宽度 | 组件的宽度 |
height | 高度 | 组件的高度 |
pageX | 屏幕级 X 坐标 | 相对于屏幕的横坐标 |
pageY | 屏幕级 Y 坐标 | 相对于屏幕的纵坐标 |
如果测量未能执行成功(例如视图尚未渲染),则返回null。
六字段的语义注释同样可以在 commonTypes.ts 的 JSDoc 中看到:
x/y是相对父组件,pageX/pageY是相对屏幕。二者是定位动画、弹窗、Tooltip 等场景最常用的两组数据。
3. 基本示例
官方文档给出了一个最典型的用法:在useDerivedValue的 worklet 中持续测量视图:
const Comp = () => { const aref = useAnimatedRef(); useDerivedValue(() => { const measured = measure(aref); if (measured !== null) { const { x, y, width, height, pageX, pageY } = measured; console.log({ x, y, width, height, pageX, pageY }); } else { console.warn('measure: could not measure view'); } }); return <View ref={aref} />; };要点拆解:
- 先
useAnimatedRef()创建 animated ref,并绑定到目标<View ref={aref} />; - 在 worklet 内调用
measure(aref),获得同步测量结果; - 务必对返回值做
null检查,因为测量失败时返回的是null,直接解构会抛错。
4. 返回值可能为 null 的场景(重要)
:::infomeasure()只能对已渲染的组件生效。例如,对屏幕外的FlatList列表项调用measure(),会返回null。因此,在使用返回值之前执行null检查是一个良好的实践。 :::
从源码实现看,原生实现 measureNative 中返回null的路径不止一种:
- React Native 运行时调用:当
globalThis.__RUNTIME_KIND === RuntimeKind.ReactNative时(即不是在 UI 运行时调用),直接返回null; - 视图标签无效:
animatedRef.value为空时返回null,源码会打印警告:"The view with tag ... is not a valid argument for measure()...",并提示这可能是视图未渲染导致的(例如屏幕外的 FlatList 项); - LayoutMetrics 未就绪:底层
global._measure(viewTag)返回null时,同样返回null并给出警告,提示视图可能尚未渲染; - Android 视图被 flatten:当测量结果的
x为NaN时,返回null并警告 "The view gets view-flattened on Android",此时需要给组件设置collapsable={false}来禁用视图扁平化。
另外值得注意:在 Jest 测试环境中(IS_JEST为真),measure会被替换为 measureJest,直接返回null并提示 "measure() cannot be used with Jest.",因此单元测试中无法使用该方法。
5. 在 useAnimatedStyle 中调用的告警与规避
:::tip 如果在useAnimatedStyle内部调用measure,你可能会看到如下警告:
[Reanimated] measure() was called from the main JS context. Measure is only available in the UI runtime. (...)
这背后的原因是:在 React Native 应用中,useAnimatedStyle的 worklet 在首次渲染期间会先在 JS 上下文中求值一次,而此刻原生端尚未完成渲染,因此measure()在 JS 上下文中不可用。这个警告本身是安全的,可以忽略;但如果不想看到它,可以用如下方式包裹调用:
if (_WORKLET || isWeb) { const measured = measure(animatedRef); if (measured !== null) { // ... } }_WORKLET是 Reanimated 注入的全局标志,在 UI 运行时(worklet)中为true;isWeb用于 Web 平台(其实现不依赖 UI 运行时)。用_WORKLET || isWeb判断后,JS 首次求值阶段就不会触发measure(),告警自然消失。 :::
这一行为与源码中的运行时检查完全吻合:原生实现 measureNative 首先判断globalThis.__RUNTIME_KIND,当不在 UI 运行时(React Native 主线程上下文)时直接返回null。因此即使在useAnimatedStyle中不包裹判断,行为也是安全的(返回null),只是会伴随告警日志。
6. 与调试工具的兼容性
:::info 当 Chrome Developer Tools(远程 JS 调试器)连接时,measure不可用。不过,React Native 官方推荐的调试工具Flipper(Chrome DevTools)支持measure,更多细节可参见调试指南。 :::
原因在于远程 JS 调试会把 worklet 的求值移出 UI 运行时,导致依赖原生 view tag 的同步测量无法进行。建议在真机调试或使用支持 UI 运行时调试的工具(如 Flipper)时使用measure()。
7. 底层实现原理:原生端如何测量
7.1 原生(Android / iOS)路径
在 measure.native.ts 中,measureNative是一个标记了'worklet'的函数,执行流程如下:
- 检查运行环境:若
globalThis.__RUNTIME_KIND !== RuntimeKind.ReactNative(即处于 UI 运行时),继续执行; - 从
animatedRef.value取出视图标签(view tag / shadow node wrapper); - 调用 UI 运行时提供的全局函数
global._measure!(viewTag); - 依次处理三种失败情形(视图标签为空、
LayoutMetrics未就绪、Android 上x为NaN),均返回null并打印日志; - 成功时返回
MeasuredDimensions对象。
global._measure的类型声明位于 privateGlobals.d.ts,其签名为(shadowNodeWrapper: ShadowNodeWrapper | null) => MeasuredDimensions,是由原生模块注入 UI 运行时的内部能力。这意味着在 RN 的新架构(Fabric)下,测量直接基于 ShadowNode 完成,能够拿到屏幕坐标系中的真实布局数据。
7.2 Web 路径
在 measure.ts 中,Web 实现不依赖 UI 运行时,而是直接操作 DOM:
- 通过
animatedRef()获取对应的HTMLElement(Web 端的 animated ref 是一个函数式 ref); - 元素不存在时返回
null并打印与原生端一致的告警; - 使用
element.getBoundingClientRect()取得视口偏移(pageX、pageY),用offsetWidth、offsetHeight、offsetLeft、offsetTop填充其余四个字段。
注意一个平台差异:原生端的
x/y是相对父组件,Web 端实现使用的是offsetLeft/offsetTop,同样是相对定位父级(offsetParent)的偏移,语义上保持一致。
8. 实战:用 measure() 实现跟随式 Tooltip
下面是一个把measure()用于实际业务(弹窗/气泡定位)的完整示例——在点击时同步测量目标视图的位置,然后把 Tooltip 绝对定位到其下方:
import { View, Text, TouchableOpacity } from 'react-native'; import Animated, { useAnimatedRef, useSharedValue, withTiming, } from 'react-native-reanimated'; const TooltipDemo = () => { const targetRef = useAnimatedRef(); const tipX = useSharedValue(0); const tipY = useSharedValue(0); const opacity = useSharedValue(0); const showTooltip = () => { // 注意:此处在 JS 线程也能调用 measure,但更推荐在 // worklet 中调用以获得同步、无跨线程延迟的测量结果 'worklet'; const m = measure(targetRef); if (m === null) { return; } tipX.value = m.pageX; tipY.value = m.pageY + m.height + 4; // 出现在目标下方 opacity.value = withTiming(1, { duration: 150 }); }; return ( <View style={{ flex: 1, paddingTop: 120 }}> <TouchableOpacity ref={targetRef} onPress={showTooltip}> <Text>点击我显示提示</Text> </TouchableOpacity> <Animated.View style={{ position: 'absolute', left: tipX, top: tipY, opacity, }}> <Text>这是跟随式 Tooltip</Text> </Animated.View> </View> ); };关键点:
- 使用
measure(targetRef)拿到目标视图相对屏幕的坐标(pageX/pageY)与高度height; - 将结果写入 shared value,再由
Animated.View的left/top驱动,实现与原生布局完全同步的定位; - 依然保留了
null检查,避免在目标未渲染时崩溃。
9. 最佳实践小结
- 始终做 null 检查:
measure()返回null的场景很多(未渲染、屏幕外 FlatList 项、Android 视图 flatten 等),这是官方文档强调的第一要点; - 优先在 worklet 中调用:
measure的设计目标是 UI 运行时内的同步测量;在useAnimatedStyle等首次 JS 求值阶段调用会触发告警,可用_WORKLET || isWeb判断规避; - Android 上遇到 NaN 检查
collapsable:如果测量结果异常,可尝试为目标组件设置collapsable={false}禁用视图扁平化; - 能不用就不用:若只需要尺寸数据、不需要屏幕坐标,
onLayout是更轻量的替代方案; - 调试注意:远程 JS 调试(Chrome DevTools)下不可用,推荐使用 Flipper 调试;
- 测试注意:Jest 环境下
measure()直接返回null,编写单元测试时应做相应 mock 或规避。
相关资源
- measure 官方文档(版本 2.x)
- scrollTo 官方文档(版本 2.x)
- 原生实现源码
- Web 实现源码
- MeasuredDimensions 类型定义
- useAnimatedRef 源码
- measure 导出位置
【免费下载链接】react-native-reanimatedReact Native's Animated library reimplemented项目地址: https://gitcode.com/GitHub_Trending/re/react-native-reanimated
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考