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

news2024/12/26 21:30:03

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

}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深圳车间厂房降温用什么设备好?

环保水空调&#xff08;也被称为水冷空调或蒸发式降温换气机组&#xff09;的特点主要体现在以下几个方面&#xff1a; 节能环保&#xff1a;环保水空调使用水作为冷媒介&#xff0c;相比传统空调的制冷方式&#xff0c;它能在制冷过程中节约更多的能源&#xff0c;减少碳排放…

测评工作室的养号成本,效率,纯净度,便捷性等问题怎么解决?

大家好&#xff0c;我是南哥聊跨境&#xff0c;最近有很多做测评工作室的朋友找到南哥&#xff0c;问我有什么新的测评养号系统可以解决成本&#xff0c;效率&#xff0c;纯净度&#xff0c;便捷性等问题 测评养号系统从最早的模拟器、虚拟机到911、VPS、手机设备等&#xff0…

革新品质检测,质构科技重塑肉类行业新篇章

革新品质检测&#xff0c;质构科技重塑肉类行业新篇章 在现代社会&#xff0c;消费者对食品安全和品质的要求日益提升&#xff0c;特别是在肉类行业。为了满足这一市场需求&#xff0c;质构科技凭借其精准、高效的优势&#xff0c;正逐渐成为肉类品质检测的新星。今天&#xf…

Rust开发工具有哪些?

目录 一、JetBrains公司的RustRover​编辑 二、微软公司的Visual Studio Code 三、Rust编译工具 一、JetBrains公司的RustRover RustRover是由JetBrains开发的一款专为Rust开发量身定制的新兴IDE&#xff0c;目前还处于早期访问阶段。它支持Rust、Cargo、TOML、Web和数据库等…

Leecode42:接雨水

第一反应是按照高低这个思路来求解&#xff0c;因为可以把盛雨水的容器想成是从左往右的&#xff0c;遇到一个沟就存一点雨水。 这个思路 看了下题解&#xff0c;发现自己的思路其实没问题&#xff0c;确实是按照最高最低来求&#xff0c;但是这个地方太复杂了求的&#xff0c…

计算机毕业设计 | springboot+vue小米商城 购物网站管理系统(源码+论文+讲解视频)

1&#xff0c;项目背景 国家大力推进信息化建设的大背景下&#xff0c;城市网络基础设施和信息化应用水平得到了极大的提高和提高。特别是在经济发达的沿海地区&#xff0c;商业和服务业也比较发达&#xff0c;公众接受新事物的能力和消费水平也比较高。开展商贸流通产业的信息…

API接口调用失败的常见原因?如何进行排查和处理?

API接口调用失败的常见原因有以下几种&#xff1a; 1. 无效的请求参数&#xff1a;可能是由于请求参数缺失、格式错误或者不符合接口要求导致的。解决方法是检查请求参数是否正确&#xff0c;并确保按照接口文档提供正确的参数。 2. 接口权限不足&#xff1a;有些接口需要特定…

互联网产品为什么要搭建会员体系?

李诞曾经说过一句话&#xff1a;每个人都可以讲5分钟脱口秀。这句话换到会员体系里面同样适用&#xff0c;每个人都能聊点会员体系相关的东西。 比如会员体系属于用户运营的范畴&#xff0c;比如怎样用户分层&#xff0c;比如用户标签及CDP、会员积分、会员等级、会员权益和付…

【go从入门到精通】go命令使用

作者简介: 高科,先后在 IBM PlatformComputing从事网格计算,淘米网,网易从事游戏服务器开发,拥有丰富的C++,go等语言开发经验,mysql,mongo,redis等数据库,设计模式和网络库开发经验,对战棋类,回合制,moba类页游,手游有丰富的架构设计和开发经验。 (谢谢…

用一个故事告诉你协程到底是什么

神秘使者 “久闻Java语言跨越平台&#xff0c;框架众多&#xff0c;不过二十年功夫&#xff0c;就已晋升天下第一编程语言&#xff0c;今日一见&#xff0c;果然名不虚传呐&#xff01;” “使者先生您过奖了&#xff0c;咱们快些走&#xff0c;国王陛下已经等候多时了” 今…

自动化机器学习——贝叶斯优化

自动化机器学习——贝叶斯优化 贝叶斯优化是一种通过贝叶斯公式推断出目标函数的后验概率分布&#xff0c;从而在优化过程中不断地利用已有信息来寻找最优解的方法。在贝叶斯优化中&#xff0c;有两个关键步骤&#xff1a;统一建模和获得函数的优化。 1. 统一建模 在贝叶斯优…

孩子用什么样的灯对眼睛没有伤害?分享多款满分护眼台灯

为人父母以后&#xff0c;深感压力山大。如今不仅要抓孩子的学习&#xff0c;还得时刻关注孩子的身心健康&#xff0c;尤其是视力问题。现在不少学生都存在近视的现象&#xff0c;而导致这一现象的主要原因&#xff0c;除了平时的学业压力过大以外&#xff0c;夜晚学习的光线也…

美港通正规炒股市场恒生科指半日跌近2% 大型科技股集体下行

查查配5月7日电 7日,港股主要股指回调。截至午盘,恒生指数跌0.85%,恒生科技指数跌1.98%。 美港通证券以其专业的服务和较低的管理费用在市场中受到不少关注。该平台提供了实盘交易、止盈止损、仓位控制等功能,旨在为投资者提供更为全面的投资体验。 来源:Wind 盘面上,零售、软…

用C#打造精美系统托盘消息提醒,让你的应用更具魅力

使用效果&#xff1a; 代码&#xff1a; #region 消息框变量private Timer fadeTimer; // 定义计时器private int fadeSpeed 2;//淡出速度private NotifyIcon notifyIcon;//气泡通知private int opacityLevel 10;//不透明度public enum NotificationType{Error,//错误Warning…

Golang | Leetcode Golang题解之第76题最小覆盖子串

题目&#xff1a; 题解&#xff1a; func minWindow(s string, t string) string {ori, cnt : map[byte]int{}, map[byte]int{}for i : 0; i < len(t); i {ori[t[i]]}sLen : len(s)len : math.MaxInt32ansL, ansR : -1, -1check : func() bool {for k, v : range ori {if c…

Linux网络编程(三)IO复用一 select系统调用

I/O复用使得程序能同时监听多个文件描述符。在以下场景中需要使用到IO复用技术&#xff1a; 客户端程序要同时处理多个socket&#xff0c;非阻塞connect技术客户端程序要同时处理用户输入和网络连接&#xff0c;聊天室程序TCP服务器要同时处理监听socket和连接socket服务器要同…

美国站群服务器的CN2线路在国际互联网通信中的优势?

美国站群服务器的CN2线路在国际互联网通信中的优势? CN2线路&#xff0c;或称中国电信国际二类线路&#xff0c;是中国电信在全球范围内建设的高速骨干网络。这条线路通过海底光缆系统将中国与全球连接起来&#xff0c;为用户提供高速、低延迟的网络服务。CN2线路在国际互联网…

抖音小店是什么?为什么要去做呢?这几点原因告诉你真相!

大家好&#xff0c;我是电商小V 抖音小店就是抖音平台进军电商行业的踏板&#xff0c;也是抖音内的电商购物业务&#xff0c;咱们就可以理解成可以在抖音平台上面卖货&#xff0c;和淘宝&#xff0c;多多店铺&#xff0c;线下超市都是一个性质的&#xff0c;但是运营的模式不同…

C++ | Leetcode C++题解之第75题颜色分类

题目&#xff1a; 题解&#xff1a; class Solution { public:void sortColors(vector<int>& nums) {int n nums.size();int p0 0, p2 n - 1;for (int i 0; i < p2; i) {while (i < p2 && nums[i] 2) {swap(nums[i], nums[p2]);--p2;}if (nums[i…

AI助力制造行业探索创新路径

近期&#xff0c;著名科技作家凯文凯利&#xff08;K.K.&#xff09;来到中国&#xff0c;发表了一场演讲,给广大听众带来了深刻的启示。他在演讲中强调了人工智能&#xff08;AI&#xff09;对全球经济的重大影响&#xff0c;并提出了AI发展的多个观点&#xff1a; AI的多样性…