skip to Main Content

I am developing a mobile app that includes a graph of data on a page. I want to be able to scroll sections of the graph based on user input and am unsure about how to go about this. I am new to Flutter and still learning a lot so am looking for a design steer, i.e. what components to use and how to structure those to achieve the effect that I want.

I currently have the whole page scrolling with a ScrollerView and ScrollControllers, but this means that the top row (the heading and axis), plus the left column (the row labels), scroll off the page. It also means that the user has to scroll the application using scroll bars rather than being able to use gestures anywhere on the screen.

I want to implement a solution where the user can scroll up/down, but the heading and axis stay in place, or can scroll left/right and the heading and row labels remain in place. The image below hopefully shows what I want to achieve.

enter image description here

What are the best components to use to achieve this effect? I am thinking I might have to implement RawGestureDetectors and CustomScrollViews, but wondering if there are some simpler out-of-the-box components to achieve this effect? I am happy to read/learn about whatever is recommended and how to implement these myself, but can someone steer me in a direction in terms of which out-of-the-box components might be best to do this and how these might need to be structured (i.e. parent and child relationships).

I have done a search on stackoverflow and not found anything that specifically covers this, other than an unanswered question here: How to implement a whatsapp mobile scroll effect in flutter. If there are other questions with answers or articles that someone can point me to then I am happy to read these too.

2

Answers


  1. Chosen as BEST ANSWER

    I have managed to achieve the effect I want using GestureDetector and AnimatedBuilder widgets. It needs some tidying up to set limits about how far the user can slide the contents, but I have managed to get the top row axis and chart content to slide left and right in sync, and the left-hand labels and chart content to slide up and down in sync.

    I have made use of DecoratedBox with a colour to mask areas of the screen where I want to hide the scrolling text, and Stack to ensure that the components are drawn in the right order so that the scrolling text moves under the masks. I haven't bothered with the edges of the app, because my chart is full screen in my mobile app. However, if you wanted to use this effect on a subsection of the screen then you would need to implement masking around the rest of the screen so that the text doesn't show up.

    If anyone knows a better way to do this, e.g. only painting the area of the screen where it is visible to the user, then please post an example because that might be a better way to do things.

    Here is my code if anyone wants to see how I managed this:

    import 'package:flutter/material.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({Key? key}) : super(key: key);
    
      // This widget is the root of your application.
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: const MyHomePage(title: 'Flutter Demo Home Page'),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      const MyHomePage({Key? key, required this.title}) : super(key: key);
    
      final String title;
    
      @override
      State<MyHomePage> createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
      double _left = 0.0;
      double _top = 0.0;
      late Offset _leftOnly = Offset(_left, 0.0);
      late Offset _leftTop = Offset(_left, _top);
      late Offset _topOnly = Offset(0.0, _top);
    
      void _setLeft(DragUpdateDetails details) {
        setState(() {
          _left += details.primaryDelta!;
          _leftTop = Offset(_left, _top);
          _leftOnly = Offset(_left, 0);
        });
      }
    
      void _setTop(DragUpdateDetails details) {
        setState(() {
          _top += details.primaryDelta!;
          _leftTop = Offset(_left, _top);
          _topOnly = Offset(0.0, _top);
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
          ),
          body: Center(
            child: Padding(
              padding: const EdgeInsets.only(top: 200, left: 200),
              child: Column(
                  // crossAxisAlignment: CrossAxisAlignment.center,
                  // mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Stack(children: [
                      Row(children: const [
                        //
                        // Top-left box blank to push bottom row into correct space
                        SizedBox(
                          height: 60,
                          width: 100,
                        ),
                        //
                        // Top-right box blank to push bottom row into correct space
                        SizedBox(
                          height: 60,
                          width: 400,
                        ),
                      ]),
                      //
                      // Draw bottom row of boxes
                      //
                      Row(children: [
                        //
                        // Bottom-left box blank to push right-hand box into correct space
                        //
                        const SizedBox(
                          height: 200,
                          width: 100,
                        ),
                        //
                        // Bottom-right box with chart content
                        //
                        GestureDetector(
                          onHorizontalDragUpdate: _setLeft,
                          onVerticalDragUpdate: _setTop,
                          child: ConstrainedBox(
                            key: _bottomRight,
                            constraints:
                              const BoxConstraints(maxHeight: 200, maxWidth: 500),
                            child: DecoratedBox(
                              decoration: BoxDecoration(
                                border: Border.all(),
                              ),
                              child: OverflowBox(
                                maxHeight: double.infinity,
                                maxWidth: double.infinity,
                                alignment: Alignment.topLeft,
                                child: Slide(
                                  offset: _leftTop,
                                  child: Column(
                                    children: const [
                                      Text(
                                          'chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--'),
                                      Text(
                                          'chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--'),
                                      Text(
                                          'chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--'),
                                      Text(
                                          'chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--'),
                                      Text(
                                          'chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--'),
                                      Text(
                                          'chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--'),
                                      Text(
                                          'chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--'),
                                      Text(
                                          'chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--'),
                                      Text(
                                          'chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--'),
                                      Text(
                                          'chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--'),
                                      Text(
                                          'chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--'),
                                      Text(
                                          'chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--'),
                                      Text(
                                          'chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--'),
                                      Text(
                                          'chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--chart-- chart--'),
                                    ],
                                  ),
                                ),
                              ),
                            ),
                          ),
                        ),
                      ]),
                      //
                      // Bottom-left box drawn with content and mask for scrolling text
                      //
                      GestureDetector(
                        onVerticalDragUpdate: _setTop,
                        child: SizedBox(
                          height: 200,
                          width: 100,
                          child: DecoratedBox(
                            decoration: BoxDecoration(
                              border: Border.all(),
                              color: Colors.white,
                            ),
                            child: OverflowBox(
                              alignment: Alignment.topLeft,
                              maxHeight: double.infinity,
                              child: Slide(
                                offset: _topOnly,
                                child: Column(
                                  children: const [
                                    Text('Label'),
                                    Text('Label'),
                                    Text('Label'),
                                    Text('Label'),
                                    Text('Label'),
                                    Text('Label'),
                                    Text('Label'),
                                    Text('Label'),
                                    Text('Label'),
                                    Text('Label'),
                                    Text('Label'),
                                    Text('Label'),
                                    Text('Label'),
                                    Text('Label'),
                                  ],
                                ),
                              ),
                            ),
                          ),
                        ),
                      ),
                      //
                      // Draw top row on boxes with masks to cover scrolling text
                      Row(children: [
                        //
                        // Top-left box blank to draw right hand axis box first
                        //
                        const SizedBox(
                          height: 60,
                          width: 100,
                        ),
                        //
                        // Top-right box draw axis with mask
                        //
                        GestureDetector(
                          onHorizontalDragUpdate: _setLeft,
                          child: SizedBox(
                            height: 60,
                            width: 400,
                            child: DecoratedBox(
                              decoration: BoxDecoration(
                                border: Border.all(),
                                color: Colors.white,
                              ),
                              child: OverflowBox(
                                maxWidth: double.infinity,
                                alignment: Alignment.centerLeft,
                                child: Slide(
                                  offset: _leftOnly,
                                  child: const Text(
                                      'axis--axis--axis--axis--axis--axis--axis--axis--axis--axis--axis--axis--axis--axis--axis--axis--axis--axis--axis--axis--axis--axis--axis--'),
                                ),
                              ),
                            ),
                          ),
                        ),
                      ]),
                      //
                      // Mask to hide text of axis on left
                      SizedBox(
                        height: 60,
                        width: 100,
                        child: DecoratedBox(
                          decoration: BoxDecoration(
                            color: Colors.white,
                            border: Border.all(),
                          ),
                          child: const Text('Heading'),
                        ),
                      ),
                    ]),
                  ]),
            ),
          ),
        );
      }
    }
    
    class Slide extends StatefulWidget {
      Slide({
        Key? key,
        required Widget child,
        required Offset offset,
      }) : super(key: key) {
        _child = child;
        _offset = offset;
      }
    
      late final Widget _child;
      late final Offset _offset;
    
      @override
      State<Slide> createState() => _SlideState();
    }
    
    class _SlideState extends State<Slide> with TickerProviderStateMixin {
      late final AnimationController _controller = AnimationController(
        duration: const Duration(milliseconds: 200),
        vsync: this,
      );
    
      @override
      void dispose() {
        _controller.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return AnimatedBuilder(
          animation: _controller,
          builder: (BuildContext context, Widget? child) {
            return Transform.translate(
              offset: widget._offset,
              child: child,
            );
          },
          child: widget._child,
        );
      }
    }
    

  2. The exact thing you’re looking for isn’t available yet but you can see the preview for two-dimensional scrolling here.

    For now, I would implement the yellow box using a CustomScrollView with a SliverAppBar.

    You can also give the DataTable widget a try.

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