skip to Main Content

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

2

Answers


  1. Chosen as BEST ANSWER

    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


  2. You can archive this using nested listView widgets.

    You can try this approach:

    class TimeTableView extends StatefulWidget {
      const TimeTableView({super.key});
    
      @override
      State<TimeTableView> createState() => _TimeTableViewState();
    }
    
    class _TimeTableViewState extends State<TimeTableView> {
      final periodsSlots = 15;
    
      final double containerHeight = 55.0;
    
      final double containerWidth = 80;
    
      final days = [
        "Monday",
        "Tuesday",
        "Wednesday",
        "Thursday",
        "Friday",
        "Saturday",
      ];
    
      final subjects = [
        "Maths",
        "Hindi",
        "English",
        "Chemistry",
        "History",
        "Geography",
      ];
    
      late ScrollController mainController;
    
      late ScrollController secondController;
    
      @override
      void initState() {
        super.initState();
        secondController = ScrollController();
    
        mainController = ScrollController()
          ..addListener(() {
            if (mainController.hasClients && secondController.hasClients) {
              secondController.jumpTo(mainController.offset);
            }
          });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text("Time Table"),
          ),
          body: Padding(
            padding: const EdgeInsets.all(8.0),
            child: PageView(
              children: [
                Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Row(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        // Top header View
                        SizedBox(
                          height: containerHeight,
                          width: containerWidth,
                          child: CustomPaint(
                            painter: RectanglePainter(),
                            child: Stack(
                              children: const [
                                Align(
                                  alignment: Alignment.topRight,
                                  child: Padding(
                                    padding: EdgeInsets.all(4.0),
                                    child: Text("Day"),
                                  ),
                                ),
                                Align(
                                  alignment: Alignment.bottomLeft,
                                  child: Padding(
                                    padding: EdgeInsets.all(4.0),
                                    child: Text("Period"),
                                  ),
                                ),
                              ],
                            ),
                          ),
                        ),
                        Expanded(
                          child: SizedBox(
                            height: containerHeight,
                            child: SingleChildScrollView(
                              physics: const ClampingScrollPhysics(),
                              padding: EdgeInsets.zero,
                              scrollDirection: Axis.horizontal,
                              controller: mainController,
                              child: Row(
                                children: List<Widget>.generate(
                                  days.length,
                                  (index) => Container(
                                    height: containerHeight,
                                    width: containerWidth,
                                    color: bgColorHeader(index),
                                    child: Padding(
                                      padding:
                                          const EdgeInsets.symmetric(horizontal: 8),
                                      child: Center(child: Text(days[index])),
                                    ),
                                  ),
                                ),
                              ),
                            ),
                          ),
                        )
                      ],
                    ),
                    SizedBox(
                      // Added fixed size to scroll listView horizontal
                      height: 500,
                      child: ListView.builder(
                        physics: const ClampingScrollPhysics(),
                        padding:
                            EdgeInsets.zero, // remove listview default padding.
                        shrinkWrap: true,
                        scrollDirection: Axis.vertical,
                        itemCount: periodsSlots,
                        itemBuilder: (context, index) => Container(
                          height: containerHeight,
                          width: containerWidth,
                          color: bgColorHeader(index),
                          child: Row(
                            mainAxisSize: MainAxisSize.min,
                            children: [
                              // period header
                              Padding(
                                padding: EdgeInsets.zero,
                                child: SizedBox(
                                  width: containerWidth,
                                  height: containerHeight,
                                  child: Center(child: Text("period ${index + 1}")),
                                ),
                              ),
                              // period subjects
                              Expanded(
                                child: SizedBox(
                                  height: containerHeight,
                                  child: SingleChildScrollView(
                                    physics: const ClampingScrollPhysics(),
                                    padding: EdgeInsets.zero,
                                    scrollDirection: Axis.horizontal,
                                    controller: secondController,
                                    child: Row(
                                      children: List<Widget>.generate(
                                        subjects.length,
                                        (index) => Container(
                                          color: Colors.white,
                                          child: Container(
                                            height: containerHeight,
                                            width: containerWidth,
                                            color: bgColorSubject(index),
                                            child: Center(
                                                child: Padding(
                                              padding: const EdgeInsets.symmetric(
                                                  horizontal: 4),
                                              child: Text(subjects[index]),
                                            )),
                                          ),
                                        ),
                                      ),
                                    ),
                                  ),
                                ),
                              )
                            ],
                          ),
                        ),
                      ),
                    )
                  ],
                ),
              ],
            ),
          ),
        );
      }
    
      // Alternate background colors
      Color bgColorHeader(int index) =>
          index % 2 == 0 ? Colors.cyan.withOpacity(0.5) : Colors.cyanAccent;
    
      Color bgColorSubject(int index) =>
          index % 2 == 0 ? Colors.grey.withOpacity(0.5) : Colors.grey;
    }
    
    // Draw cross line from top left container
    class RectanglePainter extends CustomPainter {
      @override
      void paint(Canvas canvas, Size size) {
        final backgroundPaint = Paint()
          ..color = Colors.cyanAccent
          ..strokeWidth = 2.0
          ..strokeCap = StrokeCap.round;
    
        final crossLine = Paint()
          ..color = Colors.white
          ..strokeWidth = 2.0
          ..strokeCap = StrokeCap.round;
    
        // Draw the rectangle
        canvas.drawRect(Offset.zero & size, backgroundPaint);
    
        // Draw the cross line
        canvas.drawLine(Offset.zero, Offset(size.width, size.height), crossLine);
        //canvas.drawLine(Offset(0, size.height), Offset(size.width, 0), crossLine);
      }
    
      @override
      bool shouldRepaint(RectanglePainter oldDelegate) => false;
    }
    

    enter image description here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search