Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

react native中4中获取元素定位的方法 #80

Open
wuyunqiang opened this issue Sep 30, 2024 · 0 comments
Open

react native中4中获取元素定位的方法 #80

wuyunqiang opened this issue Sep 30, 2024 · 0 comments

Comments

@wuyunqiang
Copy link
Owner

先有问题再有答案

  1. 什么场景下需要获取元素的位置信息?
  2. 如何获取子元素相对父元素的位置信息
  3. 如何获取元素相对屏幕的位置信息
  4. 这些方案有什么区别?
  5. 如何理解screen 和 window 的区别?

背景

RPReplay_Final1724766157.gif
如上demo 这是一个使用React Native构建的动画示例应用。它包含两个视图,一个红色视图(A)和一个蓝色视图(B)。我们需要将A移动到B的位置。

这个时候我们就需要知道A的位置和B的位置 然后通过react native提供的动画api, 在X轴上从其原始位置移动到与B一致的位置 以实现这个功能。

所以我们该如何获取A的位置信息和B的位置信息呢?

方法一:动态样式属性

React Native采用的是CSS in JS的方案。所有的样式都是通过JavaScript的对象来定义的,每个组件的样式都是私有的,可以避免不同组件之间的样式冲突。相同的样式可以被抽象成一个JavaScript对象,然后在不同的组件中重复使用。

由于样式是通过JS来定义的,所以可以根据状态或者属性动态生成样式。

代码如下

import React, { useRef } from 'react';
import { Animated, Easing, StyleSheet, Text, View } from 'react-native';

const styles = StyleSheet.create({
    A: {
        alignItems: 'center',
        backgroundColor: 'red',
        height: 50,
        justifyContent: 'center',
        left: 100,
        position: 'absolute',
        top: 50,
        width: 50,
        zIndex: 100,
    },
    B: {
        position: 'absolute',
        alignItems: 'center',
        backgroundColor: 'blue',
        height: 50,
        justifyContent: 'center',
        left: 300,
        top: 50,
        width: 50,
    },
    page: {
        backgroundColor: 'white',
        flex: 1,
        position: 'relative',
    },
    text: {
        color: 'white',
        fontSize: 20,
    },
});

const App = () => {
    const aValue = useRef(new Animated.Value(0)).current;
    setTimeout(() => {
        const ani = Animated.timing(aValue, {
            toValue: 1,
            duration: 1000,
            easing: Easing.bezier(0.4, 0, 0.2, 1),
            useNativeDriver: true,
        });
        ani.start(() => {
            setTimeout(() => {
                aValue.setValue(0);
            }, 1.5 * 1000);
        });
    }, 3 * 1000);
    return (
        <View style={styles.page}>
            <Animated.View
                style={[
                    styles.A,
                    {
                        transform: [
                            {
                                translateX: aValue.interpolate({
                                    inputRange: [0, 0.5, 1],
                                    outputRange: [0, 80, 200],
                                }),
                            },
                        ],
                    },
                ]}
            >
                <Text style={styles.text}>A</Text>
            </Animated.View>
            <View style={styles.B}>
                <Text style={styles.text}>B</Text>
            </View>
        </View>
    );
};
export default App;

由于样式就是一个js对象,我们可以动态的修改A元素在样式对象,因为是开发者设置的绝对布局的定位信息,所以我们可以知道B元素的位置信息。

通过动态样式属性的方法 我们可以准确的拿到在同一个参考体系下的两个元素的相对位置信息。例如A,B 都是绝对定位且都是相对于left设置的偏移。

如果将B的样式改为如下代码

const styles = StyleSheet.create({
    B: {
        position: 'absolute',
        alignItems: 'center',
        backgroundColor: 'blue',
        height: 50,
        justifyContent: 'center',
        right: 50, // 这里改为right
        top: 50,
        width: 50,
    }
});

现在我们不能直接知道视图A和视图B之间的距离,因为视图B的位置是相对于屏幕右侧确定的,而视图A的动画位置则是相对于父窗口左侧确定的。 因为在不同的屏幕宽度下 A,B的相对距离是会发生变化的 所以我们不能通过动态样式的方式直接感知到距离是多少。

方案二:onLayout回调

这种情况下我们可以使用react native 提供给我们的onLayout回调方法。当改变屏幕方向或者改变布局的时候,该函数会被触发,并且回传给我们一个包含layout属性的对象。layout属性是一个包含x,y,width,height的对象。

修改代码如下

const styles = StyleSheet.create({
    B: {
        alignItems: 'center',
        backgroundColor: 'blue',
        height: 50,
        justifyContent: 'center',
        position: 'absolute',
        right: 50,
        top: 50,
        width: 50,
    },
});

const App = () => {
    const aValue = useRef(new Animated.Value(0)).current;
    const layoutA = useRef<LayoutRectangle>(null);
    const layoutB = useRef<LayoutRectangle>(null);

    const startAni = () => {
        if (layoutA.current && layoutB.current) {
            setTimeout(() => {
                const ani = Animated.timing(aValue, {
                    toValue: layoutB.current.x - layoutA.current.x,
                    duration: 1000,
                    easing: Easing.bezier(0.4, 0, 0.2, 1),
                    useNativeDriver: true,
                });
                ani.start(() => {
                    setTimeout(() => {
                        aValue.setValue(0);
                    }, 1.5 * 1000);
                });
            }, 3 * 1000);
        }
    };

    const onLayoutB = (event: LayoutChangeEvent) => {
        layoutB.current = event.nativeEvent.layout;
        startAni();
    };

    const onLayoutA = (event: LayoutChangeEvent) => {
        layoutA.current = event.nativeEvent.layout;
        startAni();
    };

    return (
        <View style={styles.page}>
            <Animated.View
                onLayout={onLayoutA}
                style={[
                    styles.A,
                    {
                        transform: [
                            {
                                translateX: aValue,
                            },
                        ],
                    },
                ]}
            >
                <Text style={styles.text}>A</Text>
            </Animated.View>
            <View onLayout={onLayoutB} style={styles.B}>
                <Text style={styles.text}>B</Text>
            </View>
        </View>
    );
};
export default App;

因为A,B谁先layout完成不一定 所以这里需要在两个组件的onLayout方法中都调用动画执行的方法。

注意

  1. onLayout方法是异步的,这意味着在组件被渲染后,实际的布局可能需要一些事件才能被计算和提供。所以,不应该认为在componentDidMount或者useEffect中可以获取到布局值,这个节点只能说明vdom构建完成, 并不代表UI渲染完成,渲染UI的行为是由Native进行的。

    其实不管是react native还是纯web在浏览器环境中 本质上渲染都是异步的。 这也是为什么vue的 nextTick保证在dom完成执行而不保证在渲染完成执行
  2. 当每次改变屏幕方向或者改变布局的时候 onLayout事件就会被触发 所以可能被调用多次 需要在回调中做好兼容处理。
  3. x,y的相对于父组件的定位信息,而不是屏幕的。

因为这个是同一个父组件下的两个元素 所以可以使用x,y直接计算。

如果是跨多个层级的两个元素我们就需要获取到相对屏幕的x,y坐标 而不是相对于各自的父元素。

这个时候我们又该如何做?

方案三:measure方法

measure()是一个异步方法,它在一些场景下可以让你获取元素相对于其父元素或者根视图的位置信息。不同于onLayout 这个api是开发者可以手动调用的,一般在创建的ref上调用。

修改代码如下

const App = () => {
    const aValue = useRef(new Animated.Value(0)).current;

    const refA = useRef<View>(null);
    const refB = useRef<View>(null);
    const layoutA = useRef<{ x: number }>(null);
    const layoutB = useRef<{ x: number }>(null);

    const startAni = () => {
        if (layoutA.current && layoutB.current) {
            setTimeout(() => {
                const ani = Animated.timing(aValue, {
                    toValue: layoutB.current.x - layoutA.current.x,
                    duration: 1000,
                    easing: Easing.bezier(0.4, 0, 0.2, 1),
                    useNativeDriver: true,
                });
                ani.start(() => {
                    setTimeout(() => {
                        aValue.setValue(0);
                    }, 1.5 * 1000);
                });
            }, 3 * 1000);
        }
    };

    const onLayoutB = () => {
        // x和y:相对于其父元素的位置
        // width和height:组件的宽度和高度
        // pageX和pageY:相对于设备屏幕的位置
        refB.current.measure((x, y, width, height, pageX, pageY) => {
            layoutB.current = {
                x: pageX,
            };
            startAni();
        });
    };

    const onLayoutA = () => {
        refA.current.measure((x, y, width, height, pageX, pageY) => {
            layoutA.current = {
                x: pageX,
            };
            startAni();
        });
    };

    return (
        <View style={styles.page}>
            <Animated.View
                ref={refA}
                onLayout={onLayoutA}
                style={[
                    styles.A,
                    {
                        transform: [
                            {
                                translateX: aValue,
                            },
                        ],
                    },
                ]}
            >
                <Text style={styles.text}>A</Text>
            </Animated.View>
            <View ref={refB} onLayout={onLayoutB} style={styles.B}>
                <Text style={styles.text}>B</Text>
            </View>
        </View>
    );
};
export default App;

通过measure的方法获取pageX, pageY可以获取元素相对于屏幕的位置,然后在计算出相对位移,完成动画。

注意

和onLayout一样measure方法也是异步的 所以我们需要通过回调来获取参数,在调用时机上 一般建议在onLayout中执行,这样可以确保组件已经渲染完成。

方案四:measureInWindow方法

measureInWindow也可以获取元素的坐标点,但这个是相对于window的.

我们将上面的A的样式修改如下

    A: {
        alignItems: 'center',
        backgroundColor: 'red',
        height: 50,
        justifyContent: 'center',
        left: 100,
        position: 'absolute',
        top: 0, // 这里不设置顶部间距
        width: 50,
        zIndex: 100,
    }, 

android沉浸式效果如下:
IMG_20240828_164851.jpg

     const onLayoutA = () => {
        refA.current.measureInWindow((x, y, width, height) => {
            console.log('test measureInWindow: ', x, y, width, height);
        });

        refA.current.measure((x, y, width, height, pageX, pageY) => {
            console.log('test measure: ', x, y, width, height, pageX, pageY);
        });
    };

打印结果如下:
截屏2024-08-28 16.47.40.png

可以看到measure的pageX,pageY 为(100,0)
measureInWindow的x,y为(100,-32)。
这是因为measureInWindow是相对于window计算的,measure是相对于screen计算的。

window & screen的区别

ios: screen = window

android: screen = status bar + window + 底部Navigation bar(即虚拟按键)

所以我们应该尽量使用screen而不是window。

例如使用Dimensions.get('screen'),不要使用Dimensions.get('window')

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant