How to fix scrolling of TabBarView in CustomScrollView relative to content size?

78 Views Asked by At

The flutter docs provides a nice example on how to have a floating app bar above a list using CustomScrollView. The code in their example exhibits exactly the behavior I want, that is, for the app bar to only scroll away when there is enough content to scroll. Below is a verbatim copy of the code with the exception of the childCount property of the SliverList set to a small number of items to demonstrate that no scrolling will occur in that case.

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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

  @override
  Widget build(BuildContext context) {
    const title = 'Floating App Bar';

    return MaterialApp(
      title: title,
      home: Scaffold(
        // No appbar provided to the Scaffold, only a body with a
        // CustomScrollView.
        body: CustomScrollView(
          slivers: [
            // Add the app bar to the CustomScrollView.
            const SliverAppBar(
              // Provide a standard title.
              title: Text(title),
              // Allows the user to reveal the app bar if they begin scrolling
              // back up the list of items.
              floating: true,
              // Display a placeholder widget to visualize the shrinking size.
              flexibleSpace: Placeholder(),
              // Make the initial height of the SliverAppBar larger than normal.
              expandedHeight: 200,
            ),
            // Next, create a SliverList
            SliverList(
              // Use a delegate to build items as they're scrolled on screen.
              delegate: SliverChildBuilderDelegate(
                // The builder function returns a ListTile with a title that
                // displays the index of the current item.
                (context, index) => ListTile(title: Text('Item #$index')),
                // Builds 1000 ListTiles
                childCount: 3, //<------------- Adjusted to three items
              ),
            ),
          ],
        ),
      ),
    );
  }
}

I would like to mimic exactly this behavior using a TabBar with a corresponding TabBarView. The problem is that the TabBarView scrolls unnecessarily when the content is small and overflows if its content is large. I've tried using a NestedScrollView following the tutorial in the docs but the problem persists. I have currently resorted to the solution mentioned by the OP at the bottom of their question where they suggest to nest the SliverAppBar within a CustomScrollView. This does not work either. Below is a simplified version of the code which illustrates the issue. Note that the contents of tab 1 and tab 2 scrolls unnecessarily while the contents of tab 3 overflows.

This answer demonstrates exactly what I'm after but does not use a TabBarView (which allows swiping to change tabs).

import 'package:flutter/material.dart';

void main() => runApp(const NestedScrollViewExampleApp());

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(body: SafeArea(child: NestedScrollViewExample())),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 3,
      child: Scaffold(
        body: CustomScrollView(
          slivers: [
            SliverAppBar(
              title: const Text('Floating Nested SliverAppBar'),
              floating: true,
              pinned: false,
              expandedHeight: 200.0,
              forceElevated: false,
              bottom: TabBar(
                  tabs: ['tab1', 'tab2', 'tab3'].map((e) => Text(e)).toList()),
            ),
            const SliverFillRemaining(
              child: TabBarView(
                children: [Text('content1'), Text('content2'), _OverflowContent()],
              ),
            )
          ],
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Column(children: [
      for (int i = 0; i < 50; i++) ...[Text('Item $i')],
    ]);
  }
}

I think that the CustomScrollView needs to somehow be aware of the size of the TabView's content, but I'm not sure if this is at all possible. My goal is to have a TabBarView with dynamically sized content within a scrolling view.

Any help is much appreciated. Thanks.

1

There are 1 best solutions below

2
Daniel Solomon On

Edit Summary:
This is a workaround that will set the scroll property of the NestedScrollView based on the number of items that need to be shown.

I used a NestedScrollView with a TickerProviderStateMixIn
scrollOrNot() determines the scroll physics based on the number of items.
Any list with less than 5 items will not be scrollable.
tabContent() returns a sliverlist for each tab in the TabBarView

import 'package:flutter/material.dart';
void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      
      home:  HomePage());
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});
  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
 late TabController tabController; // TabController used to control both the TabBar and TabBarView
 int selectedIndex=0; // a variable that tracks the tab that is tapped.
 List<int> childCount=[2,3,100]; // a list of the number of items in each TabBarView
 
 @override
 void initState() {
    // TODO: implement initState
    super.initState();
    tabController=TabController(length: 3, vsync: this,
    // I tried setting the animation duration to 0, but 
    // any animation takes time.
    // generally speaking, swiping is slower than tapping. 
    animationDuration: const Duration(microseconds: 0)

    
    ); // initializing the TabController
// setting a listener that will check for changes in the tabs
    tabController.addListener(() {
//setState() if the index stopped changing after a tap event
// this is useful for handling the tap event 
// but it does nothing for the swipe event which is automatically handled
// by the TabBarView
if(!tabController.indexIsChanging){
  setState(() { selectedIndex=tabController.index;});

} });
  
  }

// a method that determines the scroll physics of a list based
// on the number of items it is required to show. 
// any list with less than 5 items will not be allowed to scroll
  scrollOrNot({required int numOfChildren}){
    if(numOfChildren<5){ return const NeverScrollableScrollPhysics();}
    else{return const AlwaysScrollableScrollPhysics();}
  }

  // a method that dynamically creates sliverlists based on the 
  // the number of required items
  tabContent({required int numOfChildren}){
    return Scaffold(body:
  CustomScrollView(
  physics: scrollOrNot(numOfChildren: numOfChildren),
    slivers:[
   SliverList.builder(
    itemCount: numOfChildren,
    itemBuilder: (context, index)=> ListTile(title: Text("item $index"),))
 
  ])
  );
  }

  @override
  Widget build(BuildContext context) {
    return  NestedScrollView(
      // determining the scroll physics based on the number of children
      // childCount is the list from above
      physics: scrollOrNot(numOfChildren: childCount[selectedIndex]),
headerSliverBuilder:(context, innerBoxIsScrolled)=>[
 SliverAppBar(
  expandedHeight: 300,
  floating: true,
  backgroundColor: Colors.white,
  title: 
//Note that the delay in changing the index is due to the animation duration
//I tried to make it 0 milliseconds but swiping is always slower than tapping  
Text("tabControllerIndex: ${tabController.index}"),
  bottom: TabBar(

    controller: tabController,
    indicatorColor: Colors.red,
    tabs: const [Text("Tab 1"),Text("Tab 2"),Text("Tab 3")]),
)
],
body:
 TabBarView(
  controller: tabController,
  children: [
    // creating the sliverlists using the tabContent method
  tabContent(numOfChildren: childCount[0]),
  tabContent(numOfChildren: childCount[1]),
  tabContent(numOfChildren: childCount[2]),
])

);
  }
}