2

I have a stateful widget class where I have a SingleChildScrollView which takes Column and a few widgets let's say w1, w2, w3, w4, and w5 all are scrollable what I want to achieve is when the user scrolls up the screen w1, w2, w4, w5 should behave as expected but w3 should stick when it reached to a fix position let say (screen height - 50).

Here is my code I am able to get the position and added a flag too "_isStuck", now I need to stick w3 widget when the flag turns true else it should scroll with the flow when the flag is false.


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

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  final GlobalKey _key = GlobalKey();
  ScrollController _controller = ScrollController();
  bool _isStuck = false;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
  }

  void _afterLayout(_) {
    _controller.addListener(
      () {
        final RenderBox renderBox =
            _key.currentContext!.findRenderObject() as RenderBox;
        final Offset offset = renderBox.localToGlobal(Offset.zero);
        final double startY = offset.dy;

        if (startY <= 120) {
          setState(() {
            _isStuck = true;
          });
        } else {
          setState(() {
            _isStuck = false;
          });
        }
        print("Check position:  - $startY - $_isStuck");
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      controller: _controller,
      child: Column(
        children: [
          Container(
            height: 400,
            color: Colors.red,
            child: const Text('w1'),
          ),
          Container(
            height: 400,
            color: Colors.green,
            child: const Text('w2'),
          ),
          RepaintBoundary(
            child: Container(
              height: 100,
              color: Colors.blue.shade400,
              key: _key,
              child: const Text('w3'),
            ),
          ),
          Container(
            height: 500,
            color: Colors.yellow,
            child: const Text('w4'),
          ),
          Container(
            height: 500,
            color: Colors.orange,
            child: const Text('w5'),
          ),
        ],
      ),
    );
  }
}

enter image description here enter image description here

Adii
  • 23
  • 5
  • check this out even this question dealing the same. [How do I dynamically anchor an item at the top or bottom of a list?](https://stackoverflow.com/a/75791062/14842124) – magesh magi Mar 28 '23 at 14:02

1 Answers1

0

First, create a Stack. Add a SingleChildScrollView as the first item in the Stack. Next, add a Positioned widget with w3 as its child as the second item in the Stack. This Positioned widget will only be rendered if _isStuck is true.

Inside the SingleChildScrollView widget, you will have the w3 widget as well but it will only be visible if _isStuck is false.

Here is the code.

import 'package:flutter/material.dart';

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  final GlobalKey _key = GlobalKey();
  final ScrollController _controller = ScrollController();
  bool _isStuck = false;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
  }

  void _afterLayout(_) {
    _controller.addListener(
      () {
        final RenderBox renderBox =
            _key.currentContext?.findRenderObject() as RenderBox;

        final Offset offset = renderBox.localToGlobal(Offset.zero);
        final double startY = offset.dy;

        setState(() {
          _isStuck = startY <= 120;
        });
        print("Check position:  - $startY - $_isStuck");
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        SingleChildScrollView(
          controller: _controller,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Container(
                height: 400,
                color: Colors.red,
                child: const Text('w1'),
              ),
              Container(
                height: 400,
                color: Colors.green,
                child: const Text('w2'),
              ),
              Visibility(
                visible: !_isStuck,
                maintainAnimation: true,
                maintainState: true,
                maintainSize: true,
                child: _w3(key: _key),
              ),
              Container(
                height: 500,
                color: Colors.yellow,
                child: const Text('w4'),
              ),
              Container(
                height: 500,
                color: Colors.orange,
                child: const Text('w5'),
              ),
            ],
          ),
        ),
        if (_isStuck)
          Positioned(
            top: 120,
            left: 0,
            right: 0,
            child: _w3(),
          ),
        const Padding(
          padding: EdgeInsets.fromLTRB(0, 120, 0, 0),
          child: Divider(
            color: Colors.purple,
          ),
        ),
      ],
    );
  }

  Widget _w3({GlobalKey<State<StatefulWidget>>? key}) {
    return RepaintBoundary(
      child: Container(
        height: 100,
        color: Colors.blue.shade400,
        child: const Text('w3'),
        key: key,
      ),
    );
  }
}

EDIT: I added the key only to the first w3 because the logic is based on the position of that widget. Also instead of not rendering the w3 at all, inside the SingleChildScrollView we are using Visibility widget to avoid the removal of the widget from the tree which causes the _key.currentContext to be null.

Finally I changed from

_isStuck = startY <= 120;

to

_isStuck = startY <= -120;

to make sure it shows the sticky positioned w3 when the other one offscreen.

EDIT 2: Based on the new information

Almis
  • 3,684
  • 2
  • 28
  • 58
  • Thanks for your reply, but did you run the code? the problem with Positioned widget is once it sticks the w3 widget at top: MediaQuery.of(context).size.height -(MediaQuery.of(context).size.height - 120), it is not retaining its old position when you scroll down the screen, w3 should stick when the flag is true at a certain position and should retain its old position when the flag is false, but it is not working in above code. – Adii Mar 27 '23 at 10:24
  • @Adii no I haven't run the code before, check now the edit – Almis Mar 27 '23 at 16:47
  • Thanks for your help, but the code still isn't working properly. Currently, when scrolling up, w3 goes to the top and suddenly reappears at its position when it disappears. When scrolling down, it goes back to the top again. This is not the expected behavior. I want w3 to stick at position (screen.width-120) when scrolling up touches the offset, while other widgets behave normally. When scrolling down, w3 should behave normally and scroll down with the other widgets. Thank you for your time. – Adii Mar 27 '23 at 20:58
  • @Adii sorry, it's confusing, it would help if you could show screenshots of what you expect. I will see if I can help. – Almis Mar 27 '23 at 22:06
  • Hi @Almis thanks again, I may not have professional design skills, but I would like to attempt I have updated the query with a screenshot, which might help you to clear my requirement. – Adii Mar 28 '23 at 13:17
  • I guess CustomScrollView could work in this scenario right? – Adii Mar 28 '23 at 13:29
  • @Adii so what should happen on the second screen when you scroll down a bit more? Will w3 overlap with w4? I mean will w3 cover w4? – Almis Mar 28 '23 at 16:16
  • 1st case: When scrolling up, w3 should stick to the purple line while the other widgets, such as w4, w5, etc., continue to scroll up. You can refer to the 1st, 2nd, and 3rd screens for context 2nd case: When scrolling down, w3 should only start scrolling down once it reaches its original position. As a result, w3 may overlap with w4 during the downward scroll. You can refer to the 4th screen for context. I guess CustomScrollView could work in this scenario. – Adii Mar 28 '23 at 18:59
  • @Adii check now, I think that is what you want or at least close – Almis Mar 28 '23 at 20:46
  • Thank you, @Almis for your time and effort, the layout is very similar to what we need. However, there is a slight issue where black/empty space appears when w3 is positioned beneath the purple line and we scroll upwards. Perhaps we should consider adjusting the positioning of w4 and w5 I'll check it thanks. – Adii Mar 29 '23 at 05:30
  • By any chance do you have any idea how we can reload SingleChildScrollView? I am from iOS background so don't have much idea about flutter. – Adii Mar 29 '23 at 10:01
  • @Adii To force a widget to redraw you simply use the stateful widget and call `setState(() { someThing = false; });`, this recalls the build method by redrawing everything inside! – Almis Mar 29 '23 at 10:05