extended_nested_scroll_view icon indicating copy to clipboard operation
extended_nested_scroll_view copied to clipboard

[Feature Request] For outer scroller: Enable "AlwaysScrollableScrollPhysics" (or allow using any custom physics)

Open yasinarik opened this issue 3 years ago • 1 comments

Hi! This is a great package already and I am grateful to you for doing all this work. I've seen many issues from you for 2 years solving this problem.

I almost have the desired behavior. If you can modify the package to allow using any custom physics or AlwaysScrollableScrollPhysicsfor outer scroller, my work will be done.

Currently, even though there is a physics parameter for NestedScrollView, changing it doesn't work.

Expected Behavior: (For example the iOS version of the Instagram app --> go to profile screen)

  1. Inner scroller should not bounce (or cannot be over scrolled) on the top edge (leading edge). When scrolling to the top of the page if the inner scroller reaches to minScrollExtent, it should clamp. Currently, I use ClampedScrollPhysics to stop it bounce or be over scrolled. This problem is resolved as you already said.

  2. The outer scroller can be over-scrolled or should bounce on the top edge if there is momentum. Like a pull to refresh mechanism and also I just want it to bounce instead of clamping.

  3. Inner scroller should bounce when reached to the bottom edge. I use my own scroll to load more mechanism by the way.

  4. The scroll friction should be lowered and the scroll detection threshold should be lowered too. This will improve the ease of use. Of course, if custom physics can be applied, I can fine-tune it by myself.

Sample video of the current situation:

Nested Scroll Physics - streamable.com - Sorry, Streamable.com deleted the videos due to prescription

Instagram example:

Instagram Profile Screen Nested Scrolling (Bounces and Can Be Over Scrolled) - Sorry, Streamable.com deleted the videos due to prescription

A Basic Reproducible Code:

import 'package:flutter/material.dart' hide NestedScrollView;
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart' as extended;


class NestedWithTabs extends StatefulWidget {
  @override
  _NestedWithTabsState createState() => _NestedWithTabsState();
}

class _NestedWithTabsState extends State<NestedWithTabs> with TickerProviderStateMixin {
  TabController tabController;
  static List<String> _tabButtonTextList = ["Dogs", "Cats", "Birds"];
  List<Key> _keyList = [];
  static double _tabButtonHeight = 48;
  static double _headerWidgetHeight = 250;

  @override
  void initState() {
    super.initState();

    tabController = TabController(
      length: _tabButtonTextList.length,
      initialIndex: 0,
      vsync: this,
    );

    for (var i = 0; i < _tabButtonTextList.length; i++) {
      _keyList.add(Key(_tabButtonTextList[i] + i.toString()));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: extended.NestedScrollView(
        physics: AlwaysScrollableScrollPhysics(), // THIS DOES NOT WORK?
        pinnedHeaderSliverHeightBuilder: () => _tabButtonHeight,
        headerSliverBuilder: (BuildContext c, bool f) {
          final List<Widget> widgets = <Widget>[];
          widgets.add(
            SliverList(
              delegate: SliverChildListDelegate(
                [
                  Container(
                    color: Colors.red,
                    height: _headerWidgetHeight,
                    child: Center(
                      child: Text("Header Widget"),
                    ),
                  ),
                ],
              ),
            ),
          );
          widgets.add(
            SliverPersistentHeader(
              pinned: true,
              floating: false,
              delegate: CommonSliverPersistentHeaderDelegate(
                Container(
                  height: _tabButtonHeight,
                  child: TabBar(
                    controller: tabController,
                    labelColor: Colors.blue,
                    indicatorColor: Colors.black,
                    indicatorSize: TabBarIndicatorSize.label,
                    indicatorWeight: 2.0,
                    isScrollable: false,
                    unselectedLabelColor: Colors.grey,
                    tabs: _tabButtonTextList.asMap().entries.map((entry) {
                      return Tab(text: entry.value);
                    }).toList(),
                  ),
                ),
                _tabButtonHeight,
              ),
            ),
          );

          return widgets;
        },
        innerScrollPositionKeyBuilder: () => _keyList[tabController.index],
        body: TabBarView(
          controller: tabController,
          children: _tabButtonTextList.asMap().entries.map((entry) {
            int _thisIndex = entry.key;
            return InnerScroller(
              tabKey: _keyList[_thisIndex],
              tabIndex: _thisIndex,
              tabName: _tabButtonTextList[_thisIndex],
            );
          }).toList(),
        ),
      ),
    );
  }
}

/* -------------------------------------------------------------------------- */
/*                    CommonSliverPersistentHeaderDelegate                    */
/* -------------------------------------------------------------------------- */

class CommonSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
  CommonSliverPersistentHeaderDelegate(this.child, this.height);
  final Widget child;
  final double height;

  @override
  double get minExtent => height;

  @override
  double get maxExtent => height;

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return child;
  }

  @override
  bool shouldRebuild(CommonSliverPersistentHeaderDelegate oldDelegate) {
    //print('shouldRebuild---------------');
    return oldDelegate != this;
  }
}

/* -------------------------------------------------------------------------- */
/*                                InnerScroller                               */
/* -------------------------------------------------------------------------- */

class InnerScroller extends StatefulWidget {
  final Key tabKey;
  final int tabIndex;
  final String tabName;
  InnerScroller({
    @required this.tabKey,
    @required this.tabIndex,
    @required this.tabName,
  });

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

class _InnerScrollerState extends State<InnerScroller> with AutomaticKeepAliveClientMixin {
  static List<Color> _colorList = [Colors.indigoAccent, Colors.lime, Colors.orangeAccent];

  @override
  Widget build(BuildContext context) {
    return extended.NestedScrollViewInnerScrollPositionKeyWidget(
      widget.tabKey,
      CustomScrollView(
        key: PageStorageKey(widget.tabKey),
        physics: ClampingScrollPhysics(),
        slivers: <Widget>[
          SliverGrid(
            delegate: SliverChildBuilderDelegate(
              (_, i) {
                return Container(
                  margin: EdgeInsets.all(4),
                  height: 200,
                  color: _colorList[widget.tabIndex],
                  child: Center(
                    child: Text(widget.tabName + " " + i.toString()),
                  ),
                );
              },
              childCount: 3 * 32,
            ),
            gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 3,
            ),
          ),
        ],
      ),
    );
  }

  @override
  bool get wantKeepAlive => true;
}

yasinarik avatar Nov 30 '20 14:11 yasinarik

Check my PR that fixed this issue: https://github.com/fluttercandies/extended_nested_scroll_view/pull/73

himalaya-nuts avatar May 24 '21 11:05 himalaya-nuts