Skip to content

Commit

Permalink
Fix height problems with animations and dialogs
Browse files Browse the repository at this point in the history
  • Loading branch information
KaYBlitZ committed Apr 30, 2019
1 parent b675633 commit 2827518
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 36 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 1.5.0 25/04/2019

- Added `suggestionsBoxController` property and `SuggestionsBoxController` class to
allow manual control of the suggestions box
- Fix suggestions box height problems in dialogs

## 1.4.1 09/04/2019

- Fixed BoxConstraints width parameters being ignored in `SuggestionsBoxDecoration`
Expand Down
41 changes: 39 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,38 @@ The `transitionBuilder` allows us to customize the animation of the
suggestion box. In this example, we are returning the suggestionsBox
immediately, meaning that we don't want any animation.

## Warnings

### Animations
Placing TypeAheadField in widgets with animations may cause the suggestions box
to resize incorrectly. Since animation times are variable, this has to be
corrected manually at the end of the animation. You will need to add a
SuggestionsBoxController described below and the following code for the
AnimationController.
```dart
void Function(AnimationStatus) _statusListener;
@override
void initState() {
super.initState();
_statusListener = (AnimationStatus status) {
if (status == AnimationStatus.completed ||
status == AnimationStatus.dismissed) {
_suggestionsBoxController.resize();
}
};
_animationController.addStatusListener(_statusListener);
}
@override
void dispose() {
_animationController.removeStatusListener(_statusListener);
_animationController.dispose();
super.dispose();
}
```

## Customizations
TypeAhead widgets consist of a TextField and a suggestion box that shows
as the user types. Both are highly customizable
Expand All @@ -164,7 +196,7 @@ which allows you to configure all the usual properties of `TextField`, like
`decoration`, `style`, `controller`, `focusNode`, `autofocus`, `enabled`,
etc.

### Customizing the Suggestions Box
### Customizing the suggestions box
TypeAhead provides default configurations for the suggestions box. You can,
however, override most of them. This is done by passing a `SuggestionsBoxDecoration`
to the `suggestionsBoxDecoration` property.
Expand All @@ -187,7 +219,7 @@ By default, the suggestions box will maintain the old suggestions while new
suggestions are being retrieved. To show a circular progress indicator
during retrieval instead, set `keepSuggestionsOnLoading` to false.

#### Hiding the Suggestions Box
#### Hiding the suggestions box
There are three scenarios when you can hide the suggestions box.

Set `hideOnLoading` to true to hide the box while suggestions are being
Expand Down Expand Up @@ -252,6 +284,11 @@ By default, the list grows towards the bottom. However, you can use the `directi

Set `autoFlipDirection` to true to allow the suggestions list to automatically flip direction whenever it detects that there is not enough space for the current direction. This is useful for scenarios where the TypeAheadField is in a scrollable widget or when the developer wants to ensure the list is always viewable despite different user screen sizes.

#### Controlling the suggestions box
Manual control of the suggestions box can be achieved by creating an instance of `SuggestionsBoxController` and
passing it to the `suggestionsBoxController` property. This will allow you to manually open, close, toggle, or
resize the suggestions box.

## For more information
Visit the [API Documentation](https://pub.dartlang.org/documentation/flutter_typeahead/latest/)

Expand Down
98 changes: 65 additions & 33 deletions lib/flutter_typeahead.dart
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ class TypeAheadFormField<T> extends FormField<String> {
Duration debounceDuration: const Duration(milliseconds: 300),
SuggestionsBoxDecoration suggestionsBoxDecoration:
const SuggestionsBoxDecoration(),
SuggestionsBoxController suggestionsBoxController,
@required SuggestionSelectionCallback<T> onSuggestionSelected,
@required ItemBuilder<T> itemBuilder,
@required SuggestionsCallback<T> suggestionsCallback,
Expand Down Expand Up @@ -304,6 +305,7 @@ class TypeAheadFormField<T> extends FormField<String> {
loadingBuilder: loadingBuilder,
debounceDuration: debounceDuration,
suggestionsBoxDecoration: suggestionsBoxDecoration,
suggestionsBoxController: suggestionsBoxController,
textFieldConfiguration: textFieldConfiguration.copyWith(
decoration: textFieldConfiguration.decoration
.copyWith(errorText: state.errorText),
Expand Down Expand Up @@ -474,6 +476,10 @@ class TypeAheadField<T> extends StatefulWidget {
/// If null, default decoration with an elevation of 4.0 is used
final SuggestionsBoxDecoration suggestionsBoxDecoration;

/// Used to control the `_SuggestionsBox`. Allows manual control to
/// open, close, toggle, or resize the `_SuggestionsBox`.
final SuggestionsBoxController suggestionsBoxController;

/// The duration to wait after the user stops typing before calling
/// [suggestionsCallback]
///
Expand Down Expand Up @@ -643,6 +649,7 @@ class TypeAheadField<T> extends StatefulWidget {
this.textFieldConfiguration: const TextFieldConfiguration(),
this.suggestionsBoxDecoration: const SuggestionsBoxDecoration(),
this.debounceDuration: const Duration(milliseconds: 300),
this.suggestionsBoxController,
this.loadingBuilder,
this.noItemsFoundBuilder,
this.errorBuilder,
Expand Down Expand Up @@ -681,7 +688,7 @@ class _TypeAheadFieldState<T> extends State<TypeAheadField<T>>
with WidgetsBindingObserver {
FocusNode _focusNode;
TextEditingController _textEditingController;
_SuggestionsBoxController _suggestionsBoxController;
_SuggestionsBox _suggestionsBox;

TextEditingController get _effectiveController =>
widget.textFieldConfiguration.controller ?? _textEditingController;
Expand All @@ -704,12 +711,12 @@ class _TypeAheadFieldState<T> extends State<TypeAheadField<T>>
@override
void didChangeMetrics() {
// Catch keyboard event and orientation change; resize suggestions list
this._suggestionsBoxController.onChangeMetrics();
this._suggestionsBox.onChangeMetrics();
}

@override
void dispose() {
this._suggestionsBoxController.widgetMounted = false;
this._suggestionsBox.widgetMounted = false;
WidgetsBinding.instance.removeObserver(this);
_keyboardVisibility.removeListener(_keyboardVisibilityId);
_effectiveFocusNode.removeListener(_focusNodeListener);
Expand All @@ -731,8 +738,9 @@ class _TypeAheadFieldState<T> extends State<TypeAheadField<T>>
this._focusNode = FocusNode();
}

this._suggestionsBoxController = _SuggestionsBoxController(
context, widget.direction, widget.autoFlipDirection);
this._suggestionsBox =
_SuggestionsBox(context, widget.direction, widget.autoFlipDirection);
widget.suggestionsBoxController?._suggestionsBox = this._suggestionsBox;

// hide suggestions box on keyboard closed
this._keyboardVisibilityId = _keyboardVisibility.addNewListener(
Expand All @@ -745,23 +753,23 @@ class _TypeAheadFieldState<T> extends State<TypeAheadField<T>>

this._focusNodeListener = () {
if (_effectiveFocusNode.hasFocus) {
this._suggestionsBoxController.open();
this._suggestionsBox.open();
} else {
this._suggestionsBoxController.close();
this._suggestionsBox.close();
}
};

WidgetsBinding.instance.addPostFrameCallback((duration) {
if (mounted) {
this._initOverlayEntry();
// calculate initial suggestions list size
this._suggestionsBoxController.resize();
this._suggestionsBox.resize();

this._effectiveFocusNode.addListener(_focusNodeListener);

// in case we already missed the focus event
if (this._effectiveFocusNode.hasFocus) {
this._suggestionsBoxController.open();
this._suggestionsBox.open();
}

ScrollableState scrollableState = Scrollable.of(context);
Expand All @@ -775,11 +783,11 @@ class _TypeAheadFieldState<T> extends State<TypeAheadField<T>>
// Scroll started
_resizeOnScrollTimer =
Timer.periodic(_resizeOnScrollRefreshRate, (timer) {
_suggestionsBoxController.resize();
_suggestionsBox.resize();
});
} else {
// Scroll finished
_suggestionsBoxController.resize();
_suggestionsBox.resize();
}
});
}
Expand All @@ -788,10 +796,9 @@ class _TypeAheadFieldState<T> extends State<TypeAheadField<T>>
}

void _initOverlayEntry() {
this._suggestionsBoxController._overlayEntry =
OverlayEntry(builder: (context) {
this._suggestionsBox._overlayEntry = OverlayEntry(builder: (context) {
final suggestionsList = _SuggestionsList<T>(
suggestionsBoxController: _suggestionsBoxController,
suggestionsBox: _suggestionsBox,
decoration: widget.suggestionsBoxDecoration,
debounceDuration: widget.debounceDuration,
controller: this._effectiveController,
Expand All @@ -805,17 +812,18 @@ class _TypeAheadFieldState<T> extends State<TypeAheadField<T>>
getImmediateSuggestions: widget.getImmediateSuggestions,
onSuggestionSelected: (T selection) {
this._effectiveFocusNode.unfocus();
this._suggestionsBox.close();
widget.onSuggestionSelected(selection);
},
itemBuilder: widget.itemBuilder,
direction: _suggestionsBoxController.direction,
direction: _suggestionsBox.direction,
hideOnLoading: widget.hideOnLoading,
hideOnEmpty: widget.hideOnEmpty,
hideOnError: widget.hideOnError,
keepSuggestionsOnLoading: widget.keepSuggestionsOnLoading,
);

double w = _suggestionsBoxController.textBoxWidth;
double w = _suggestionsBox.textBoxWidth;
if (widget.suggestionsBoxDecoration.constraints != null) {
if (widget.suggestionsBoxDecoration.constraints.minWidth != 0.0 &&
widget.suggestionsBoxDecoration.constraints.maxWidth !=
Expand All @@ -841,11 +849,11 @@ class _TypeAheadFieldState<T> extends State<TypeAheadField<T>>
showWhenUnlinked: false,
offset: Offset(
0.0,
_suggestionsBoxController.direction == AxisDirection.down
? _suggestionsBoxController.textBoxHeight +
_suggestionsBox.direction == AxisDirection.down
? _suggestionsBox.textBoxHeight +
widget.suggestionsBoxVerticalOffset
: _suggestionsBoxController.directionUpOffset),
child: _suggestionsBoxController.direction == AxisDirection.down
: _suggestionsBox.directionUpOffset),
child: _suggestionsBox.direction == AxisDirection.down
? suggestionsList
: FractionalTranslation(
translation:
Expand Down Expand Up @@ -893,7 +901,7 @@ class _TypeAheadFieldState<T> extends State<TypeAheadField<T>>
}

class _SuggestionsList<T> extends StatefulWidget {
final _SuggestionsBoxController suggestionsBoxController;
final _SuggestionsBox suggestionsBox;
final TextEditingController controller;
final bool getImmediateSuggestions;
final SuggestionSelectionCallback<T> onSuggestionSelected;
Expand All @@ -914,7 +922,7 @@ class _SuggestionsList<T> extends StatefulWidget {
final bool keepSuggestionsOnLoading;

_SuggestionsList({
@required this.suggestionsBoxController,
@required this.suggestionsBox,
this.controller,
this.getImmediateSuggestions: false,
this.onSuggestionSelected,
Expand Down Expand Up @@ -1081,11 +1089,11 @@ class _SuggestionsListState<T> extends State<_SuggestionsList<T>>
BoxConstraints constraints;
if (widget.decoration.constraints == null) {
constraints = BoxConstraints(
maxHeight: widget.suggestionsBoxController.maxHeight,
maxHeight: widget.suggestionsBox.maxHeight,
);
} else {
double maxHeight = min(widget.decoration.constraints.maxHeight,
widget.suggestionsBoxController.maxHeight);
widget.suggestionsBox.maxHeight);
constraints = widget.decoration.constraints.copyWith(
minHeight: min(widget.decoration.constraints.minHeight, maxHeight),
maxHeight: maxHeight,
Expand Down Expand Up @@ -1162,7 +1170,7 @@ class _SuggestionsListState<T> extends State<_SuggestionsList<T>>
padding: EdgeInsets.zero,
primary: false,
shrinkWrap: true,
reverse: widget.suggestionsBoxController.direction == AxisDirection.down
reverse: widget.suggestionsBox.direction == AxisDirection.down
? false
: true, // reverses the list to start at the bottom
children: this._suggestions.map((T suggestion) {
Expand Down Expand Up @@ -1465,7 +1473,7 @@ class TextFieldConfiguration<T> {
}
}

class _SuggestionsBoxController {
class _SuggestionsBox {
static const int waitMetricsTimeoutMillis = 1000;
static const double minOverlaySpace = 64.0;

Expand All @@ -1483,25 +1491,24 @@ class _SuggestionsBoxController {
double textBoxHeight = 100.0;
double directionUpOffset;

_SuggestionsBoxController(
this.context, this.direction, this.autoFlipDirection)
_SuggestionsBox(this.context, this.direction, this.autoFlipDirection)
: desiredDirection = direction;

open() {
void open() {
if (this._isOpened) return;
assert(this._overlayEntry != null);
Overlay.of(context).insert(this._overlayEntry);
this._isOpened = true;
}

close() {
void close() {
if (!this._isOpened) return;
assert(this._overlayEntry != null);
this._overlayEntry.remove();
this._isOpened = false;
}

toggle() {
void toggle() {
if (this._isOpened) {
this.close();
} else {
Expand Down Expand Up @@ -1536,8 +1543,9 @@ class _SuggestionsBoxController {
_findRootMediaQuery() != initialRootMediaQuery) {
return true;
}
await Future.delayed(const Duration(milliseconds: 10));
timer += 10;
// TODO: reduce delay if showDialog ever exposes detection of animation end
await Future.delayed(const Duration(milliseconds: 150));
timer += 150;
}
}

Expand Down Expand Up @@ -1667,3 +1675,27 @@ class _SuggestionsBoxController {
}
}
}

class SuggestionsBoxController {
_SuggestionsBox _suggestionsBox;

/// Opens the suggestions box
void open() {
_suggestionsBox?.open();
}

/// Closes the suggestions box
void close() {
_suggestionsBox?.close();
}

/// Opens the suggestions box if closed and vice-versa
void toggle() {
_suggestionsBox?.toggle();
}

/// Recalculates the height of the suggestions box
void resize() {
_suggestionsBox?.resize();
}
}
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: flutter_typeahead
version: 1.4.1
version: 1.5.0

description: A highly customizable typeahead (autocomplete) text input field for Flutter
homepage: https://github.com/AbdulRahmanAlHamali/flutter_typeahead
Expand Down

0 comments on commit 2827518

Please sign in to comment.