Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
yasinarik opened this issue Nov 30, 2020 · 1 comment

Comments

@yasinarik
Copy link

yasinarik commented Nov 30, 2020

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;
}
@himalaya-nuts
Copy link

Check my PR that fixed this issue: #73

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants