fluttercandies / extended_nested_scroll_view

extended nested scroll view to fix following issues. 1.pinned sliver header issue 2.inner scrollables in tabview sync issue 3.pull to refresh is not work. 4.do without ScrollController in NestedScrollView's body
MIT License
591 stars 117 forks source link

在BottomNavigationBar的PageView上使用,ExtendedNestedScrollView的body中TabBarView的不满屏的情况下,无法顺畅滑动 #91

Closed EchoPuda closed 2 years ago

EchoPuda commented 2 years ago

使用底部导航栏BottomNavigationBar,在其PageView中使用ExtendedNestedScrollView(), 其中headerSliverBuilder中使用了固定的SliverAppBar,TabBar,和中间的使用SliverToBoxAdapter展示的信息。 在body中使用的普通的滚动布局,如GridView

然后在GridView中内容较少,或无的情况下,向上滑动折叠不连贯,不顺畅,需要两次拖拽才能滑至最顶处,中间的卡住时,向上向下都无法继续滑动,得松手重新触摸。 经对比,官方的滑动不会卡住。在页面中直接使用也不会有这问题。GridView的内容超过一屏左右也不会有问题。 希望能有解决的方法

BottomNavigationBar 为正常的使用方式:

   return Scaffold(
      body: PageView.builder(
        itemBuilder: (context, index) => pages[index],
        controller: pageController,
        physics: NeverScrollableScrollPhysics(),
      ),
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.fixed,
        selectedItemColor: Colors.black,
        unselectedItemColor: Colors.grey,
        selectedFontSize: 10.0,
        unselectedFontSize: 10.0,
        items: <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.home_rounded),
            activeIcon: Icon(Icons.home_rounded),
            label: "Home",
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.people),
            activeIcon: Icon(Icons.people),
            label: "account",
          ),
        ],
        currentIndex: _selectedIndex,
        onTap: _onItemTapped,
      ),
    );

ExtendedNestedScrollView:

class TestExtendedNestedScrollView extends StatefulWidget {
  const TestExtendedNestedScrollView({Key? key}) : super(key: key);

  @override
  _TestExtendedNestedScrollViewState createState() => _TestExtendedNestedScrollViewState();
}

class _TestExtendedNestedScrollViewState extends State<TestExtendedNestedScrollView> {

  Widget build(BuildContext context) {
    final List<String> _tabs = <String>['Tab 1', 'Tab 2'];
    return DefaultTabController(
      length: _tabs.length, // This is the number of tabs.
      child: Scaffold(
        body: GlowNotificationWidget(
          ExtendedNestedScrollView(
            onlyOneScrollInBody: true,
            pinnedHeaderSliverHeightBuilder: () {
              double statusBarHeight =
                  MediaQueryData.fromWindow(window).padding.top;
              return appBarHeight + 50 + statusBarHeight;
            },
            headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
              // These are the slivers that show up in the "outer" scroll view.
              return <Widget>[

                SliverAppBar(
                  title: Text("Test"),
                  automaticallyImplyLeading: false,
                  expandedHeight: 200,
                  pinned: true,
                ),

                SliverToBoxAdapter(
                  child: Column(
                    children: [
                      ListTile(
                        title: Text('Order'),
                      ),
                      ListTile(
                        title: Text('Order'),
                      ),
                      ListTile(
                        title: Text('Order'),
                      ),
                      ListTile(
                        title: Text('Order'),
                      ),
                    ],
                  ),
                ),

                SliverPersistentHeader(
                  pinned: true,
                  delegate: NRSliverHeaderDelegate(
                    backgroundColor: Colors.white,
                    islucency: false,
                    child: PreferredSize(
                      preferredSize: Size(double.maxFinite, 50),
                      child: TabBar(
                        padding: EdgeInsets.zero,
                        tabs: [
                          Tab(
                            text: "Test",
                          ),
                          Tab(
                            text: "Tessssssssst",
                          )
                        ],
                        isScrollable: true,
                        indicatorColor: Colors.black,
                        indicatorSize: TabBarIndicatorSize.tab,
                        labelColor: Colors.black,
                        labelStyle: TextStyle(
                          fontSize: 16,
                          fontWeight: FontWeight.w600,
                          color: Colors.black,
                        ),
                        unselectedLabelColor: Colors.grey[100],
                        labelPadding: EdgeInsets.only(left: 20, right: 15),
                        onTap: (index) {

                        },
                      ),
                    ),
                  ),
                ),
              ];
            },
            body: TabBarView(
              // These are the contents of the tab views, below the tabs.
              children: [
                TestTabOne(),
                TestTabTwo(),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class NRSliverHeaderDelegate extends SliverPersistentHeaderDelegate {
  PreferredSize child; //传入preferredsize组件,因为此组件需要固定高度
  bool islucency; //入参 是否更加滑动变化透明度,true,false
  Color backgroundColor; //需要设置的背景色
  NRSliverHeaderDelegate(
      {required this.islucency,
        required this.child,
        required this.backgroundColor});

  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    double mainHeight = maxExtent - shrinkOffset; //动态获取滑动剩余高度
    return Column(
      children: [
        Container(
          height: 49,
          width: MediaQuery.of(context).size.width,
          color: backgroundColor,
          child: Opacity(
              opacity: islucency == true && mainHeight != maxExtent
                  ? ((mainHeight / maxExtent) * 0.5).clamp(0, 1)
                  : 1, //根据滑动高度隐藏显示
              child: child),
        ),
        Divider(
          height: 1,
          color: Colors.grey[200],
        )
      ],
    );
  }

  @override
  double get maxExtent => this.child.preferredSize.height;

  @override
  double get minExtent => this.child.preferredSize.height;

  @override
  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
    return false;
  }
}

GridView:

class TestTabTwo extends StatefulWidget {
  const TestTabTwo({Key? key}) : super(key: key);

  @override
  _TestTabTwoState createState() => _TestTabTwoState();
}

class _TestTabTwoState extends State<TestTabTwo> with AutomaticKeepAliveClientMixin {
  @override
  Widget build(BuildContext context) {
    super.build(context);
    return GridView.builder(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        mainAxisSpacing: 15,
        crossAxisSpacing: 15,
        childAspectRatio: 0.5,
      ),
      itemCount: 0,
      itemBuilder: (context, index) {
        return Container(
          color: Colors.green,
          width: double.maxFinite,
          height: double.maxFinite,
        );
      },
    );
  }

  @override
  bool get wantKeepAlive => true;
}
skylerpfli commented 2 years ago

试了下这样改就好了,参照作者自己的文章:https://juejin.cn/post/6997202342655311879

然后改库,在_ExtendedNestedScrollPosition的applyContentDimensions方法,改为:

  @override
  bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
    if (debugLabel == 'outer' &&
        coordinator.pinnedHeaderSliverHeightBuilder != null) {
      maxScrollExtent =
          maxScrollExtent - coordinator.pinnedHeaderSliverHeightBuilder!();
      maxScrollExtent = math.max(0.0, maxScrollExtent);
    }

    ///新增部分
    if (debugLabel == 'inner' && coordinator.pinnedHeaderSliverHeightBuilder != null) {
      maxScrollExtent = math.max(maxScrollExtent, 0.1);
    }

    return super.applyContentDimensions(minScrollExtent, maxScrollExtent);
  }

修改可以使用Flutter Aop的方案:https://juejin.cn/post/7036352267389239303

EchoPuda commented 2 years ago

我目前是通过修改 updateCanDrag。观察源码发现控制是否可拖动的决定因素是updateCanDrag。 由于不满屏的maxScrollExtent始终为0,导致inter的updateCanDrag为0,所以在outer的updateCanDrag滚动完毕后,inner不会有滚动的区域,而在上面的情况中,outer的updateCanDrag是小于理应滚动的区域的(因为底部导航导致整体向上的padding,导致max-min实际少于应该滚动的范围)。

@override
  void updateCanDrag({_NestedScrollPosition? position}) {
    double maxInnerExtent = 0.0;

    if (onlyOneScrollInBody &&
        position != null &&
        position.debugLabel == 'inner') {
      if (position.haveDimensions) {
        maxInnerExtent = math.max(
          maxInnerExtent,
          position.maxScrollExtent - position.minScrollExtent,
        );
        position.updateCanDrag(maxInnerExtent);
      }
    }
    if (!_outerPosition!.haveDimensions) {
      return;
    }

    for (final _NestedScrollPosition position in _innerPositions) {
      if (!position.haveDimensions) {
        return;
      }
      maxInnerExtent = math.max(
        maxInnerExtent,
        position.maxScrollExtent - position.minScrollExtent,
      );
    }
    _outerPosition!.updateCanDrag(maxInnerExtent);
  }
}

要达到完整折叠的效果,inner.updateCanDrag + outer.updateCanDrag 应该要大于等于inner的顶部到折叠闭合tab的距离,不然就会卡顿,需要重新识别Drag,也就是第二次拖动才能拖到顶部。

所以我主要是通过修改maxInnerExtent的默认值来实现。使其大于等于顶部折叠tab到屏幕底部的距离(不管底部有什么padding,大于就好)

@override
  void updateCanDrag({_NestedScrollPosition? position}) {
    double maxInnerExtent = 0;

    if (onlyOneScrollInBody &&
        position != null &&
        position.debugLabel == 'inner') {
      if (position.haveDimensions) {
        double pinnedHeader = pinnedHeaderSliverHeightBuilder?.call() ?? 0;
        maxInnerExtent = MediaQueryData.fromWindow(window).size.height - pinnedHeader;
        maxInnerExtent = math.max(
          maxInnerExtent,
          position.maxScrollExtent - position.minScrollExtent,
        );
        position.updateCanDrag(maxInnerExtent);
      }
    }
    if (!outerPosition!.haveDimensions) {
      return;
    }

    for (final _NestedScrollPosition position in innerPositions) {
      if (!position.haveDimensions) {
        return;
      }
      maxInnerExtent = math.max(
        maxInnerExtent,
        position.maxScrollExtent - position.minScrollExtent,
      );
    }
    outerPosition!.updateCanDrag(maxInnerExtent);
  }

最终就能流畅的滑动折叠展开了。

我会关闭这个issue,但我觉得这个作者是可以考虑优化下这部分的判断的。

lgGuo commented 2 years ago

试了下这样改就好了,参照作者自己的文章:https://juejin.cn/post/6997202342655311879

然后改库,在_ExtendedNestedScrollPosition的applyContentDimensions方法,改为:

  @override
  bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
    if (debugLabel == 'outer' &&
        coordinator.pinnedHeaderSliverHeightBuilder != null) {
      maxScrollExtent =
          maxScrollExtent - coordinator.pinnedHeaderSliverHeightBuilder!();
      maxScrollExtent = math.max(0.0, maxScrollExtent);
    }

    ///新增部分
    if (debugLabel == 'inner' && coordinator.pinnedHeaderSliverHeightBuilder != null) {
      maxScrollExtent = math.max(maxScrollExtent, 0.1);
    }

    return super.applyContentDimensions(minScrollExtent, maxScrollExtent);
  }

修改可以使用Flutter Aop的方案:https://juejin.cn/post/7036352267389239303

兄弟你能提下代码让作者去合并吗,作者貌似没发现

zmtzawqlp commented 2 years ago

试了下这样改就好了,参照作者自己的文章:https://juejin.cn/post/6997202342655311879 然后改库,在_ExtendedNestedScrollPosition的applyContentDimensions方法,改为:

  @override
  bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
    if (debugLabel == 'outer' &&
        coordinator.pinnedHeaderSliverHeightBuilder != null) {
      maxScrollExtent =
          maxScrollExtent - coordinator.pinnedHeaderSliverHeightBuilder!();
      maxScrollExtent = math.max(0.0, maxScrollExtent);
    }

    ///新增部分
    if (debugLabel == 'inner' && coordinator.pinnedHeaderSliverHeightBuilder != null) {
      maxScrollExtent = math.max(maxScrollExtent, 0.1);
    }

    return super.applyContentDimensions(minScrollExtent, maxScrollExtent);
  }

修改可以使用Flutter Aop的方案:https://juejin.cn/post/7036352267389239303

兄弟你能提下代码让作者去合并吗,作者貌似没发现

如果你觉得这个方案有效。你可以fork 自己做修改。在没有准确的原理表明或者官方验证之前,不打算merge 这部分代码

EchoPuda commented 2 years ago

@zmtzawqlp 目前就是自己fork了修改。可以考虑检查下吧,我的修改主要是在业务方面去解决,源头没有时间去研究。