Search code examples
flutterdartgridviewscrollscrollview

Flutter : Horizontally and Vertically Scrolling Time Table with two sided header


In my flutter application I want to create a timetable widget as below which will scroll horizontally and vertically with corresponding heading. The timetable should have 'Day' as horizontal heading and 'Period' as vertical heading. During horizontal scrolling the 'Period' header should freeze and horizontal 'Day' header should scroll with data. Similarly, during vertical scrolling the 'Day' header should freeze and vertical 'Period' header should scroll with data. How can I achieve a widget like that.Please help..

In Android we can obtain the above type of scrolling by extending HorizontalScrollView & VerticalScrollView.

Original Scrolling horizontally Scrolling Vertically


Solution

  • Scrolling widgets will create a default scroll controller (ScrollController class) if none is provided. A scroll controller creates a ScrollPosition to manage the state specific to an individual Scrollable widget.

    To link our scroll controllers we’ll use linked_scroll_controller, a scroll controller that allows two or more scroll views to be in sync.

    import 'package:flutter/material.dart';
    import 'package:linked_scroll_controller/linked_scroll_controller.dart';
    
    class ScrollDemo extends StatefulWidget {
    @override
    _ScrollDemoState createState() => _ScrollDemoState();
    }
    
    class _ScrollDemoState extends State<ScrollDemo> {
    final List<String> colEntries = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split('');
    final List<String> rowEntries =
        Iterable<int>.generate(15).map((e) => e.toString()).toList();
    
    late LinkedScrollControllerGroup _horizontalControllersGroup;
    late ScrollController _horizontalController1;
    late ScrollController _horizontalController2;
    
    late LinkedScrollControllerGroup _verticalControllersGroup;
    late ScrollController _verticalController1;
    late ScrollController _verticalController2;
    
    @override
    void initState() {
        super.initState();
        _horizontalControllersGroup = LinkedScrollControllerGroup();
        _horizontalController1 = _horizontalControllersGroup.addAndGet();
        _horizontalController2 = _horizontalControllersGroup.addAndGet();
        _verticalControllersGroup = LinkedScrollControllerGroup();
        _verticalController1 = _verticalControllersGroup.addAndGet();
        _verticalController2 = _verticalControllersGroup.addAndGet();
    }
    
    @override
    void dispose() {
        _horizontalController1.dispose();
        _horizontalController2.dispose();
        _verticalController1.dispose();
        _verticalController2.dispose();
        super.dispose();
    }
    
    @override
    Widget build(BuildContext context) {
        return Scaffold(
        appBar: AppBar(
            title: const Text('sync scroll demo'),
        ),
        body: SafeArea(
            child: Padding(
            padding: const EdgeInsets.all(20),
            child: Column(
                children: <Widget>[
                Row(
                    children: <Widget>[
                    Container(
                        width: 75,
                        height: 75,
                        color: Colors.grey[200],
                    ),
                    const SizedBox(width: 10),
                    Container(
                        height: 75,
                        width: 400,
                        color: Colors.blue[100],
                        child: SingleChildScrollView(
                        scrollDirection: Axis.horizontal,
                        controller: _horizontalController2,
                        child: HeaderContainer(rowEntries: rowEntries),
                        ),
                    )
                    ],
                ),
                const SizedBox(height: 10),
                Row(
                    children: <Widget>[
                    Container(
                        width: 75,
                        height: 400,
                        color: Colors.blue[100],
                        child: SingleChildScrollView(
                        controller: _verticalController2,
                        child: ColumnContainer(
                            colEntries: colEntries,
                        ),
                        ),
                    ),
                    const SizedBox(width: 10),
                    SizedBox(
                        width: 400,
                        height: 400,
                        child: SingleChildScrollView(
                        controller: _verticalController1,
                        child: SingleChildScrollView(
                            scrollDirection: Axis.horizontal,
                            controller: _horizontalController1,
                            child: BodyContainer(
                            rowEntries: rowEntries,
                            colEntries: colEntries,
                            ),
                        ),
                        ),
                    )
                    ],
                ),
                ],
            ),
            ),
        ),
        );
    }
    }
    
    class ColumnContainer extends StatelessWidget {
    final List<String> colEntries;
    const ColumnContainer({
        Key? key,
        required this.colEntries,
    }) : super(key: key);
    
    @override
    Widget build(BuildContext context) {
        int numberOfRows = colEntries.length;
        return Column(
        children: List.generate(
            numberOfRows,
            (i) {
            return Container(
                height: 75,
                width: 75,
                decoration: BoxDecoration(border: Border.all(color: Colors.white)),
                child: Center(child: Text(colEntries[i])),
            );
            },
        ),
        );
    }
    }
    
    class HeaderContainer extends StatelessWidget {
    final List<String> rowEntries;
    const HeaderContainer({
        Key? key,
        required this.rowEntries,
    }) : super(key: key);
    
    @override
    Widget build(BuildContext context) {
        int _numberOfColumns = rowEntries.length;
        return Row(
        children: List.generate(
            _numberOfColumns,
            (i) {
            return Container(
                height: 75,
                width: 75,
                decoration: BoxDecoration(border: Border.all(color: Colors.white)),
                child: Center(child: Text(rowEntries[i])),
            );
            },
        ),
        );
    }
    }
    
    class BodyContainer extends StatelessWidget {
    final List<String> colEntries;
    final List<String> rowEntries;
    const BodyContainer({
        Key? key,
        required this.colEntries,
        required this.rowEntries,
    }) : super(key: key);
    
    @override
    Widget build(BuildContext context) {
        int _numberOfColumns = rowEntries.length;
        int _numberOfLines = colEntries.length;
        return Column(
        children: List.generate(_numberOfLines, (y) {
            return Row(
            children: List.generate(_numberOfColumns, (x) {
                return TableCell(item: "${colEntries[y]}${rowEntries[x]}");
            }),
            );
        }),
        );
    }
    }
    
    class TableCell extends StatelessWidget {
    final String item;
    const TableCell({
        Key? key,
        required this.item,
    }) : super(key: key);
    
    @override
    Widget build(BuildContext context) {
        return Container(
        height: 75,
        width: 75,
        decoration: BoxDecoration(border: Border.all(color: Colors.grey)),
        child: Center(child: Text(item)),
        );
    }
    }
    

    I found these solutions from these 2 links.

    Flutter: How to create linked scroll widgets

    Flutter: Creating a two-direction scrolling table with a fixed head and column