AbdulRahmanAlHamali / flutter_typeahead

A TypeAhead widget for Flutter, where you can show suggestions to users as they type
BSD 2-Clause "Simplified" License
831 stars 350 forks source link

[Bug] [Regression from v4] Suggestions box not resizing after scroll #555

Open davidmartos96 opened 11 months ago

davidmartos96 commented 11 months ago

I'm not sure if this is exactly the same as https://github.com/AbdulRahmanAlHamali/flutter_typeahead/issues/455, because that issue appears to exist on v4 unlike this one, but looks somewhat related.

Steps to reproduce

  1. Run the example provided
  2. Open the suggestions
  3. Scroll the ListView, not the suggestions box results

These steps can be run on version v4.8.0 and v5.x to see the difference

Expected results

The suggestions box should be resized automatically according to its constraints after the scroll action has finished. This was the behavior in version v4.

The original code from v4 which was taking care of this has been removed. https://github.com/AbdulRahmanAlHamali/flutter_typeahead/blob/c6ff9b23581a072b3208ec99d6040971b39db848/lib/src/material/field/typeahead_field.dart#L612C31-L612C31

Actual results

The box does not resize

Package Version

5.0.1

Platform

Android

Code sample

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

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'TypeAhead Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: ScrollExample(),
    );
  }
}

class ScrollExample extends StatelessWidget {
  final List<String> items = List.generate(50, (index) => "Item $index");

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Scrollbar(
        child: ListView(
          children: [
            Container(
              height: 200,
              color: Colors.red.withOpacity(0.3),
              child: const Center(
                child: Padding(
                  padding: EdgeInsets.all(8.0),
                  child: Text("Suggestion box should resize when scrolling"),
                ),
              ),
            ),
            // Typeahead V5
            /* TypeAheadField<String>(
              suggestionsCallback: (String pattern) async {
                return items.where((item) => item.toLowerCase().startsWith(pattern.toLowerCase())).toList();
              },
              itemBuilder: (context, String suggestion) {
                return ListTile(
                  title: Text(suggestion),
                );
              },
              onSelected: (String suggestion) {
                print("Suggestion selected");
              },
            ), */
            // Typeahead V4
            TypeAheadField<String>(
              suggestionsCallback: (String pattern) async {
                return items.where((item) => item.toLowerCase().startsWith(pattern.toLowerCase())).toList();
              },
              itemBuilder: (context, String suggestion) {
                return ListTile(
                  title: Text(suggestion),
                );
              },
              onSuggestionSelected: (String suggestion) {
                print("Suggestion selected");
              },
            ),
            Container(height: 1000, color: Colors.green),
          ],
        ),
      ),
    );
  }
}
jooikwanw commented 10 months ago

Any update on this? I am facing this issue still

clragon commented 10 months ago

Hi @davidmartos96, thanks for your issue.

It's currently unclear to me whether this would be desirable behaviour. I have to investigate whether this would be a good feature.

In the last version, this behaviour seemed to barely work correctly on desktop platforms. Scrolling the outer scrollview to expand the suggestions box even beyond the original layout where it was contained in does not sound like a good user experience on the face of it.

We might run into some issues with how we hide the box when it is out of view. We would also need to rewrite the layout calculation, accounting for a negative offset of the field in relation to the overlay in which the box is displayed.

When I wrote version 5, I have intentionally left this out, as it seemed janky and I was unsure whether we actually want it. I would be interested in hearing a compelling case for this feature though, and maybe a piece of sample code where this feature shows being worthwhile.

davidmartos96 commented 10 months ago

@clragon Thank you for considering! Here is a more elaborated example (code below)

It's how I'm currently using typeahead in a scrollable UI. To improve UX, if the user focuses the field when it's almost at the bottom in the viewport, I scroll the necessary pixels in order to show some amount of the suggestions box.

With v5 I cannot manage to make the same behavior. What behavior barely worked on desktop? I'm trying the demo from this comment on both desktop and mobile and it works how I expect. If the box goes out of view it also hides correctly

The user would tap the typeahead when the UI is like this:

image

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'TypeAhead Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const ScrollExample(),
    );
  }
}

class ScrollExample extends StatelessWidget {
  const ScrollExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Scroll Example'),
      ),
      body: Scrollbar(
        child: ListView(
          primary: true,
          children: [
            const Center(
              child: Text(
                'BELOW THERE IS A TYPEAHEAD FIELD.',
                style: TextStyle(fontWeight: FontWeight.bold),
              ),
            ),
            for (int i = 0; i < 10; i++)
              Container(
                margin: const EdgeInsets.all(8.0),
                height: 50,
                color: Colors.red.withOpacity(0.3),
                child: Center(
                  child: Text("some UI above field $i"),
                ),
              ),
            Container(
              margin: const EdgeInsets.all(8.0),
              padding: const EdgeInsets.all(8.0),
              color: Colors.blue.withOpacity(0.3),
              child: const _TypeadFieldWrapper(),
            ),
            for (int i = 0; i < 20; i++)
              Container(
                margin: const EdgeInsets.all(8.0),
                height: 50,
                color: Colors.green.withOpacity(0.3),
                child: Center(
                  child: Text("some UI below field $i"),
                ),
              ),
          ],
        ),
      ),
    );
  }
}

class _TypeadFieldWrapper extends StatefulWidget {
  const _TypeadFieldWrapper();

  @override
  State<_TypeadFieldWrapper> createState() => __TypeadFieldWrapperState();
}

class __TypeadFieldWrapperState extends State<_TypeadFieldWrapper> {
  final FocusNode _focusNode = FocusNode();
  final List<String> items = List.generate(50, (index) => "Item $index");

  //late final suggestionsController = SuggestionsController<String>();

  @override
  void initState() {
    super.initState();
    _focusNode.addListener(_ensureSuggestionsVisible);
  }

  @override
  void dispose() {
    _focusNode.removeListener(_ensureSuggestionsVisible);
    _focusNode.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // Typeahead v5
    /* TypeAheadField<String>(
      suggestionsController: suggestionsController,
      suggestionsCallback: (String pattern) async {
        return items.where((item) => item.toLowerCase().startsWith(pattern.toLowerCase())).toList();
      },
      itemBuilder: (context, String suggestion) {
        return ListTile(
          title: Text(suggestion),
        );
      },
      onSelected: (String suggestion) {
        print("Suggestion selected");
      },
    ), */

    // Typeahead v4
    return TypeAheadField<String>(
      suggestionsCallback: (String pattern) async {
        return items.where((item) => item.toLowerCase().startsWith(pattern.toLowerCase())).toList();
      },
      suggestionsBoxDecoration: const SuggestionsBoxDecoration(
        constraints: BoxConstraints(
          maxHeight: 300,
        ),
      ),
      itemBuilder: (context, String suggestion) {
        return ListTile(
          title: Text(suggestion),
        );
      },
      textFieldConfiguration: TextFieldConfiguration(
        focusNode: _focusNode,
      ),
      onSuggestionSelected: (String suggestion) {
        print("Suggestion selected");
      },
    );
  }

  Future<void> _ensureSuggestionsVisible() async {
    // Wait for keyboard open
    await Future<void>.delayed(const Duration(milliseconds: 600));

    if (!mounted || !_focusNode.hasFocus) return;

    final RenderObject fieldRenderObject = context.findRenderObject()!;
    final RenderAbstractViewport viewport = RenderAbstractViewport.of(fieldRenderObject);

    final ScrollableState scrollableState = Scrollable.of(context);

    final ScrollPosition position = scrollableState.position;

    final offsetToRevealField = viewport.getOffsetToReveal(fieldRenderObject, 1.0);

    // How much of the suggestions box we want to reveal
    const double boxRevealSize = 150;

    // Add boxRevealSize to offsetToReveal to account for the amount of the suggestions box
    final offsetToRevealBox = offsetToRevealField.offset + boxRevealSize;

    if (offsetToRevealBox < 0 || position.pixels >= offsetToRevealBox) {
      // The desired amount is already visible
      return;
    }

    // Scroll to reveal the suggestions box
    await position.animateTo(
      offsetToRevealBox,
      duration: const Duration(milliseconds: 100),
      curve: Curves.linear,
    );
  }
}
clragon commented 10 months ago

Thank you for the elaborate example. I understand the issue better now. I will investigate how we can fix this when I have time.

davidmartos96 commented 7 months ago

@clragon Would exposing a resize method to be called from the user side be feasible to do? I believe that could work too, as we know when we need to resize the suggestions.

frederikstonge commented 7 months ago

It would be nice to determine the position of the control in the screen and accordingly change the position of the suggestions (if at the bottom, show suggestions at the top).

clragon commented 7 months ago

@davidmartos96 The resize method is already public and you can call it on the controller whenever you need to. @frederikstonge This behaviour is already part of the package, though it is unrelated to this issue here.

frederikstonge commented 7 months ago

@davidmartos96 The resize method is already public and you can call it on the controller whenever you need to. @frederikstonge This behaviour is already part of the package, though it is unrelated to this issue here.

Well it didn't work for me. It is related because when my control is too low on my screen, the suggestions are built under the keyboard, resulting in no suggestion box. When I scroll down, nothing happens because there's no resize.

frederikstonge commented 7 months ago

Also, just added a periodic timer to resize, if the suggestionbox was drawn shrinked because of the available space at the bottom of the screen, calling resize on the controller doesn't do anything.

davidmartos96 commented 7 months ago

@frederikstonge Yes, it didn't work for me either when I tried it out back when I opened the thread. Otherwise I would have gone with the Timer approach too. There must be something else that prevents it from being resized correctly unlike with v4.

clragon commented 6 months ago

I see. the resize method should definitely trigger the field to recalculate its position/size though it seems this is somehow not working correctly, from multiple reports. I will investigate that too, thank you.

frederikstonge commented 5 months ago

I see. the resize method should definitely trigger the field to recalculate its position/size though it seems this is somehow not working correctly, from multiple reports. I will investigate that too, thank you.

Any news on the issue? Maybe I could help... This is the only package decent enough to use.