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

}
  • 25
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要让 `ListView` 滚动到指定的位置,可以使用 `ScrollController`,将其传递给 `ListView` 的 `controller` 参数。然后,调用 `animateTo` 方法,传递要滚动到的位置。以下是一个简单的示例代码: ```dart class MyPage extends StatefulWidget { @override _MyPageState createState() => _MyPageState(); } class _MyPageState extends State<MyPage> { final List<String> items = List.generate(100, (index) => "Item $index"); final ScrollController _scrollController = ScrollController(); void _scrollToIndex(int index) { _scrollController.animateTo( index * 50.0, // 50.0 为每个 item 的高度 duration: Duration(milliseconds: 500), curve: Curves.easeInOut, ); } @override Widget build(BuildContext context) { return Scaffold( body: NestedScrollView( headerSliverBuilder: (context, _) { return [ SliverAppBar( title: Text("My Page"), pinned: true, expandedHeight: 200.0, flexibleSpace: FlexibleSpaceBar( background: Image.network( "https://picsum.photos/200/300", fit: BoxFit.cover, ), ), bottom: TabBar( tabs: [ Tab(text: "Tab 1"), Tab(text: "Tab 2"), Tab(text: "Tab 3"), ], ), ), ]; }, body: TabBarView( children: [ ListView.builder( controller: _scrollController, itemCount: items.length, itemBuilder: (context, index) { return ListTile( title: Text(items[index]), ); }, ), Container(color: Colors.green), Container(color: Colors.blue), ], ), ), floatingActionButton: FloatingActionButton( onPressed: () => _scrollToIndex(20), // 滚动到第 20 个 item child: Icon(Icons.arrow_downward), ), ); } } ``` 在这个示例中,我们创建了一个 `MyPage` widget,使用了 `NestedScrollView` 和 `TabBarView`,其中包含了一个 `ListView`。我们创建了一个 `_scrollController`,并将其传递给了 `ListView` 的 `controller` 参数。然后,我们定义了一个 `_scrollToIndex` 方法,用于将 `ListView` 滚动到指定位置,这里以第 20 个 item 为例。最后,我们在页面中添加了一个悬浮按钮,点击它会调用 `_scrollToIndex` 方法,将 `ListView` 滚动到指定位置

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值