flutter_typeahead
flutter_typeahead copied to clipboard
[Bug] [Regression from v4] Suggestions box not resizing after scroll
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
- Run the example provided
- Open the suggestions
- 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),
],
),
),
);
}
}
Any update on this? I am facing this issue still
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.
@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:
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,
);
}
}
Thank you for the elaborate example. I understand the issue better now. I will investigate how we can fix this when I have time.
@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.
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).
@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.
@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.
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.
@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.