07_Flutter使用NestedScrollView+TabBarView滚动位置共享问题修复

embedded/2024/9/22 21:05:45/

07_Flutter使用NestedScrollView+TabBarView滚动位置共享问题修复

一.案发现场

在这里插入图片描述

可以看到,上图中三个列表的滑动位置共享了,滑动其中一个列表,会影响到另外两个,这显然不符合要求,先来看下布局,再说明产生这个问题的原因:

  • 布局整体使用NestedScrollView,顶部banner和TabBar通过headerSliverBuilder创建,body为TabBarView,TabBarView中有三个列表,通过TabController与TabBar实现联动,同时每一个列表通过继承StatefulWidget构建并混入AutomaticKeepAliveClientMixin,重写wantKeepAlive的getter方法返回true,这样可以保证每次切换Tab的时候,ListView不会重新创建,实现懒加载。

    dart">NestedScrollView(headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {return [SliverToBoxAdapter(child: Container(height: 200,color: Colors.red,alignment: Alignment.center,child: const Text("banner",style: TextStyle(color: Colors.white,fontSize: 16),),),),SliverOverlapAbsorber(handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),sliver: StickySliverToBoxAdapter(child: Container(color: Colors.white,child: TabBar(tabs: List.generate(_tabs.length, (index) {return Padding(padding: const EdgeInsets.symmetric(vertical: 15),child: Text(_tabs[index]),);}),unselectedLabelColor: const Color(0xFF7B7B7B),labelColor: const Color(0xFF5E80FF),isScrollable: false,indicatorSize: TabBarIndicatorSize.label,indicator: UnderlineTabIndicator(borderRadius: BorderRadius.circular(3),borderSide: const BorderSide(color: Color(0xFF5E80FF), width: 3),insets: const EdgeInsets.symmetric(horizontal: 3, vertical: 9)),controller: _tabController,),),),),];},body: LayoutBuilder(builder: (context, _) {return Container(padding: EdgeInsets.only(top: NestedScrollView.sliverOverlapAbsorberHandleFor(context).layoutExtent ?? 0),child: NestedTabBarView(controller: _tabController,children: List.generate(_tabs.length, (index) {return _TabInnerListView(tabName: _tabs[index],);})));})
    )class _TabInnerListViewState extends State<_TabInnerListView> with AutomaticKeepAliveClientMixin {final int length = 20;Widget build(BuildContext context) {return CustomScrollView(physics: const ClampingScrollPhysics(),slivers: [...(List.generate(length, (index) {return SliverToBoxAdapter(child: Container(height: 100,margin: const EdgeInsets.only(top: 16, left: 16, right: 16),color: Colors.orange,alignment: Alignment.center,child: Text("${widget.tabName} item $index",style: const TextStyle(color: Colors.white),),),);})),const SliverToBoxAdapter(child: SizedBox(height: 16,),)],);}bool get wantKeepAlive => true;}
    
  • 上述问题产生的原因,需要追踪NestedScrollView的源码,NestedScrollView整体的布局结构如下:

    在这里插入图片描述

    如果没有_NestedScrollCoordinator的加持,那么外层的CustomScrollView和内层的CustomScrollView就会各划各的。_NestedScrollCoordinator处理嵌套滑动是在applyUserOffset方法中完成的:

    dart">class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldController {void applyUserOffset(double delta) {updateUserScrollDirection(delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse,);assert(delta != 0.0);if (_innerPositions.isEmpty) {_outerPosition!.applyFullDragUpdate(delta);} else if (delta < 0.0) {...} else {double innerDelta = delta;if (_floatHeaderSlivers) {innerDelta = _outerPosition!.applyClampedDragUpdate(delta);}if (innerDelta != 0.0) {double outerDelta = 0.0;final List<double> overscrolls = <double>[];final List<_NestedScrollPosition> innerPositions = _innerPositions.toList();for (final _NestedScrollPosition position in innerPositions) {final double overscroll = position.applyClampedDragUpdate(innerDelta);outerDelta = math.max(outerDelta, overscroll);overscrolls.add(overscroll);}if (outerDelta != 0.0) {outerDelta -= _outerPosition!.applyClampedDragUpdate(outerDelta);}for (int i = 0; i < innerPositions.length; ++i) {final double remainingDelta = overscrolls[i] - outerDelta;if (remainingDelta > 0.0) {innerPositions[i].applyFullDragUpdate(remainingDelta);}}}}}
    }
    

    可以看到在applyUserOffset中,是通过_NestedScrollPosition的applyFullDragUpdate响应滑动事件的,如果调用_outerPosition!.applyFullDragUpdate,则外层的CustomScrollView滑动。同理,内层CustomScrollView滑动,只不过applyUserOffset在处理内层滑动时,是遍历innerPositions把所有内层CustomScrollView的_NestedScrollPosition滚动相同的位移。

    dart">_NestedScrollPosition? get _outerPosition {if (!_outerController.hasClients) {return null;}return _outerController.nestedPositions.single;
    }Iterable<_NestedScrollPosition> get _innerPositions {return _innerController.nestedPositions;
    }
    

    这也就解释了上图中,为什么滚动其中一个列表,其他列表也会跟着滑动相同的位置?。

二.解决方案

综上所述,_NestedScrollCoordinator的_innerPositions的返回结果是所有内层CustomScrollView的_NestedScrollPosition,要解决这个问题,我们只需要想办法将_NestedScrollCoordinator的_innerPositions的返回结果改变成只包含当前选中的内层CustomScrollView的_NestedScrollPosition即可,而_innerPositions的取值是来源于_innerController的nestedPositions。_innerController是一个_NestedScrollController对象,接着看_NestedScrollController的源码:

dart">class _NestedScrollController extends ScrollController {...ScrollPosition createScrollPosition(ScrollPhysics physics,ScrollContext context,ScrollPosition? oldPosition,) {return _NestedScrollPosition(coordinator: coordinator,physics: physics,context: context,initialPixels: initialScrollOffset,oldPosition: oldPosition,debugLabel: debugLabel,);}void attach(ScrollPosition position) {assert(position is _NestedScrollPosition);super.attach(position);coordinator.updateParent();coordinator.updateCanDrag();position.addListener(_scheduleUpdateShadow);_scheduleUpdateShadow();}void detach(ScrollPosition position) {assert(position is _NestedScrollPosition);(position as _NestedScrollPosition).setParent(null);position.removeListener(_scheduleUpdateShadow);super.detach(position);_scheduleUpdateShadow();}...Iterable<_NestedScrollPosition> get nestedPositions {// TODO(vegorov): use instance method version of castFrom when it is available.return Iterable.castFrom<ScrollPosition, _NestedScrollPosition>(positions);}
}

可以看到_NestedScrollController是私有类,并且NestedScrollView从头到尾都没有暴露任何可以修改或替换_innerController的方法给我们,因此,想在外部直接修改是不可能的。怎么办呢?

首先,内层的每一个CustomScrollView都是我们在外部人为编写的,我们可以在外部给内层的每一个CustomScrollView重新指定ScrollController,虽然暂时没什么卵用😄,但是别着急。

dart">class _TabInnerListViewState extends State<_TabInnerListView> with AutomaticKeepAliveClientMixin {final int length = 20;late ScrollController _scrollController;void initState() {super.initState();_scrollController = ScrollController();}void dispose() {_scrollController.dispose();super.dispose();}Widget build(BuildContext context) {return CustomScrollView(controller: _scrollController,physics: const ClampingScrollPhysics(),slivers: [...(List.generate(length, (index) {return SliverToBoxAdapter(child: Container(height: 100,margin: const EdgeInsets.only(top: 16, left: 16, right: 16),color: Colors.orange,alignment: Alignment.center,child: Text("${widget.tabName} item $index",style: const TextStyle(color: Colors.white),),),);})),const SliverToBoxAdapter(child: SizedBox(height: 16,),)],);}bool get wantKeepAlive => true;}

在这里插入图片描述

可以看到,此时嵌套滑动失效了,这是因为我们为内层的每一个CustomScrollView单独指定ScrollController后,CustomScrollView的滑动全部交给了这个这个ScrollController处理,跟NestedScrollView的_innerController已经没有半毛钱关系了。既然没有关系,那我们就建立关系,怎么建立:

  • 创建NestedInnerScrollController类继承ScrollController

  • 重写createScrollPosition方法,通过PrimaryScrollController.maybeOf(context)获取NestedScrollView的_innerController,将createScrollPosition转交给_innerController完成

  • 重写attach方法,将attach转交给_innerController完成

  • 重写detach方法,将detach转交给_innerController完成

  • 为每一个内层的CustomScrollView指定controller为NestedInnerScrollController的实例

    dart">class NestedInnerScrollController extends ScrollController {ScrollController? _inner;NestedInnerScrollController();ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition? oldPosition) {ScrollPosition scrollPosition;ScrollableState? scrollableState = context as ScrollableState;if(scrollableState != null) {_inner = PrimaryScrollController.maybeOf(scrollableState.context);}if(_inner == null) {scrollPosition = super.createScrollPosition(physics, context, oldPosition);} else {scrollPosition = _inner!.createScrollPosition(physics, context, oldPosition);}return scrollPosition;}void attach(ScrollPosition position) {super.attach(position);_inner?.attach(position);}void detach(ScrollPosition position) {_inner?.detach(position);super.detach(position);}}class _TabInnerListViewState extends State<_TabInnerListView> with AutomaticKeepAliveClientMixin {final int length = 20;late ScrollController _scrollController;void initState() {super.initState();_scrollController = NestedInnerScrollController();}void dispose() {_scrollController.dispose();super.dispose();}Widget build(BuildContext context) {return CustomScrollView(controller: _scrollController,physics: const ClampingScrollPhysics(),slivers: [...(List.generate(length, (index) {return SliverToBoxAdapter(child: Container(height: 100,margin: const EdgeInsets.only(top: 16, left: 16, right: 16),color: Colors.orange,alignment: Alignment.center,child: Text("${widget.tabName} item $index",style: const TextStyle(color: Colors.white),),),);})),const SliverToBoxAdapter(child: SizedBox(height: 16,),)],);}bool get wantKeepAlive => true;}
    

    在这里插入图片描述

可以看到,嵌套滑动它又回来了😄。那么接下来…,就只剩下解决共享滑动了:

  • 将TabBarView单独定义成StatefulWidget,这样我们就可以很方便的为每一个内层的CustomScrollView维护上面定义好的NestedInnerScrollController,同时通过TabController监听TabBar的选中状态。

    dart">class NestedTabBarView extends StatefulWidget {final TabController? controller;final List<Widget> children;final ScrollPhysics? physics;final DragStartBehavior dragStartBehavior;final double viewportFraction;final Clip clipBehavior;const NestedTabBarView({super.key,required this.children,this.controller,this.physics,this.dragStartBehavior = DragStartBehavior.start,this.viewportFraction = 1.0,this.clipBehavior = Clip.hardEdge,});State<StatefulWidget> createState() => _NestedTabBarViewState();
    }class _NestedTabBarViewState extends State<NestedTabBarView> {List<NestedInnerScrollController> _nestedInnerControllers = [];void initState() {super.initState();_initNestedInnerControllers();widget.controller?.addListener(_onTabChange);}void didUpdateWidget(covariant NestedTabBarView oldWidget) {super.didUpdateWidget(oldWidget);if(oldWidget.children.length != widget.children.length) {_initNestedInnerControllers();}}void dispose() {widget.controller?.removeListener(_onTabChange);_disposeNestedInnerControllers();super.dispose();}void _onTabChange() {}void _initNestedInnerControllers() {_disposeNestedInnerControllers();List<NestedInnerScrollController> controllers = List.generate(widget.children.length, (index) {return NestedInnerScrollController();});if(mounted) {setState(() {_nestedInnerControllers = controllers;});} else {_nestedInnerControllers = controllers;}}void _disposeNestedInnerControllers() {_nestedInnerControllers.forEach((element) {element.dispose();});}Widget build(BuildContext context) {return TabBarView(controller: widget.controller,physics: widget.physics,dragStartBehavior: widget.dragStartBehavior,viewportFraction: widget.viewportFraction,clipBehavior: widget.clipBehavior,children: widget.children);}
    }
    
  • 使用InheritedWidget,将NestedInnerScrollController暴露给对应的内层CustomScrollView使用

    dart">class _InheritedInnerScrollController extends InheritedWidget {final ScrollController controller;const _InheritedInnerScrollController({required super.child,required this.controller});bool updateShouldNotify(covariant _InheritedInnerScrollController oldWidget) => controller != oldWidget.controller;}class _NestedTabBarViewState extends State<NestedTabBarView> {...Widget build(BuildContext context) {return TabBarView(controller: widget.controller,physics: widget.physics,dragStartBehavior: widget.dragStartBehavior,viewportFraction: widget.viewportFraction,clipBehavior: widget.clipBehavior,children: List<Widget>.generate(widget.children.length, (index) {return _InheritedInnerScrollController(controller: _nestedInnerControllers[index],child: widget.children[index],);}));}
    }class NestedInnerScrollController extends ScrollController {...static ScrollController of(BuildContext context) {final _InheritedInnerScrollController? target = context.dependOnInheritedWidgetOfExactType<_InheritedInnerScrollController>();assert(target != null,'NestedInnerScrollController.of must be called with a context that contains a NestedTabBarView\'s children.',);return target!.controller;}static ScrollController? maybeOf(BuildContext context) {final _InheritedInnerScrollController? target = context.dependOnInheritedWidgetOfExactType<_InheritedInnerScrollController>();return target?.controller;}}class _TabInnerListViewState extends State<_TabInnerListView> with AutomaticKeepAliveClientMixin {final int length = 20;Widget build(BuildContext context) {return CustomScrollView(controller: NestedInnerScrollController.maybeOf(context),physics: const ClampingScrollPhysics(),slivers: [...],);}bool get wantKeepAlive => true;}
    

    使用的时候

    dart">NestedScrollView(headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {return [...];},body: LayoutBuilder(builder: (context, _) {return Container(padding: EdgeInsets.only(top: NestedScrollView.sliverOverlapAbsorberHandleFor(context).layoutExtent ?? 0),child: NestedTabBarView(controller: _tabController,children: List.generate(_tabs.length, (index) {return _TabInnerListView(tabName: _tabs[index],);})));})
    )
    
  • 监听TabBar的选中状态,然后通过NestedInnerScrollController将NestedScrollView的_innerController中所有的ScrollPosition detach,然后再attach与当前选中的NestedInnerScrollController对应的ScrollPosition。

    dart">class NestedInnerScrollController extends ScrollController {...void attachCurrent() {if(_inner != null) {while(_inner!.positions.isNotEmpty) {_inner!.detach(_inner!.positions.first);}_inner!.attach(position);}}}class _NestedTabBarViewState extends State<NestedTabBarView> {...void _onTabChange() {int index = widget.controller!.index;if (index == widget.controller!.animation?.value) {_nestedInnerControllers[index].attachCurrent();}}...
    }
    

    在这里插入图片描述

搞定。

三.完整代码
dart">class _NestedScrollPageState extends State<NestedScrollPage> with TickerProviderStateMixin {final List<String> _tabs = ["tab1", "tab2", "tab3"];late TabController _tabController;void initState() {super.initState();_tabController = TabController(length: _tabs.length, vsync: this);}void dispose() {_tabController.dispose();super.dispose();}Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text("nested scroll"),),body: SafeArea(child: NestedScrollView(headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {return [SliverToBoxAdapter(child: Container(height: 200,color: Colors.red,alignment: Alignment.center,child: const Text("banner",style: TextStyle(color: Colors.white,fontSize: 16),),),),SliverOverlapAbsorber(handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),sliver: StickySliverToBoxAdapter(child: Container(color: Colors.white,child: TabBar(tabs: List.generate(_tabs.length, (index) {return Padding(padding: const EdgeInsets.symmetric(vertical: 15),child: Text(_tabs[index]),);}),unselectedLabelColor: const Color(0xFF7B7B7B),labelColor: const Color(0xFF5E80FF),isScrollable: false,indicatorSize: TabBarIndicatorSize.label,indicator: UnderlineTabIndicator(borderRadius: BorderRadius.circular(3),borderSide: const BorderSide(color: Color(0xFF5E80FF), width: 3),insets: const EdgeInsets.symmetric(horizontal: 3, vertical: 9)),controller: _tabController,),),),),];},body: LayoutBuilder(builder: (context, _) {return Container(padding: EdgeInsets.only(top: NestedScrollView.sliverOverlapAbsorberHandleFor(context).layoutExtent ?? 0),child: NestedTabBarView(controller: _tabController,children: List.generate(_tabs.length, (index) {return _TabInnerListView(tabName: _tabs[index],);})));}))),);}}class _TabInnerListView extends StatefulWidget {final String? tabName;const _TabInnerListView({this.tabName});State<StatefulWidget> createState() => _TabInnerListViewState();}class _TabInnerListViewState extends State<_TabInnerListView> with AutomaticKeepAliveClientMixin {final int length = 20;Widget build(BuildContext context) {return CustomScrollView(controller: NestedInnerScrollController.maybeOf(context),physics: const ClampingScrollPhysics(),slivers: [...(List.generate(length, (index) {return SliverToBoxAdapter(child: Container(height: 100,margin: const EdgeInsets.only(top: 16, left: 16, right: 16),color: Colors.orange,alignment: Alignment.center,child: Text("${widget.tabName} item $index",style: const TextStyle(color: Colors.white),),),);})),const SliverToBoxAdapter(child: SizedBox(height: 16,),)],);}bool get wantKeepAlive => true;}
dart">class NestedTabBarView extends StatefulWidget {final TabController? controller;final List<Widget> children;final ScrollPhysics? physics;final DragStartBehavior dragStartBehavior;final double viewportFraction;final Clip clipBehavior;const NestedTabBarView({super.key,required this.children,this.controller,this.physics,this.dragStartBehavior = DragStartBehavior.start,this.viewportFraction = 1.0,this.clipBehavior = Clip.hardEdge,});State<StatefulWidget> createState() => _NestedTabBarViewState();
}class _NestedTabBarViewState extends State<NestedTabBarView> {List<NestedInnerScrollController> _nestedInnerControllers = [];void initState() {super.initState();_initNestedInnerControllers();widget.controller?.addListener(_onTabChange);}void didUpdateWidget(covariant NestedTabBarView oldWidget) {super.didUpdateWidget(oldWidget);if(oldWidget.children.length != widget.children.length) {_initNestedInnerControllers();}}void dispose() {widget.controller?.removeListener(_onTabChange);_disposeNestedInnerControllers();super.dispose();}void _onTabChange() {int index = widget.controller!.index;if (index == widget.controller!.animation?.value) {_nestedInnerControllers[index].attachCurrent();}}void _initNestedInnerControllers() {_disposeNestedInnerControllers();List<NestedInnerScrollController> controllers = List.generate(widget.children.length, (index) {return NestedInnerScrollController();});if(mounted) {setState(() {_nestedInnerControllers = controllers;});} else {_nestedInnerControllers = controllers;}}void _disposeNestedInnerControllers() {_nestedInnerControllers.forEach((element) {element.dispose();});}Widget build(BuildContext context) {return TabBarView(controller: widget.controller,physics: widget.physics,dragStartBehavior: widget.dragStartBehavior,viewportFraction: widget.viewportFraction,clipBehavior: widget.clipBehavior,children: List<Widget>.generate(widget.children.length, (index) {return _InheritedInnerScrollController(controller: _nestedInnerControllers[index],child: widget.children[index],);}));}
}class _InheritedInnerScrollController extends InheritedWidget {final ScrollController controller;const _InheritedInnerScrollController({required super.child,required this.controller});bool updateShouldNotify(covariant _InheritedInnerScrollController oldWidget) => controller != oldWidget.controller;}class NestedInnerScrollController extends ScrollController {ScrollController? _inner;NestedInnerScrollController();ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition? oldPosition) {ScrollPosition scrollPosition;ScrollableState? scrollableState = context as ScrollableState;if(scrollableState != null) {_inner = PrimaryScrollController.maybeOf(scrollableState.context);}if(_inner == null) {scrollPosition = super.createScrollPosition(physics, context, oldPosition);} else {scrollPosition = _inner!.createScrollPosition(physics, context, oldPosition);}return scrollPosition;}void attach(ScrollPosition position) {super.attach(position);_inner?.attach(position);}void detach(ScrollPosition position) {_inner?.detach(position);super.detach(position);}void attachCurrent() {if(_inner != null) {while(_inner!.positions.isNotEmpty) {_inner!.detach(_inner!.positions.first);}_inner!.attach(position);}}static ScrollController of(BuildContext context) {final _InheritedInnerScrollController? target = context.dependOnInheritedWidgetOfExactType<_InheritedInnerScrollController>();assert(target != null,'NestedInnerScrollController.of must be called with a context that contains a NestedTabBarView\'s children.',);return target!.controller;}static ScrollController? maybeOf(BuildContext context) {final _InheritedInnerScrollController? target = context.dependOnInheritedWidgetOfExactType<_InheritedInnerScrollController>();return target?.controller;}}

http://www.ppmy.cn/embedded/36763.html

相关文章

HCIP的学习(OSPF总篇)

HCIA的复习 这边可以与我之前写的HCIA博客结合起来一起看&#xff0c;效果更好 HCIA的学习&#xff08;6&#xff09; OSPF状态机 down—关闭-----一旦启动OSPF进程&#xff0c;并发出hello报文&#xff0c;则进入下一个状态init----初始化状态------当收到的hello报文中存在…

视频素材库在哪里找免费手机版?8个可以用手机浏览的素材网

在视觉内容占据主导地位的今天&#xff0c;合适的视频素材可以大大提升项目的吸引力和效果。以下列出的视频素材网站为广告制作者、社交媒体策略师及电影制作人提供了从传统到现代风格的各种视频素材选择&#xff0c;满足不同的创作需求。 1. 蛙学府&#xff08;中国&#xff…

vue2迁移到vue3,v-model的调整

项目从vue2迁移到vue3,v-model不能再使用了,需要如何调整? 下面只提示变化最小的迁移,不赘述vue2和vue3中的常规写法。 vue2迁移到vue3,往往不想去调整之前的代码,以下就使用改动较小的方案进行调整。 In terms of what has changed, at a high level: BREAKING: When…

四级英语翻译随堂笔记

降维表达&#xff1a;中译英&#xff0c;英译英 没有强调主语&#xff0c;没有说明主语&#xff1a;用被动 但如果实在不行&#xff0c;再增添主语 不会就不翻译&#xff0c;不要乱翻译 以xxx为背景&#xff1a;against the backdrop of the xxx eg:against the backdrop of…

DeepSeek发布全新开源大模型,GPT-4级别能力 价格仅百分之一

最新国产开源MoE大模型&#xff0c;刚刚亮相就火了。 DeepSeek-V2性能达GPT-4级别&#xff0c;但开源、可免费商用、API价格仅为GPT-4-Turbo的百分之一。 因此一经发布&#xff0c;立马引发不小讨论。 从公布的性能指标来看&#xff0c;DeepSeek-V2的中文综合能力超越一众开源…

【C 数据结构-动态内存管理】4. 无用单元收集(垃圾回收机制)

文章目录 【 1. 问题描述与解决方法 】【 2. 中断回收机制 】 【 1. 问题描述与解决方法 】 问题描述 动态存储管理的运行机制可以概括为&#xff1a;当用户发出申请空间的请求后&#xff0c;系统向用户分配内存&#xff1b;用户运行结束释放存储空间后&#xff0c;系统回收内…

C++中容器string的简单模拟实现

文章目录 一、使用C文档查看函数功能1. 网站查看C手册a.cplusplus网站b. cppreference网站 2. 搜索string容器&#xff0c;查看容器所拥有的函数3. 了解函数a.构造函数b. 析构函数c. 赋值重载d. begin()、end()e. size()、capacity()、clear()、empty()f. resize()、reserve()g…

05 华三交换机原理

交换机的工作原理(第三十课)-CSDN博客 1 华三交换机原理 交换机是一种网络设备,用于在局域网(LAN)中实现数据帧的转发和过滤。其工作原理基于MAC地址表,它可以学习、过滤和转发帧到正确的端口。以下是交换机的基本工作原理: 1. 学习阶段: - 当设备首次发送数据包时,…