当前位置: 首页 > news >正文

【React Native】粘性布局StickyScrollView

React Native 实现粘性头部与动态缩放小头部组件(附源码解析)

一、前言

在移动端滚动视图(如商品详情页、个人中心页)中,粘性头部是常见交互:大头部随滚动渐变消失,小头部固定在顶部并伴随缩放/透明度动画,同时TabBar跟随粘性定位。本文基于react-nativeAnimated模块,实现一个可复用的粘性滚动组件。

二、组件概述

本组件StickyScrollView核心功能:

  1. 大头部:初始完全显示,滚动到一定距离后粘性定位
  2. 小头部:滚动到顶部时固定,伴随透明度(渐显)和缩放动画
  3. TabBar:跟随大头部保持粘性定位
  4. 通用粘性逻辑:通过Sticky组件复用,支持任意元素粘性
  5. Ref暴露:通过forwardRef暴露scrollY和headerHeight给父组件
  6. 滚动回调:支持自定义onScroll回调函数
  7. 可配置粘性偏移:支持自定义stickyHeaderLeftOffset调整粘性定位偏移量

三、快速上手(使用示例)

直接复制以下代码,替换renderXXX函数即可快速使用:

import StickyScrollView from './StickyScrollView';const DemoPage = () => {return (<StickyScrollView// 大头部(初始显示,滚动渐变消失)renderHeader={() => (<View style={{ height: 100, backgroundColor: '#ff5722' }}><Text>大头部(滚动消失)</Text></View>)}// 小头部(固定顶部,带缩放/透明度)renderSmallHeader={() => (<View style={{ height: 50, backgroundColor: '#2196f3' }}><Text>小头部(固定+缩放)</Text></View>)}// TabBar(跟随小头部粘性)renderTabBar={() => (<View style={{ height: 40, backgroundColor: '#4caf50' }}><Text>TabBar(粘性)</Text></View>)}// 主内容(填充剩余空间)renderContent={() => (<View style={{ height: 5000, backgroundColor: '#f0f0f0' }}><Text>主内容区域</Text></View>)}/>);
);export default DemoPage;

四、源码解析

组件分为两部分:通用粘性组件Sticky滚动容器StickyScrollView

import React, {forwardRef,useImperativeHandle,useMemo,useRef,useState,
} from 'react';
import {Animated,LayoutChangeEvent,View,ViewProps,type ViewStyle,
} from 'react-native';
import {NativeSyntheticEvent} from 'react-native/Libraries/Types/CoreEventTypes';
import {NativeScrollEvent} from 'react-native/Libraries/Components/ScrollView/ScrollView';const Sticky = forwardRef<typeof Animated.View & View,{stickyWhileScrollY?: number;scrollY: Animated.Value;} & ViewProps
>(({stickyWhileScrollY, scrollY, children, style, onLayout, ...otherProps},ref,) => {const [posY, setPosY] = useState(0);const handleLayout = (event: LayoutChangeEvent) => {setPosY(event.nativeEvent.layout.y);onLayout?.(event);};const translateY = useMemo(() => {const bY = stickyWhileScrollY ? stickyWhileScrollY : posY;return scrollY.interpolate({inputRange: [-1, 0, bY, bY + 1],outputRange: [0, 0, 0, 1],});}, [stickyWhileScrollY, posY, scrollY]);return (<Animated.Viewref={ref}style={[style,{position: 'relative',zIndex: 1,},{transform: [{translateY}]} as ViewStyle,]}onLayout={handleLayout}{...otherProps}>{children}</Animated.View>);},
);export interface StickyScrollViewRef {scrollY: Animated.Value;headerHeight: number;
}interface StickyScrollViewProps {renderHeader?: () => React.ReactNode;renderSmallHeader?: () => React.ReactNode;renderContent?: () => React.ReactNode;renderTabBar?: () => React.ReactNode;style?: ViewStyle;onScroll?:| ((event: NativeSyntheticEvent<NativeScrollEvent>) => void)| undefined;// 头布局折叠剩余距离,默认为 0,完全折叠stickyHeaderLeftOffset?: number;
}const StickyScrollView = forwardRef<StickyScrollViewRef, StickyScrollViewProps>((props: StickyScrollViewProps, ref) => {const scrollY = useRef(new Animated.Value(0));const [headerHeight, setHeaderHeight] = useState(0);const [smallHeaderHeight, setSmallHeaderHeight] = useState(0);useImperativeHandle(ref, () => ({scrollY: scrollY.current,headerHeight: headerHeight,}));const smallHeaderOpacity = () => {if (smallHeaderHeight <= 0 || !scrollY.current) return 0; // 默认隐藏return scrollY.current.interpolate({inputRange: [headerHeight - smallHeaderHeight - 5,headerHeight - smallHeaderHeight,],outputRange: [0, 1],extrapolate: 'clamp', // 超出范围保持边界值});};const calSmallHeaderScale = () => {if (smallHeaderHeight <= 0 || !scrollY.current) return 0; // 默认隐藏return scrollY.current.interpolate({inputRange: [0, headerHeight - smallHeaderHeight],outputRange: [headerHeight / smallHeaderHeight, 1],extrapolate: 'clamp', // 超出范围保持边界值});};const handleHeaderLayout = (event: LayoutChangeEvent) => {setHeaderHeight(event.nativeEvent.layout.height);};const handleSmallHeaderLayout = (event: LayoutChangeEvent) => {if (calSmallHeaderScale() <= 0) {setSmallHeaderHeight(event.nativeEvent.layout.height);}};const offset = () => {let a =headerHeight - smallHeaderHeight - (props.stickyHeaderLeftOffset ?? 0);return a < 0 ? 0 : a;};const header = () => {return (props.renderHeader && (<StickystickyWhileScrollY={offset()}scrollY={scrollY.current}onLayout={handleHeaderLayout}>{props.renderHeader()}</Sticky>));};const smallHeader = () => {return (props.renderSmallHeader && (<Animated.Viewstyle={{width: '100%',zIndex: 3,opacity: smallHeaderOpacity(),position: 'absolute',top: 0,}}onLayout={handleSmallHeaderLayout}>{props.renderSmallHeader()}</Animated.View>));};const tab = () => {return (props.renderTabBar && (<Sticky stickyWhileScrollY={offset()} scrollY={scrollY.current}>{props.renderTabBar()}</Sticky>));};const content = () => {return props.renderContent && props.renderContent();};return (<View style={props.style}><Animated.ScrollViewshowsVerticalScrollIndicator={false}onScroll={Animated.event([{nativeEvent: {contentOffset: {y: scrollY.current}},},],{useNativeDriver: true, listener: props.onScroll},)}scrollEventThrottle={1}>{header()}{tab()}{content()}</Animated.ScrollView>{smallHeader()}</View>);},
);export default StickyScrollView;

1. 通用粘性组件:Sticky

负责实现元素的粘性定位,核心是通过translateY动画控制元素滚动时的偏移。该组件使用forwardRef接收和传递ref,支持外部访问和控制。

关键逻辑说明:
  • posY:记录元素布局后的Y坐标(通过onLayout回调获取)。
  • stickyWhileScrollY:可选的粘性触发阈值,如果未提供则使用元素自身的Y坐标。
  • translateY:通过scrollY.interpolate计算偏移量。当滚动到阈值时,元素开始向下偏移,视觉上保持“固定”效果。
  • 动画插值:当滚动位置在[-1, bY]范围内时,元素正常滚动;当超过bY时,开始产生偏移。

2. 滚动容器:StickyScrollView

负责管理滚动状态、动画计算和子元素渲染。组件使用forwardRef暴露滚动状态给父组件。

关键逻辑说明:
  1. 状态管理

    • scrollY:记录滚动位置,驱动所有动画。
    • headerHeight/smallHeaderHeight:记录大/小头部的实际高度,用于计算动画参数。
    • ref暴露:通过useImperativeHandle向父组件暴露scrollYheaderHeight,便于自定义扩展。
  2. 粘性阈值计算

    • offset()函数:计算粘性触发的Y坐标,支持通过stickyHeaderLeftOffset配置头布局折叠后的剩余距离。
  3. 动画计算

    • 小头部透明度:当滚动位置接近大头部底部时,小头部从透明(0)渐变到不透明(1)。
    • 小头部缩放:当滚动位置在[0, headerHeight - smallHeaderHeight]范围内时,小头部从放大状态(比例为headerHeight/smallHeaderHeight)缩小到原始大小(1)。
  4. 布局测量

    • handleHeaderLayout:测量并记录大头部的实际高度。
    • handleSmallHeaderLayout:测量并记录小头部的实际高度,确保只在缩放比例为0时更新。
  5. 渲染逻辑

    • 大头部和TabBar通过Sticky组件实现粘性,粘性阈值为offset()函数返回值。
    • 小头部使用绝对定位(position: 'absolute')固定在顶部,绑定缩放和透明度动画。
    • 支持通过onScroll属性自定义滚动事件处理。
http://www.dtcms.com/a/576804.html

相关文章:

  • 无人机RTMP推流平台EasyDSS:构建新一代智能交通视频监控系统
  • 从大数据角度看时序数据库选型:Apache IoTDB的实战经验分享
  • Apache Drill 连接 MySQL 或 PostgreSQL 数据库
  • React Native App 图表绘制完整实现指南
  • 做招商加盟网站怎么样济南网站优化的周期
  • 怡梦姗网站做么动漫与游戏制作专业就业方向
  • js原生、vue导出、react导出、axios ( post请求方式)跨平台导出下载四种方式的demo
  • Springboot + vue 宿舍管理系统
  • 【Python3教程】Python3高级篇之pip标准包管理工具
  • 段权限检查(Segement Privilege Check)
  • JD京东线下HR面(准备)
  • 构建高可靠 OpenEuler 运维体系:从虚拟化部署到 Systemd 自动化核心实践
  • 让医学影像跨越“域”的鸿沟:FAMNet 的频域觉知匹配新思路
  • 麒麟Server版安装EMQX
  • 数字机器人教学项目开发:基于Python的教育技术创新实践
  • 《C语言疑难点 --- C语内存函数专题》
  • 公司网站建设文章wordpress cms主题教程
  • 第十天~ARXML IPDU Group全面解析:从基础到高级批量控制策略
  • 【029】智能停车计费系统
  • 51CTO学院个人网站开发视频经典 wordpress主题下载
  • Java大厂面试真题:Spring Boot + 微服务 + 缓存架构三轮技术拷问实录
  • 患者随访管理抖音快手微信小程序看广告流量主开源
  • 做视频资源网站有哪些内容网站浮动代码
  • c#笔记之类的继承
  • Flink 流式计算的状态之道从 Table/SQL 语义到算子状态与 TTL 精准控制
  • 嘉兴做微网站多少钱有哪些好的网站
  • ps -ef | grep redis
  • 网站开发语言有哪些网站开发的问题
  • 在 JavaScript 中, `Map` 和 `Object` 都可用于存储键值对,但设计目标、特性和适用场景有显著差异。
  • Vue 3中reactive函数如何通过Proxy实现响应式?使用时要避开哪些误区?