1

I have a code that has these main widgets:

  CustomScrollView(
    physics: const AlwaysScrollableScrollPhysics(
      parent: BouncingScrollPhysics(),
    ),
    slivers: [
      SliverFillRemaining(
        hasScrollBody: true,
        child: ListView(
          children: [],
        ),
      ),
    ],
  ),

I have it this way because around the ListView widget I have a Column so that on top of I have a widget that simulates a title.

I chose to work with all of them this way so that when my list has 2-3 items, the entire list and title show on the centre of the screen, and the outer scroll is bouncing with the title.

When the list is longer, what I wanted to accomplish was almost what I got, but I want to know if I'm able to control the scrolling with these rules:

  1. Scroll the list;
  2. If the list has ended (either top or bottom), scroll the CustomScrollView

Edit

Here is a link for you to see what I mean. Test on Chrome mobile view so you can actually see the physics in place.

What I'm asking is that when my list has more items than can fit the screen, I mainly scroll my list, but when it gets to the bottom or the top, it lets my CustomScrollView handle the physics.

More explanations on the code are here.

2 Answers 2

2

I haven't delved that deep into scrolling yet, so i can only tell you what my approach would be. And i hope i understand your wanted behaviour correctly.

Similar to your example snippet use a CustomScrollView with SliverFixedExtentList for the parts of your outer list.

If you now want an inner list in the middle that is scrollable as well, use SliverToBoxAdapter with SizedBox.

And if you want an inner scrollable list at the end that is expanded to the remaining screen space, then use SliverFillRemaining.

To now scroll the outer list if the inner lists reach the scroll end, use a ScrollController with OverscrollNotification.

And then you have inner scrollable lists that scroll the outer list when they reach the end of their own scroll:

Widget _build(BuildContext context) {
  final ScrollController outerController = ScrollController(); // todo: this scroll controller should be created
  // inside of your state object instead!

  return CustomScrollView(
    controller: outerController,
    slivers: <Widget>[
      SliverFixedExtentList(
        itemExtent: 100,
        delegate: SliverChildBuilderDelegate(
          (BuildContext context, int index) => Container(color: Colors.red[(index % 4) * 200 + 200], height: 100),
          childCount: 10,
        ),
      ),
      SliverToBoxAdapter(
        child: SizedBox(
          height: 300,
          child: NotificationListener<OverscrollNotification>(
            child: ListView.builder(
              itemBuilder: (BuildContext context, int index) =>
                  Container(color: Colors.green[(index % 4) * 200 + 200], height: 100),
              itemCount: 10,
            ),
            onNotification: (OverscrollNotification notification) {
              final double newOffset = outerController.offset + notification.overscroll;
              outerController.jumpTo(newOffset);
              return true;
            },
          ),
        ),
      ),
      SliverFillRemaining(
        hasScrollBody: true,
        child: NotificationListener<OverscrollNotification>(
          child: ListView.builder(
            itemBuilder: (BuildContext context, int index) =>
                Container(color: Colors.blue[(index % 4) * 200 + 200], height: 100),
            itemCount: 10,
          ),
          onNotification: (OverscrollNotification notification) {
            final double newOffset = outerController.offset + notification.overscroll;
            if (newOffset < outerController.position.maxScrollExtent &&
                newOffset > outerController.position.minScrollExtent) {
              // todo: this if condition prevents bouncy scrolling which is a bit weird without better physics
              // calculations
              outerController.jumpTo(newOffset);
            }
            return true;
          },
        ),
      ),
    ],
  );
}

Edit: is this getting close to what you want? (Still has a bit buggy bouncy scrolling and not the best physics).

class CustomScroll extends MaterialScrollBehavior {
  @override
  Set<PointerDeviceKind> get dragDevices => {PointerDeviceKind.touch, PointerDeviceKind.mouse};

  @override
  Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
    return child;
  }

  @override
  Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) {
    return child;
  }
}

void main() {
  runApp(MaterialApp(
    scrollBehavior: CustomScroll(),
    home: Scaffold(body: _build()),
  ));
}

extension RandomColorExt on Random {
  Color nextColor() {
    return Color.fromARGB(255, nextInt(256), nextInt(256), nextInt(256));
  }
}

final Random random = Random(DateTime.now().microsecondsSinceEpoch);
final ScrollController outerController = ScrollController();

Widget _build() {
  return LayoutBuilder(
    builder: (BuildContext context, BoxConstraints constraints) {
      return SingleChildScrollView(
        physics: const AlwaysScrollableScrollPhysics(
          parent: BouncingScrollPhysics(),
        ),
        controller: outerController,
        child: SizedBox(
          height: constraints.maxHeight,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Center(
                child: Padding(
                  padding: EdgeInsets.all(32),
                  child: Text('Title'),
                ),
              ),
              Flexible(
                child: NotificationListener<OverscrollNotification>(
                  child: ListView.builder(
                    itemCount: 20,
                    shrinkWrap: true,
                    itemBuilder: (BuildContext context, int index) {
                      return Padding(
                        padding: const EdgeInsets.all(8),
                        child: ColoredBox(
                          color: random.nextColor(),
                          child: const SizedBox(height: 50),
                        ),
                      );
                    },
                  ),
                  onNotification: (OverscrollNotification notification) {
                    final double newOffset = outerController.offset + notification.overscroll;
                    outerController.jumpTo(newOffset);
                    return true;
                  },
                ),
              ),
            ],
          ),
        ),
      );
    },
  );
}

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks for answering! I've updated my question so you can have more details on what I want.
No Problem! I just updated my answer with a new example, let me know if this one is going in the right direction.
Yes, it does 90% of what I want and I think I can live with the fact that when the user scrolls focusing on the title (by mistake or something similar) the entire screen moves even when the list still has room to scroll in that direction. Thanks! I'll wait a day or two before accepting so other people can give more input if they know how to solve that
Nice, i'm glad that it helped! With my current knowledge of scrolling widgets i don't know how, but there will be a way to achieve your wanted behaviour (even if it comes down to writing your own custom RenderObject).
1

You can use sliver_tools package to pin the title only for an amount of scroll. I created an example by modifying this medium tutorial. The example will seem like https://imgur.com/a/W3nND7x

import 'package:flutter/material.dart';
import 'package:sliver_tools/sliver_tools.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center( // center content if shrinkWrap is true
          child: CustomScrollView(
            shrinkWrap: true, // Important for center content when it is smaller that screen height
            physics: const AlwaysScrollableScrollPhysics(
              parent: BouncingScrollPhysics(),
            ),
            slivers: [
              Section( // use custom 'Section' class with title property for semi-pinned header
                title: 'Semi-Pinned header',
                items: List.generate(20, (index) => ListTile(
                  title: Text('Item with pinned header $index'),
                )),
              ),
              Section( // use custom 'Section' class without title property for only content.
                items: List.generate(20, (index) => ListTile(
                  title: Text('Item without any header $index'),
                )),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class Section extends MultiSliver {
  Section({
    Key? key,
    String? title,
    Color headerColor = Colors.blue,
    Color titleColor = Colors.black,
    required List<Widget> items,
  }) : super(
          key: key,
          pushPinnedChildren: true,
          children: [
            if (title != null) SliverPinnedHeader(
              child: ColoredBox(
                color: headerColor,
                child: ListTile(
                  textColor: titleColor,
                  title: Text(title),
                ),
              ),
            ),
            SliverList(
              delegate: SliverChildListDelegate.fixed(items),
            ),
          ],
        );
}

3 Comments

Almost what I wanted! (Or maybe I'm doing it wrong?) The only thing I don't like about this option is that the pinned header doesn't work properly with AlwaysScrollableScrollPhysics(parent: BouncingScrollPhysics()) if the header is transparent. I have only a centred text, not a coloured box in any way, and the way it renders the header differently from the actual list in that physics is what bothers me
I've updated my question so you can have more details on what I want.
I think my answer can not provide your requirements completely. I am glad to see Niko's answer is working.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.