flutter-autocomplete-textfield icon indicating copy to clipboard operation
flutter-autocomplete-textfield copied to clipboard

Make Scrollable

Open anthonycic opened this issue 5 years ago • 11 comments

Hi,

Your textfield is very usefull, but how could we make the suggestions scrollable ?

Thank you in advance,

anthonycic avatar Jul 29 '19 22:07 anthonycic

YES! WE NEED THIS!

danieramiz avatar Sep 23 '19 02:09 danieramiz

For everyone who's asking the same thing, i find another solution :

SimpleAutocompleteFormField

Check on Google.

anthonycic avatar Sep 23 '19 07:09 anthonycic

Here my quick and dirty, only-for-my-needs snippet. No time now to make it more configurable and push a PR

listSuggestionsEntry = OverlayEntry(builder: (context) {
    return Positioned(
        width: width,
        child: CompositedTransformFollower(
            link: _layerLink,
            showWhenUnlinked: false,
            offset: Offset(0.0, height),
            child: ConstrainedBox(
                constraints: BoxConstraints(
                    minWidth: width,
                    maxWidth: width,
                    minHeight: 0,
                    maxHeight: max(0,
                        MediaQuery.of(context).viewInsets.bottom - height - 15)),
                child: Card(
                    child: SingleChildScrollView(
                        child: Column(
                  children: filteredSuggestions.map((suggestion) {
                    return Row(children: [
                      Expanded(
                          child: InkWell(
                              child: itemBuilder(context, suggestion),
                              onTap: () {
                                setState(() {
                                  if (submitOnSuggestionTap) {
                                    String Text = suggestion.toString();
                                    textField.controller.text = Text;
                                    textField.focusNode.unfocus();
                                    itemSubmitted(suggestion);
                                    if (clearOnSubmit) {
                                      clear();
                                    }
                                  } else {
                                    String Text = suggestion.toString();
                                    textField.controller.text = Text;
                                    textChanged(Text);
                                  }
                                });
                              }))
                    ]);
                  }).toList(),
                ))))));
}

Hope it helps

tuarrep avatar Dec 12 '19 10:12 tuarrep

@tuarrep Tried to use this snippet but it doesn't even pass syntax check. Flutter reports "Too many positional arguments 0 expected but 1 found". It seems the code snippet is incomplete to replace the whole listSuggestionsEntry assignment. Also when trying to collapse in Android Studio it has at least missing brackets. Tried adding those but it didn't help on the positional argument error.

Can you please post a complete example block for filling listSuggestionsEntry

Update: I found the missing part but now I wonder why only 2 entries are shown and why there's now scrollbar

marcelser avatar Feb 27 '20 17:02 marcelser

@marcelser

It works fine on my side. This snippet was extracted from a working and published app (I've added missing bracket).

Without more precise information I can't help you.

tuarrep avatar Mar 22 '20 17:03 tuarrep

I tried this snippet, turn out the overlay does not show up.

sirapolw avatar Jun 13 '20 09:06 sirapolw

I'm sorry @sirapolw I couldn't help you. It was working for me in December. Since then the project was shipped to my client and I do not maintain it anymore.

tuarrep avatar Jun 13 '20 11:06 tuarrep

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

typedef Widget AutoCompleteOverlayItemBuilder<T>( BuildContext context, T suggestion);

typedef bool Filter<T>(T suggestion, String query);

typedef InputEventCallback<T>(T data);

typedef StringCallback(String data);

class AutoCompleteTextField<T> extends StatefulWidget { final List<T> suggestions; final Filter<T> itemFilter; final Comparator<T> itemSorter; final StringCallback textChanged, textSubmitted; final ValueSetter onFocusChanged; final InputEventCallback<T> itemSubmitted; final AutoCompleteOverlayItemBuilder<T> itemBuilder; final GlobalKey<AutoCompleteTextFieldState<T>> key; final bool submitOnSuggestionTap, clearOnSubmit; final List<TextInputFormatter> inputFormatters; final int minLength;

final InputDecoration decoration; final TextStyle style; final TextInputType keyboardType; final TextInputAction textInputAction; final TextCapitalization textCapitalization; final TextEditingController controller; final FocusNode focusNode;

AutoCompleteTextField( {@required this.itemSubmitted, //Callback on item selected, this is the item selected of type <T> @required this.key, //GlobalKey used to enable addSuggestion etc @required this.suggestions, //Suggestions that will be displayed @required this.itemBuilder, //Callback to build each item, return a Widget @required this.itemSorter, //Callback to sort items in the form (a of type <T>, b of type <T>) @required this.itemFilter, //Callback to filter item: return true or false depending on input text this.inputFormatters, this.style, this.decoration: const InputDecoration(), this.textChanged, //Callback on input text changed, this is a string this.textSubmitted, //Callback on input text submitted, this is also a string this.onFocusChanged, this.keyboardType: TextInputType.text, // this.suggestionsAmount: // 5, //The amount of suggestions to show, larger values may result in them going off screen this.submitOnSuggestionTap: true, //Call textSubmitted on suggestion tap, itemSubmitted will be called no matter what this.clearOnSubmit: true, //Clear autoCompleteTextfield on submit this.textInputAction: TextInputAction.done, this.textCapitalization: TextCapitalization.sentences, this.minLength = 1, this.controller, this.focusNode}) : super(key: key);

void clear() => key.currentState.clear();

void addSuggestion(T suggestion) => key.currentState.addSuggestion(suggestion);

void removeSuggestion(T suggestion) => key.currentState.removeSuggestion(suggestion);

void updateSuggestions(List<T> suggestions) => key.currentState.updateSuggestions(suggestions);

void triggerSubmitted() => key.currentState.triggerSubmitted();

void updateDecoration( {InputDecoration decoration, List<TextInputFormatter> inputFormatters, TextCapitalization textCapitalization, TextStyle style, TextInputType keyboardType, TextInputAction textInputAction}) => key.currentState.updateDecoration(decoration, inputFormatters, textCapitalization, style, keyboardType, textInputAction);

TextField get textField => key.currentState.textField;

@override State<StatefulWidget> createState() => new AutoCompleteTextFieldState<T>( suggestions, textChanged, textSubmitted, onFocusChanged, itemSubmitted, itemBuilder, itemSorter, itemFilter, submitOnSuggestionTap, clearOnSubmit, minLength, inputFormatters, textCapitalization, decoration, style, keyboardType, textInputAction, controller, focusNode); }

class AutoCompleteTextFieldState<T> extends State<AutoCompleteTextField> { final LayerLink _layerLink = LayerLink();

TextField textField; List<T> suggestions; StringCallback textChanged, textSubmitted; ValueSetter onFocusChanged; InputEventCallback<T> itemSubmitted; AutoCompleteOverlayItemBuilder<T> itemBuilder; Comparator<T> itemSorter; OverlayEntry listSuggestionsEntry; List<T> filteredSuggestions; Filter<T> itemFilter; int minLength; bool submitOnSuggestionTap, clearOnSubmit; TextEditingController controller; FocusNode focusNode;

String currentText = "";

InputDecoration decoration; List<TextInputFormatter> inputFormatters; TextCapitalization textCapitalization; TextStyle style; TextInputType keyboardType; TextInputAction textInputAction;

AutoCompleteTextFieldState( this.suggestions, this.textChanged, this.textSubmitted, this.onFocusChanged, this.itemSubmitted, this.itemBuilder, this.itemSorter, this.itemFilter, this.submitOnSuggestionTap, this.clearOnSubmit, this.minLength, this.inputFormatters, this.textCapitalization, this.decoration, this.style, this.keyboardType, this.textInputAction, this.controller, this.focusNode) { textField = new TextField( inputFormatters: inputFormatters, textCapitalization: textCapitalization, decoration: decoration, style: style, keyboardType: keyboardType, focusNode: focusNode ?? new FocusNode(), controller: controller ?? new TextEditingController(), textInputAction: textInputAction, onChanged: (newText) { currentText = newText; updateOverlay(newText);

    if (textChanged != null) {
      textChanged(newText);
    }
  },
  onTap: () {
    updateOverlay(currentText);
  },
  onSubmitted: (submittedText) =>
      triggerSubmitted(submittedText: submittedText),
);

if (this.controller != null && this.controller.text != null) {
  currentText = this.controller.text;
}

textField.focusNode.addListener(() {
  if (onFocusChanged != null) {
    onFocusChanged(textField.focusNode.hasFocus);
  }

  if (!textField.focusNode.hasFocus) {
    filteredSuggestions = [];
    updateOverlay();
  } else if (!(currentText == "" || currentText == null)) {
    updateOverlay(currentText);
  }
});

}

void updateDecoration( InputDecoration decoration, List<TextInputFormatter> inputFormatters, TextCapitalization textCapitalization, TextStyle style, TextInputType keyboardType, TextInputAction textInputAction) { if (decoration != null) { this.decoration = decoration; }

if (inputFormatters != null) {
  this.inputFormatters = inputFormatters;
}

if (textCapitalization != null) {
  this.textCapitalization = textCapitalization;
}

if (style != null) {
  this.style = style;
}

if (keyboardType != null) {
  this.keyboardType = keyboardType;
}

if (textInputAction != null) {
  this.textInputAction = textInputAction;
}

setState(() {
  textField = new TextField(
    inputFormatters: this.inputFormatters,
    textCapitalization: this.textCapitalization,
    decoration: this.decoration,
    style: this.style,
    keyboardType: this.keyboardType,
    focusNode: focusNode ?? new FocusNode(),
    controller: controller ?? new TextEditingController(),
    textInputAction: this.textInputAction,
    onChanged: (newText) {
      currentText = newText;
      updateOverlay(newText);

      if (textChanged != null) {
        textChanged(newText);
      }
    },
    onTap: () {
      updateOverlay(currentText);
    },
    onSubmitted: (submittedText) =>
        triggerSubmitted(submittedText: submittedText),
  );
});

}

void triggerSubmitted({submittedText}) { submittedText == null ? textSubmitted(currentText) : textSubmitted(submittedText);

if (clearOnSubmit) {
  clear();
}

}

void clear() { textField.controller.clear(); currentText = ""; updateOverlay(); }

void addSuggestion(T suggestion) { suggestions.add(suggestion); updateOverlay(currentText); }

void removeSuggestion(T suggestion) { suggestions.contains(suggestion) ? suggestions.remove(suggestion) : throw "List does not contain suggestion and therefore cannot be removed"; updateOverlay(currentText); }

void updateSuggestions(List<T> suggestions) { this.suggestions = suggestions; updateOverlay(currentText); }

void updateOverlay([String query]) { if (listSuggestionsEntry == null) { final Size textFieldSize = (context.findRenderObject() as RenderBox).size; final width = textFieldSize.width; final height = textFieldSize.height; listSuggestionsEntry = new OverlayEntry(builder: (context) { return new Positioned( width: width, child: CompositedTransformFollower( link: _layerLink, showWhenUnlinked: false, offset: Offset(0.0, height), child: ConstrainedBox( constraints: BoxConstraints( minWidth: width, minHeight: 0, maxHeight: max( 0, MediaQuery.of(context).viewInsets.bottom - height - 15), ), child: new Card( child: Scrollbar( child: SingleChildScrollView( child: new Column( children: filteredSuggestions.map((suggestion) { return new InkWell( child: itemBuilder(context, suggestion), onTap: () { setState(() { if (submitOnSuggestionTap) { String Text = suggestion.toString(); textField.controller.text = Text; textField.focusNode.unfocus(); itemSubmitted(suggestion); if (clearOnSubmit) { clear(); } } else { String Text = suggestion.toString(); textField.controller.text = Text; textChanged(Text); } }); }); }).toList(), ), ), ))))); }); Overlay.of(context).insert(listSuggestionsEntry); }

filteredSuggestions = getSuggestions(
    suggestions,
    itemSorter,
    itemFilter,
    // suggestionsAmount,
    query);

listSuggestionsEntry.markNeedsBuild();

}

List<T> getSuggestions( List<T> suggestions, Comparator<T> sorter, Filter<T> filter, String query) { if (null == query || query.length < minLength) { return []; }

suggestions = suggestions.where((item) => filter(item, query)).toList();
suggestions.sort(sorter);
return suggestions;

}

@override void dispose() { // if we created our own focus node and controller, dispose of them // otherwise, let the caller dispose of their own instances if (focusNode == null) { textField.focusNode.dispose(); } if (controller == null) { textField.controller.dispose(); } super.dispose(); }

@override Widget build(BuildContext context) { return CompositedTransformTarget(link: _layerLink, child: textField); } }

class SimpleAutoCompleteTextField extends AutoCompleteTextField<String> { final StringCallback textChanged, textSubmitted; final int minLength; final ValueSetter onFocusChanged; final TextEditingController controller; final FocusNode focusNode;

SimpleAutoCompleteTextField( {TextStyle style, InputDecoration decoration: const InputDecoration(), this.onFocusChanged, this.textChanged, this.textSubmitted, this.minLength = 1, this.controller, this.focusNode, TextInputType keyboardType: TextInputType.text, @required GlobalKey<AutoCompleteTextFieldState<String>> key, @required List<String> suggestions, bool submitOnSuggestionTap: true, bool clearOnSubmit: true, TextInputAction textInputAction: TextInputAction.done, TextCapitalization textCapitalization: TextCapitalization.sentences}) : super( style: style, decoration: decoration, textChanged: textChanged, textSubmitted: textSubmitted, itemSubmitted: textSubmitted, keyboardType: keyboardType, key: key, suggestions: suggestions, itemBuilder: null, itemSorter: null, itemFilter: null, submitOnSuggestionTap: submitOnSuggestionTap, clearOnSubmit: clearOnSubmit, textInputAction: textInputAction, textCapitalization: textCapitalization);

@override State<StatefulWidget> createState() => new AutoCompleteTextFieldState<String>( suggestions, textChanged, textSubmitted, onFocusChanged, itemSubmitted, (context, item) { return new Padding(padding: EdgeInsets.all(8.0), child: new Text(item)); }, (a, b) { return a.compareTo(b); }, (item, query) { return item.toLowerCase().startsWith(query.toLowerCase()); }, submitOnSuggestionTap, clearOnSubmit, minLength, [], textCapitalization, decoration, style, keyboardType, textInputAction, controller, focusNode); }

` Here's my working version of autocomplete textfield with scrollable, implemented from @tuarrep 's snippet. thank me later.

sirapolw avatar Jun 13 '20 19:06 sirapolw

Any updates on this? Really need that feature

adschi avatar Sep 17 '20 11:09 adschi

Here my quick and dirty, only-for-my-needs snippet. No time now to make it more configurable and push a PR

listSuggestionsEntry = OverlayEntry(builder: (context) {
    return Positioned(
        width: width,
        child: CompositedTransformFollower(
            link: _layerLink,
            showWhenUnlinked: false,
            offset: Offset(0.0, height),
            child: ConstrainedBox(
                constraints: BoxConstraints(
                    minWidth: width,
                    maxWidth: width,
                    minHeight: 0,
                    maxHeight: max(0,
                        MediaQuery.of(context).viewInsets.bottom - height - 15)),
                child: Card(
                    child: SingleChildScrollView(
                        child: Column(
                  children: filteredSuggestions.map((suggestion) {
                    return Row(children: [
                      Expanded(
                          child: InkWell(
                              child: itemBuilder(context, suggestion),
                              onTap: () {
                                setState(() {
                                  if (submitOnSuggestionTap) {
                                    String Text = suggestion.toString();
                                    textField.controller.text = Text;
                                    textField.focusNode.unfocus();
                                    itemSubmitted(suggestion);
                                    if (clearOnSubmit) {
                                      clear();
                                    }
                                  } else {
                                    String Text = suggestion.toString();
                                    textField.controller.text = Text;
                                    textChanged(Text);
                                  }
                                });
                              }))
                    ]);
                  }).toList(),
                ))))));
}

Hope it helps

Currently, this is the best way. Thank you

pacakman avatar Aug 05 '21 10:08 pacakman

Here is my working snippet If the above shared code is not working for you:


 listSuggestionsEntry = OverlayEntry(builder: (context) {
        return Positioned(
            width: width,
            child: CompositedTransformFollower(
                link: _layerLink,
                showWhenUnlinked: false,
                offset: Offset(0.0, height),
                child: SizedBox(
                    width: width,
                    height: 300,
                    child: SingleChildScrollView(
                      child: Card(
                          child:  Column(
                        children: filteredSuggestions!.map((suggestion) {
                          return Row(children: [
                            Expanded(
                                child: InkWell(
                                    child: itemBuilder!(context, suggestion),
                                    onTap: () {
                                      if (!mounted) return;
                                      setState(() {
                                        if (submitOnSuggestionTap) {
                                          String newText = suggestion.toString();
                                          textField!.controller!.text = newText;
                                          if (unFocusOnItemSubmitted) {
                                            textField!.focusNode!.unfocus();
                                          }
                                          itemSubmitted!(suggestion);
                                          if (clearOnSubmit) {
                                            clear();
                                          }
                                        } else {
                                          String newText = suggestion.toString();
                                          textField!.controller!.text = newText;
                                          textChanged!(newText);
                                        }
                                      });
                                    }))
                          ]);
                        }).toList(),
                      )),
                    ))));
      }); 

PS: Above code is null safe and has lint issues resolved

mustafabhatkar avatar Jul 05 '23 12:07 mustafabhatkar