flutterdartscrollable

Flutter PageView with scrollable pages excluding pagination area


My main issue is with the content in the first child of PageView where if you scroll the content vertically you will see that the Text('Hello') is behind the PageIndicator. How do I create the PageView so that is uses a scrollable region that excludes the PageIndicator area?

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

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('PageView Sample')),
        body: const PageViewExample(),
      ),
    );
  }
}

class PageViewExample extends StatefulWidget {
  const PageViewExample({super.key});

  @override
  State<PageViewExample> createState() => _PageViewExampleState();
}

class _PageViewExampleState extends State<PageViewExample>
    with TickerProviderStateMixin {
  late PageController _pageViewController;
  late TabController _tabController;
  int _currentPageIndex = 0;

  @override
  void initState() {
    super.initState();
    _pageViewController = PageController();
    _tabController = TabController(length: 3, vsync: this);
  }

  @override
  void dispose() {
    super.dispose();
    _pageViewController.dispose();
    _tabController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final TextTheme textTheme = Theme.of(context).textTheme;

    return Stack(
      alignment: Alignment.bottomCenter,
      children: <Widget>[
        PageView(
          controller: _pageViewController,
          onPageChanged: _handlePageViewChanged,
          children: <Widget>[
            SingleChildScrollView(
              child: Column(
                children: [
                  SizedBox(
                    height: 800.0,
                    child: Text('First Page', style: textTheme.titleLarge),
                  ),
                  Text('Hello',
                      style: Theme.of(context)
                          .primaryTextTheme
                          .headlineLarge
                          ?.copyWith(color: Colors.black)),
                ],
              ),
            ),
            Center(
              child: Text('Second Page', style: textTheme.titleLarge),
            ),
            Center(
              child: Text('Third Page', style: textTheme.titleLarge),
            ),
          ],
        ),
        PageIndicator(
          tabController: _tabController,
          currentPageIndex: _currentPageIndex,
          onUpdateCurrentPageIndex: _updateCurrentPageIndex,
          isOnDesktopAndWeb: _isOnDesktopAndWeb,
        ),
      ],
    );
  }

  void _handlePageViewChanged(int currentPageIndex) {
    if (!_isOnDesktopAndWeb) {
      return;
    }
    _tabController.index = currentPageIndex;
    setState(() {
      _currentPageIndex = currentPageIndex;
    });
  }

  void _updateCurrentPageIndex(int index) {
    _tabController.index = index;
    _pageViewController.animateToPage(
      index,
      duration: const Duration(milliseconds: 400),
      curve: Curves.easeInOut,
    );
  }

  bool get _isOnDesktopAndWeb {
    if (kIsWeb) {
      return true;
    }
    switch (defaultTargetPlatform) {
      case TargetPlatform.macOS:
      case TargetPlatform.linux:
      case TargetPlatform.windows:
        return true;
      case TargetPlatform.android:
      case TargetPlatform.iOS:
      case TargetPlatform.fuchsia:
        return false;
    }
  }
}

class PageIndicator extends StatelessWidget {
  const PageIndicator({
    super.key,
    required this.tabController,
    required this.currentPageIndex,
    required this.onUpdateCurrentPageIndex,
    required this.isOnDesktopAndWeb,
  });

  final int currentPageIndex;
  final TabController tabController;
  final void Function(int) onUpdateCurrentPageIndex;
  final bool isOnDesktopAndWeb;

  @override
  Widget build(BuildContext context) {
    if (!isOnDesktopAndWeb) {
      return const SizedBox.shrink();
    }
    final ColorScheme colorScheme = Theme.of(context).colorScheme;

    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          IconButton(
            splashRadius: 16.0,
            padding: EdgeInsets.zero,
            onPressed: () {
              if (currentPageIndex == 0) {
                return;
              }
              onUpdateCurrentPageIndex(currentPageIndex - 1);
            },
            icon: const Icon(
              Icons.arrow_left_rounded,
              size: 32.0,
            ),
          ),
          TabPageSelector(
            controller: tabController,
            color: colorScheme.background,
            selectedColor: colorScheme.primary,
          ),
          IconButton(
            splashRadius: 16.0,
            padding: EdgeInsets.zero,
            onPressed: () {
              if (currentPageIndex == 2) {
                return;
              }
              onUpdateCurrentPageIndex(currentPageIndex + 1);
            },
            icon: const Icon(
              Icons.arrow_right_rounded,
              size: 32.0,
            ),
          ),
        ],
      ),
    );
  }
}

screenshot of app on Dartpad


Solution

  • Just like @chunhunghan said.

    You can change the stack with column and wrap your pageview with expanded to make it expand.

    The final code below:

    import 'package:flutter/foundation.dart';
    import 'package:flutter/material.dart';
    
    void main() => runApp(const PageViewExampleApp());
    
    class PageViewExampleApp extends StatelessWidget {
      const PageViewExampleApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            appBar: AppBar(title: const Text('PageView Sample')),
            body: const PageViewExample(),
          ),
        );
      }
    }
    
    class PageViewExample extends StatefulWidget {
      const PageViewExample({super.key});
    
      @override
      State<PageViewExample> createState() => _PageViewExampleState();
    }
    
    class _PageViewExampleState extends State<PageViewExample>
        with TickerProviderStateMixin {
      late PageController _pageViewController;
      late TabController _tabController;
      int _currentPageIndex = 0;
    
      @override
      void initState() {
        super.initState();
        _pageViewController = PageController();
        _tabController = TabController(length: 3, vsync: this);
      }
    
      @override
      void dispose() {
        super.dispose();
        _pageViewController.dispose();
        _tabController.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        final TextTheme textTheme = Theme.of(context).textTheme;
    
        return Column(
          children: <Widget>[
            Expanded(
              child: PageView(
                controller: _pageViewController,
                onPageChanged: _handlePageViewChanged,
                children: <Widget>[
                  SingleChildScrollView(
                    child: Column(
                      children: [
                        SizedBox(
                          height: 800.0,
                          child: Text('First Page', style: textTheme.titleLarge),
                        ),
                        Text('Hello',
                            style: Theme.of(context)
                                .primaryTextTheme
                                .headlineLarge
                                ?.copyWith(color: Colors.black)),
                      ],
                    ),
                  ),
                  Center(
                    child: Text('Second Page', style: textTheme.titleLarge),
                  ),
                  Center(
                    child: Text('Third Page', style: textTheme.titleLarge),
                  ),
                ],
              ),
            ),
            PageIndicator(
              tabController: _tabController,
              currentPageIndex: _currentPageIndex,
              onUpdateCurrentPageIndex: _updateCurrentPageIndex,
              isOnDesktopAndWeb: _isOnDesktopAndWeb,
            ),
          ],
        );
      }
    
      void _handlePageViewChanged(int currentPageIndex) {
        if (!_isOnDesktopAndWeb) {
          return;
        }
        _tabController.index = currentPageIndex;
        setState(() {
          _currentPageIndex = currentPageIndex;
        });
      }
    
      void _updateCurrentPageIndex(int index) {
        _tabController.index = index;
        _pageViewController.animateToPage(
          index,
          duration: const Duration(milliseconds: 400),
          curve: Curves.easeInOut,
        );
      }
    
      bool get _isOnDesktopAndWeb {
        if (kIsWeb) {
          return true;
        }
        switch (defaultTargetPlatform) {
          case TargetPlatform.macOS:
          case TargetPlatform.linux:
          case TargetPlatform.windows:
            return true;
          case TargetPlatform.android:
          case TargetPlatform.iOS:
          case TargetPlatform.fuchsia:
            return false;
        }
      }
    }
    
    class PageIndicator extends StatelessWidget {
      const PageIndicator({
        super.key,
        required this.tabController,
        required this.currentPageIndex,
        required this.onUpdateCurrentPageIndex,
        required this.isOnDesktopAndWeb,
      });
    
      final int currentPageIndex;
      final TabController tabController;
      final void Function(int) onUpdateCurrentPageIndex;
      final bool isOnDesktopAndWeb;
    
      @override
      Widget build(BuildContext context) {
        if (!isOnDesktopAndWeb) {
          return const SizedBox.shrink();
        }
        final ColorScheme colorScheme = Theme.of(context).colorScheme;
    
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              IconButton(
                splashRadius: 16.0,
                padding: EdgeInsets.zero,
                onPressed: () {
                  if (currentPageIndex == 0) {
                    return;
                  }
                  onUpdateCurrentPageIndex(currentPageIndex - 1);
                },
                icon: const Icon(
                  Icons.arrow_left_rounded,
                  size: 32.0,
                ),
              ),
              TabPageSelector(
                controller: tabController,
                color: colorScheme.background,
                selectedColor: colorScheme.primary,
              ),
              IconButton(
                splashRadius: 16.0,
                padding: EdgeInsets.zero,
                onPressed: () {
                  if (currentPageIndex == 2) {
                    return;
                  }
                  onUpdateCurrentPageIndex(currentPageIndex + 1);
                },
                icon: const Icon(
                  Icons.arrow_right_rounded,
                  size: 32.0,
                ),
              ),
            ],
          ),
        );
      }
    }
    

    This is the result:

    wrap w expanded

    Hopefully it can solve your problem, Thanks 😉