【问题标题】:How to animate a View sticky at a certain position on scrolling in React Native?如何在 React Native 中滚动时在特定位置为 View 设置动画?
【发布时间】:2020-05-07 11:14:53
【问题描述】:

我正在尝试为View 设置动画,使其在滚动时粘在某个位置。我使用的滚动组件是FlatList

这是我到目前为止所做的:

ProfileScreen

基本上,这就是我的ProfileScreen.js的结构:

<View>
    <Animated.View /> --> The animated cover photo (includes the animated avatar as well)

    <Animated.FlatList /> --> This renders the list of images you see in the video. And a ListHeaderComponent renders the content from the name to the View of follow numbers
</View

现在我想要实现的是:

当用户滚动整个FlatList并到达包含跟随数字View到达标题的底部边框的位置时,follow View 会坚持下去。 (关注View必须与FlatList一起滚动)

就像一个粘性标签视图。

你可以想象我的“目标”屏幕就像 Twitter 的个人资料屏幕。

这是完整的代码:

//...
const ProfileScreen = (props) => {

    const [index, setIndex] = useState(0);
    const [isFollowed, setIsFollowed] = useState(false);
    const [firstFollow, setFirstFollow] = useState(false);
    const [enableScrollView, setEnableScrollView] = useState(false);
    const trips = useSelector(state => state.trips.availableTrips);
    const scrollY = useRef(new Animated.Value(0)).current;

    const headerHeight = scrollY.interpolate({
        inputRange: [0, HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT], // [0, 50]
        outputRange: [HEADER_MAX_HEIGHT, HEADER_MIN_HEIGHT], // [120,70]
        extrapolate: 'clamp'
    });

    const profileImageHeight = scrollY.interpolate({
        inputRange: [0, HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT-10],
        outputRange: [PROFILE_IMAGE_MAX_HEIGHT,PROFILE_IMAGE_MIN_HEIGHT],
        extrapolate: 'clamp'
    });
  
    const profileImageMarginTop = scrollY.interpolate({
        inputRange: [0, HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT],
        outputRange: [
          HEADER_MAX_HEIGHT - PROFILE_IMAGE_MAX_HEIGHT / 2,
          HEADER_MIN_HEIGHT /2 - PROFILE_IMAGE_MIN_HEIGHT/2
        ],
        extrapolate: 'clamp'
    });
    const profileImageMarginLeft = scrollY.interpolate({
        inputRange: [0, HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT],
        outputRange: [
            WIDTH/2 - PROFILE_IMAGE_MAX_HEIGHT/2,
            10
        ],
        extrapolate: 'clamp'
    })
    const headerZindex = scrollY.interpolate({
        inputRange: [0, HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT, 120],
        outputRange: [0, 0, 1000],
        extrapolate: 'clamp'
    });
  
    const headerTitleColor = scrollY.interpolate({
        inputRange: [0, 50, 70, 80, 90, 100],
        outputRange: ['transparent', 'transparent', 'transparent', 'transparent', 'transparent', 'white'],
        extrapolate: 'extend'
    });

    const tabBarPosition = scrollY.interpolate({
        inputRange: [0,200],
        outputRange: [(HEADER_MAX_HEIGHT + PROFILE_IMAGE_MIN_HEIGHT)*2, HEADER_MIN_HEIGHT],
        extrapolate: 'clamp'
    })


    const _render_Sitcky_Info_View = () => {
        return(
            <View style={{marginTop: HEADER_MAX_HEIGHT+ PROFILE_IMAGE_MAX_HEIGHT/2}}>
                <InfoDisplay
                    isFollowed={isFollowed}
                    onFollow={onFollow}
                    userName={profile.userName}
                    tripsNumber={trips.length}
                    navigateToTripsListScreen={() => props.navigation.navigate('TripsListScreen')}
                />
                
            </View>
        )
    }

    return(
        <SafeAreaView style={styles.container}>
            <Animated.View style={[styles.backgroundImage, {
                height: headerHeight,
                zIndex: headerZindex,
                elevation: headerZindex,
                justifyContent: 'center'
            }]}>
                <Animated.Image 
                    source={require('../../../assets/images/beach.jpg')} 
                    style={{
                        flex: 1,
                    }}
                />
                <View style={{
                    position: 'absolute',
                    left: 60,
                }}>
                    <Animated.Text style={{color: headerTitleColor, fontSize: 20, fontWeight: 'bold'}}>{profile.userName}</Animated.Text>
                </View>                
                <Animated.View style={[styles.profileImgContainer,{
                    height: profileImageHeight,
                    width: profileImageHeight,
                    borderRadius: PROFILE_IMAGE_MAX_HEIGHT/2,
                    position: 'absolute',
                    top: profileImageMarginTop,
                    left: profileImageMarginLeft,
                    alignSelf: 'center'
                }]}>                         
                    <Image source={{uri: profile.userAvatar}} style={styles.profileImg} />
                </Animated.View>
            </Animated.View>
            
            <Animated.FlatList
                style={{flex: 1, backgroundColor: 'transparent'}}
                contentContainerStyle={{justifyContent: 'center', alignItems: 'center'}}
                scrollEventThrottle={16}
                onScroll={Animated.event(
                    [{nativeEvent: {contentOffset: {y: scrollY}}}],
                    
                )}
                showsVerticalScrollIndicator={false}
                bounces={true}
                data={imgData}
                numColumns={NUM_COLUMNS}
                keyExtractor={item => item.id}
                renderItem={(itemData) => {
                    return(
                        <TouchableOpacity style={{
                            marginHorizontal: findMidOfThree(imgData, itemData.index) ? 0 : 5 ,
                            marginVertical: 3,
                            shadowColor: 'black',
                            shadowOpacity: 0.26,
                            shadowOffset: { width: 0, height: 2 },
                            shadowRadius: 8,
                            elevation: 7,
                    }}>
                            <Image source={itemData.item.source} style={{
                                height: WIDTH/3.5, 
                                width: WIDTH/3.5,
                                borderColor: 'white',
                                borderWidth: 2,
                                borderRadius: 10,
                                
                            }} />
                        </TouchableOpacity>
                    )
                }}
                ListHeaderComponent={_render_Sitcky_Info_View}
                {...props}
            >
                
                
            </Animated.FlatList>
        </SafeAreaView>
    );
};

export default ProfileScreen;

const styles = StyleSheet.create({
//...

})

InfoDisplay 包含关注号码 View。我知道我必须把它分开并带到ProfileScreen。我还尝试使用tabBarPosition 对其进行动画处理,但它并没有像我想象的那样工作

你可以想象我的“目标”屏幕就像 Twitter 个人资料屏幕,我的关注号码 View 就像 Twitter 中的标签视图

https://drive.google.com/file/d/1ERt_7gnPgiwXPg-WODXSnrOZ10kQgPQl/view?usp=drivesdk

请帮助我。我将不胜感激!

【问题讨论】:

  • 你能分享一下最终结果吗,我没有推特应用!
  • 谢谢奥利弗。我已添加到问题内容中

标签: javascript reactjs react-native animation react-animated


【解决方案1】:

计算如图所示follow View需要移动的距离(设为x)。

这里我取 500 作为补偿。您可以选择任何大于 x 的数字。 (或使用屏幕高度

const range = [x, x+500];
const translateY = scrollY.interpolate({inputRange:range, outputRange:range, extrapolateLeft: 'clamp'});

即,如果 x = 200;

const translateY = scrollY.interpolate({inputRange:[200, 700], outputRange:[200, 700], extrapolateLeft: 'clamp'});

逻辑是,当follow View 在滚动时到达顶部(即移动距离x)。它将以相同的滚动速度开始向下翻译。

将此 translateY 提供给后续视图。

<Animated.View style={{...styles, transform: [{translateY}]}} />  // your  follow View

希望对你有帮助!

【讨论】:

  • 非常感谢@AswinC 的帮助。我会尝试实现它并提供更新
猜你喜欢
  • 2017-10-08
  • 1970-01-01
  • 2018-05-07
  • 1970-01-01
  • 1970-01-01
  • 2018-10-25
  • 2018-12-02
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多