skip to Main Content

First of all, some optional context:

I am working on a certain aspect of our app where I need to be able to scroll to a certain widget inside a ListView. To be specific, I have a bunch of input categories (comprised of Cards with multimodal input Widgets like TextInputs or Take-Image-Buttons). At the very end of the ListView, I have a button labeled "What is still missing?". When the user clicks this button, the app validates the different categories, and subsequently should scroll up to the highest (e.g. lowest scroll offset) category that does not yet have valid input.

To achieve this, I created a bunch of different reactive components that amongst other things, allow widgets to "register" themselves with a callback that is supposed to determine their position within the ListView.

class _RoofSketchState extends State<_RoofSketch>
    with AutomaticKeepAliveClientMixin {
  static const _category = TqRoofCategory.roofSketch;
  final _key = GlobalKey();

  TqScrollMasterNineThousand? _scrollMaster;

  @override
  void initState() {
    super.initState();
    _registerScrollEventCallback();
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _unregisterScrollEventCallback();
    _scrollMaster = context.read<TqScrollMasterNineThousand>();
    _registerScrollEventCallback();
  }

  @override
  void dispose() {
    _unregisterScrollEventCallback();
    super.dispose();
  }

  Option<TqScrollMasterNineThousandScrollEvent> _createScrollEvent() {
    final scrollable = Scrollable.maybeOf(context);
    if ((scrollable, _key.currentContext)
        case (final scrollable?, final currentContext?)) {
      final renderBox = currentContext.findRenderObject() as RenderBox?;
      final scrollableBox = scrollable.context.findRenderObject() as RenderBox?;
      if ((renderBox, scrollableBox)
          case (final renderBox?, final scrollableBox?)) {
        final offset =
            renderBox.localToGlobal(Offset.zero, ancestor: scrollableBox).dy;

        return Some(
          TqScrollMasterNineThousandScrollEvent(
            scrollOffset: offset,
            onScrollToOffset: () {},
          ),
        );
      }
    }
    return const None();
  }

  void _registerScrollEventCallback() {
    _scrollMaster?.registerScrollEventCallback(
      category: _category,
      eventCallback: _createScrollEvent,
    );
  }

  void _unregisterScrollEventCallback() =>
      _scrollMaster?.unregisterScrollEvent(_category);

The implementation details of the class TqScrollMasterNineThousand are not relevant here. When the user clicks the button "What is missing?", the TqScrollMasterNineThousand invokes all registered callbacks, and attempts to scroll to the earliest scroll offset that was associated with a category that contained non-valid inputs.

My actual question

Consider this function that is supposed to serve as a callback that determines the scroll position of my widget within the next Scrollable inside the Widget tree:

  Option<TqScrollMasterNineThousandScrollEvent> _createScrollEvent() {
    final scrollable = Scrollable.maybeOf(context);
    if ((scrollable, _key.currentContext)
        case (final scrollable?, final currentContext?)) {
      final renderBox = currentContext.findRenderObject() as RenderBox?;
      final scrollableBox = scrollable.context.findRenderObject() as RenderBox?;
      if ((renderBox, scrollableBox)
          case (final renderBox?, final scrollableBox?)) {
        final offset =
            renderBox.localToGlobal(Offset.zero, ancestor: scrollableBox).dy;

        return Some(
          TqScrollMasterNineThousandScrollEvent(
            scrollOffset: offset,
            onScrollToOffset: () {},
          ),
        );
      }
    }
    return const None();
  }

I hoped this would work, and that renderBox.localToGlobal(Offset.zero, ancestor: scrollableBox).dy; would evaluate to the scroll position. However, it returns NaN. I couldn’t find any documentation on how I may be able to get the scroll position, so if anybody has an idea, please enlighten me.

Best regards

2

Answers


  1. I can’t help with your code but I can suggest some packages. I have used 2 packages below and it work well. Hope this help:
    https://pub.dev/packages/scrollable_positioned_list
    https://pub.dev/packages/scroll_to_index

    Login or Signup to reply.
  2. You can assign the keys to each of your input widgets, for example:

    final _nameFieldKey = GlobalKey<FormState>();
    final _ageFieldKey = GlobalKey<FormState>();
    final _uploadImageFieldKey = GlobalKey<FormState>();
    
    ListView(
           
            children: [
              TextFormField(
                key: _nameFieldKey,
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter your name';
                  }
                  return null;
                },
              ),
              TextFormField(
                key: _ageFieldKey,
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter your age';
                  }
                  return null;
                },
              ),
              ImageUploadField(
                key: _imageUploadFieldKey,
                //other attributes
              ),
    
              ElevatedButton(
                  onPressed: () {
                    if(!nameIsValid()) {
                       Scrollable.ensureVisible(
                         _nameFieldKey.currentContext!,
                         duration: Duration(seconds: 1),
                       );
                    }
                  },
                  child: const Text('Submit'),
              ),
          ],
    )
    

    and then use the Scrollable.ensureVisible method to scroll to a specific input field by key

    Scrollable.ensureVisible(
       _nameFieldKey.currentContext!,
       duration: Duration(seconds: 1),
    );
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search