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

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

一.案发现场

在这里插入图片描述

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

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

    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方法中完成的:

    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滚动相同的位移。

    _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的源码:

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,虽然暂时没什么卵用😄,但是别着急。

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的实例

    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的选中状态。

    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使用

    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;
    
    }
    

    使用的时候

    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。

    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();
        }
      }
    
      ...
    }
    

    在这里插入图片描述

搞定。

三.完整代码
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;

}
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;
  }

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/611138.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Nginx rewrite项目练习

Nginx rewrite练习 1、访问ip/xcz&#xff0c;返回400状态码&#xff0c;要求用rewrite匹配/xcz a、访问/xcz返回400 b、访问/hello时正常访问xcz.html页面server {listen 192.168.99.137:80;server_name 192.168.99.137;charset utf-8;root /var/www/html;location / {root …

TDN: Temporal Difference Networks for Efficient Action Recognition 论文阅读

TDN: Temporal Difference Networks for Efficient Action Recognition 论文阅读 Abstract1. Introduction2. Related work3. Temporal Difference Networks3.1. Overview3.2. Short-term TDM3.3. Long-term TDM3.4. Exemplar: TDN-ResNet 4. ExperimentsAblation studiesCompa…

智能创作时代:AI引领下的内容生产革命与效率提升

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

部署xwiki服务需要配置 hibernate.cfg.xml如何配置?

1. 定位 hibernate.cfg.xml 文件 首先&#xff0c;确保您可以在 Tomcat 的 XWiki 部署目录中找到 hibernate.cfg.xml 文件&#xff1a; cd /opt/tomcat/latest/webapps/xwiki/WEB-INF ls -l hibernate.cfg.xml如果文件存在&#xff0c;您可以继续编辑它。如果不存在&#xff…

梅兰日兰NSJ400N断路器NSJ400N可议价

梅兰日兰 NSJ400N 3 极 400 安培 600 伏交流电 紧凑型断路器 制造商的原始标签 脱扣单元&#xff1a;LS 功能 –&#xff08;长时间和短时间&#xff09; 负载侧凸耳 中断额定值&#xff1a;65kA 240 Vac 35kA 480 伏交流电压 18kA 600 伏交流电压 &#xff08;外观可能与照…

中国地面基本气象逐小时数据获取方式

环境气象数据服务平台提供了全国大约2100个点位&#xff0c;2023年1月1日至今的小时级数据。包括气温、气压、湿度、风、降水等要素。 数据基于ECMWF ERA5-Land Hourly陆面再分析资料和中国地面基本气象观测逐三小时数据&#xff0c;使用机器学习模型加工所得&#xff0c;对比…

【17-Ⅱ】Head First Java 学习笔记

HeadFirst Java 本人有C语言基础&#xff0c;通过阅读Java廖雪峰网站&#xff0c;简单速成了java&#xff0c;但对其中一些入门概念有所疏漏&#xff0c;阅读本书以弥补。 第一章 Java入门 第二章 面向对象 第三章 变量 第四章 方法操作实例变量 第五章 程序实战 第六章 Java…

一文彻底读懂信息安全等级保护:包含等保标准、等保概念、等保对象、等保流程及等保方案(附:等保相关标准文档)

1. 什么是等级保护&#xff1f; 1.1. 概念 信息安全等级保护是指根据我国《信息安全等级保护管理办法》的规定&#xff0c;对各类信息系统按照其重要程度和保密需求进行分级&#xff0c;并制定相应的技术和管理措施&#xff0c;确保信息系统的安全性、完整性、可用性。根据等…

[C++][数据结构]哈希2:开散列/哈希桶的介绍和简单实现

前言 接着上一篇文章&#xff0c;我们知道了闭散列的弊端是空间利用率比较低&#xff0c;希望今天学习的开散列可以帮我们解决这个问题 引入 开散列法又叫链地址法(开链法)&#xff0c;首先对关键码集合用散列函数计算散列地址**&#xff0c;具有相同地址的关键码归于同一子…

数据库表自增主键超过代码Integer长度问题

数据库自增主键是 int(10) unsigned类型的字段&#xff0c;int(M) 中 M指示最大显示宽度&#xff0c;不代表存储长度&#xff0c;实际int(1)也是可以存储21.47亿长度的数字&#xff0c;如果是无符号类型的&#xff0c;那么可以从0~42.94亿。 我们的表主键自增到21.47亿后&#…

英语学习笔记3——Sorry, sir.

Sorry, sir. 对不起&#xff0c;先生。 词汇 Vocabulary umbrella n. 伞&#xff0c;保护伞 注意读音 [ʌm’brelə] 英国人离不开雨伞。 please 请 特殊用法&#xff1a;让路&#xff08;升调&#xff09;      用餐礼仪&#xff08;平调&#xff09;      求求你…

大数据信用和征信报告的区别和联系,一定不要搞混了!

在当今数据驱动的社会&#xff0c;大数据的应用已经深入到各个领域。其中&#xff0c;大数据信用和征信报告成为金融、经济等领域中两个重要的概念。那么&#xff0c;大数据信用和征信报告有什么区别和联系呢? 一、定义与区别 1、大数据信用 大数据信用是指利用大数据技术&…

鸿蒙OpenHarmony技术:【Docker编译环境】

Docker环境介绍 OpenHarmony为开发者提供了两种Docker环境&#xff0c;以帮助开发者快速完成复杂的开发环境准备工作。两种Docker环境及适用场景如下&#xff1a; 独立Docker环境&#xff1a;适用于直接基于Ubuntu、Windows操作系统平台进行版本编译的场景。基于HPM的Docker环…

数学:人工智能领域的基石与灵魂

在科技日新月异的今天&#xff0c;人工智能&#xff08;AI&#xff09;已经渗透到了我们生活的方方面面&#xff0c;从智能家居、智能医疗到自动驾驶、智能客服&#xff0c;AI无处不在。然而&#xff0c;当我们赞叹于AI的神奇时&#xff0c;却往往忽视了其背后的推动力——数学…

2024.5.10

TCP服务器端 #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);//设置窗口大小和窗口大小固定this->resize(727,879);this->setFixedSize(727,879);//创建…

泰尔指数和泰尔指数模型:代码、案例及复现

泰尔指数模型是衡量个人或地区收入差距的重要工具。参考朱红根&#xff08;2023年&#xff09;老师的方法&#xff0c;《农业经济问题》使用泰尔指数分析了中国不同地区数字乡村发展水平的差异。该资料包括了Stata全流程代码、案例数据、参考文献&#xff0c;并提供了Excel计算…

解决mybatis的配置文件没代码提示的问题

1.将org.apache.ibatis.builder.xml包里的两个dtd文件复制出来&#xff0c;jar包里复制 2.复制dtd的url地址&#xff1a; http://mybatis.org/dtd/mybatis-3-mapper.dtd 一样的做法&#xff01; 3.关闭两个配置文件&#xff0c;重新打开&#xff0c;就可以有代码提示了&…

【Linux】Linux——Centos7安装Tomcat

1.下载Tomcat 安装包 官网地址&#xff1a;Apache Tomcat - Apache Tomcat 9 Software Downloadshttps://tomcat.apache.org/download-90.cgi 2.将下载的安装包上传到 Xftp 上&#xff0c;我是直接放到 usr 下了 3.将安装包解压到 /usr/local/ tar -zxvf apache-tomcat-9.0.8…

Java入门——类和对象(上)

经读者反映与笔者考虑&#xff0c;近期以及往后内容更新将主要以java为主&#xff0c;望读者周知、见谅。 类与对象是什么&#xff1f; C语言是面向过程的&#xff0c;关注的是过程&#xff0c;分析出求解问题的步骤&#xff0c;通过函数调用逐步解决问题。 JAVA是基于面向对…

c++:(map和set的底层简单版本,红黑树和AVL树的基础) 二叉搜索树(BST)底层和模拟实现

文章目录 二叉搜索树的概念二叉搜索树的操作二叉搜索树的查找find 二叉搜索树的模拟实现构造节点insertfinderase(细节巨多,面试可能会考)a.叶子节点b.有一个孩子左孩子右孩子 c.有两个孩子注意: erase代码 中序遍历 二叉搜索树的应用k模型k模型模拟实现的总代码 k-value模型k-…