Atualizado dependencias do packege
26
.gitignore
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
.DS_Store
|
||||
.dart_tool/
|
||||
|
||||
.packages
|
||||
.pub/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
|
||||
build/
|
||||
example/.flutter-plugins*
|
||||
ios/.generated/
|
||||
ios/Flutter/Generated.xcconfig
|
||||
ios/Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
.idea
|
||||
pubspec.lock
|
||||
flutter_typeahead
|
||||
|
||||
doc
|
||||
|
||||
example/android/gradle/wrapper/gradle-wrapper.jar
|
||||
example/android/gradlew
|
||||
example/android/gradlew.bat
|
||||
example/example.iml
|
||||
example/ios/Flutter/flutter_export_environment.sh
|
||||
example/pubspec.lock`
|
326
CHANGELOG.md
Normal file
@ -0,0 +1,326 @@
|
||||
## 4.3.7 - 26-February-2023
|
||||
|
||||
Update Changelog file
|
||||
|
||||
## 4.3.6 - 18-February-2023
|
||||
Fixed visibility of cupertino decoration and formated code
|
||||
|
||||
## 4.3.5 - 17-February-2023
|
||||
Fix in visibility of suggestions box classes
|
||||
|
||||
## 4.3.4 - 16-February-2023
|
||||
Improved the main example to be able to read it in pub dev,
|
||||
|
||||
## 4.3.3 - 1-Feburary-2023
|
||||
|
||||
-- Apply PR to fix onSelected issue introduced in Flutter 3.7.0
|
||||
|
||||
## 4.3.2 - 28-January-2023
|
||||
|
||||
-- Update sdk level to 2.19.0 for Flutter 3.7.0
|
||||
|
||||
## 4.3.1 - 28-January-2023
|
||||
|
||||
-- PR #447 - fix: Use maybeOf for scrollable to not throw an exception in flutter …
|
||||
|
||||
## 4.3.0 - 15-November-2022
|
||||
|
||||
-- PR #436 - Added onReset callback to TypeAheadFormField
|
||||
-- PR #435 - Block up and down keys
|
||||
|
||||
## 4.2.0 - 27-October-2022
|
||||
|
||||
-- PR #432 - Fix suggestions box behavior on web
|
||||
-- PR #431 - Add autoFlipListDirection option
|
||||
|
||||
## 4.1.1 - 17-September-2022
|
||||
|
||||
-- PR #428 - Fix some issues with web / arrows etc.
|
||||
|
||||
## 4.1.0 - 5-September-2022
|
||||
|
||||
-- PR #409 - Add null safety to suggestions box controller
|
||||
-- PR #417 - Improved support for VoiceOver/TalkBack
|
||||
-- PR #418 - Feat/key up down
|
||||
-- PR #422 - Adding hideKeyboardOnDrag option
|
||||
-- PR #424 - Updated README: added pub.dev shield
|
||||
|
||||
## 4.0.0 - 15-May-2022
|
||||
|
||||
-- PR #395 - Fix Flutter 3.0 warnings
|
||||
|
||||
## 3.2.7 - 15-May-2022
|
||||
|
||||
-- REVERT PR #395 - Fix Flutter 3.0 warnings
|
||||
|
||||
## 3.2.6 - 15-May-2022
|
||||
|
||||
-- PR #393 - Hide suggestion on close issue fix
|
||||
-- PR #395 - Fix Flutter 3.0 warnings
|
||||
|
||||
## 3.2.5 - 18-Apr-2022
|
||||
|
||||
-- PR #383 - Fix maxLengthEnforced deprecation
|
||||
|
||||
## 3.2.4 - 9-Dec-2021
|
||||
|
||||
-- PR #360 - Resize before opening box (fixes issue 220)
|
||||
|
||||
## 3.2.3 - 21-Nov-2021
|
||||
|
||||
-- PR #354 - Make maxLines nullable
|
||||
-- PR #349 - Add option for min number of chars before suggestionsCallback is called
|
||||
-- PR #347 - Un private some variables
|
||||
-- PR #347 - Fix deprecated info's in example
|
||||
-- PR #344 - Add textAlignVertical support, fixes #231
|
||||
|
||||
## 3.2.2 - 20-Aug-2021
|
||||
|
||||
-- PR #333 - support Windows and MacOS by making keyboard_visibility optional
|
||||
|
||||
## 3.2.1 - 10-Sept-2021
|
||||
|
||||
-- PR #327 - Added Scrollcontroler as optional parameter
|
||||
-- PR #335 - Fix ErrorBuilder Widget display
|
||||
-- PR #339 - Strong mode and type fixes
|
||||
|
||||
## 3.2.0 - 9-Jul-2021
|
||||
|
||||
-- PR #326 - file structure reorganisation
|
||||
-- PR #325 - Fix Scrollbar ScrollController
|
||||
|
||||
## 3.1.3 - 7-May-2021
|
||||
|
||||
-- PR #308 - Allow suggestionsCallback to return null
|
||||
|
||||
## 3.1.2 - 1-May-2021
|
||||
|
||||
-- PR #303 - Guard against missing size in \_adjustMaxHeightAndOrientation
|
||||
-- PR #306 - Fixed Issue #286 - Suggestions callback called immediately
|
||||
|
||||
## 3.1.1 - 28-March-2021
|
||||
|
||||
-- PR #297 - Fix styling of CHANGELOG.md
|
||||
|
||||
## 3.1.0 - 21-March-2021
|
||||
|
||||
-- PR #295 - autoFillHints for TextFieldConfiguration
|
||||
-- PR #294 - Check if the overlay is open
|
||||
-- PR #292 - Various bug fixes, including null safety
|
||||
-- PR #291 - Check for platform and run the correct example demo
|
||||
-- PR #287 - Cancel the debounce timer when widget is destroyed
|
||||
-- PR #285 - Fix possible race condition by doing an await
|
||||
|
||||
## 3.0.0-nullsafety.0 - 9-Feburary-2021
|
||||
|
||||
-- PR #290 Null-safety pre-release
|
||||
|
||||
## 2.0.0 - 11-January-2021
|
||||
|
||||
-- NOTE!! BREAKING CHANGE! Not major but changed Types for some calls
|
||||
-- RE-APPLY : 1.9.2 will become 2.0.0
|
||||
|
||||
## 1.9.3 - 10-January-2021
|
||||
|
||||
-- REVERT : Reverting back to 1.9.1 as 1.9.3. 1.9.2 will become 2.0.0
|
||||
|
||||
## 1.9.2 - 06-January-2021
|
||||
|
||||
-- #267 : Remove undeeded typecasts and add String types
|
||||
|
||||
## 1.9.1 - 03-December-2020
|
||||
|
||||
-- #256 : Change default to disabled for autovalidateMode and fix typo
|
||||
|
||||
## 1.9.0 - 01-December-2020
|
||||
|
||||
- Merged 4 PRs:
|
||||
-- #238 : Added hideKeyboard even if textfield has focus (edge case)
|
||||
-- #248 : Added enabled and autovalidateMode properties. Resolves Issue #247
|
||||
-- #249 : Added enableSuggestions to Textfield Configuration. Resolves Issue #210
|
||||
-- #255 : Update to use flutter_keyboard_visibility 4.X from 3.X
|
||||
|
||||
## 1.8.8 - 12-August-2020
|
||||
|
||||
- Merged PR to fix typo and validator tests and examples using deprecated consts.
|
||||
|
||||
## 1.8.7 - 30-July-2020
|
||||
|
||||
- Merged PR to fix dispose() error in tests.
|
||||
|
||||
## 1.8.6 - 05/07/2020
|
||||
|
||||
- Merged PR to fix "flashing" bug.
|
||||
|
||||
## 1.8.5 - 01/07/2020
|
||||
|
||||
- Dependency Update : Updated flutter_Keyboard_visibility to ^3.0.0 from ^2.0.0
|
||||
|
||||
## 1.8.4 - 30/06/2020
|
||||
|
||||
- Bug Fix : Merged 3 PRs for various bug fixes.
|
||||
|
||||
## 1.8.3 - 11/06/2020
|
||||
|
||||
- Bug Fix : PR to address keyboard visibility issues from @alphamikle
|
||||
|
||||
## 1.8.1 - 08/04/2020
|
||||
|
||||
- Bug fixes
|
||||
|
||||
## 1.8.0 - 23/01/2020
|
||||
|
||||
- Change from List to Iterable for flexibility
|
||||
- Added `onTap` property to `TextFieldConfiguration`
|
||||
- Added `offsetX` property to `SuggestionsBoxDecoration` and `CupertinoSuggestionsBoxDecoration`
|
||||
- Support iOS 13 dark mode
|
||||
- Bug fixes
|
||||
|
||||
## 1.7.0 - 16/10/2019
|
||||
|
||||
- Updated keyboard_visibility dependency
|
||||
- Scolling bug fix
|
||||
- Added new property `enableInteractiveSelection`
|
||||
- Fix disposing overlay
|
||||
|
||||
Thanks to MisterJimson, davidmartos96, pparadox11, diegoveloper
|
||||
|
||||
## 1.6.1 - 05/06/2019
|
||||
|
||||
- Fixed onChanged not being called for TypeAheadFormField
|
||||
|
||||
## 1.6.0 - 19/05/2019
|
||||
|
||||
- Added CupertinoTypeAheadField for Cupertino users
|
||||
- Updated example project
|
||||
- Bug fixes
|
||||
|
||||
## 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
|
||||
- Add `textDirection` property to `TextFieldConfiguration`
|
||||
|
||||
## 1.4.1 - 09/04/2019
|
||||
|
||||
- Fixed BoxConstraints width parameters being ignored in `SuggestionsBoxDecoration`
|
||||
|
||||
## 1.4.0 - 26/03/2019
|
||||
|
||||
- Added property `autoFlipDirection` to allow automatic direction flipping if
|
||||
there is not enough space for the suggestions list
|
||||
|
||||
## 1.3.0 - 19/03/2019
|
||||
|
||||
- Limit number of suggestionsCallbacks until current call is finished
|
||||
|
||||
## 1.2.1 - 19/03/2019
|
||||
|
||||
- Bug fixes & optimizations
|
||||
|
||||
## 1.2.0 - 05/03/2019
|
||||
|
||||
- Added property `keepSuggestionsOnLoading`
|
||||
- Changed default behavior: suggestions box will no longer
|
||||
show circular progress indicator when loading; it will maintain previous results if available
|
||||
|
||||
## 1.1.0 - 01/03/2019
|
||||
|
||||
- Suggestions box now closes on keyboard hide by default
|
||||
- Added property `hideSuggestionsOnKeyboardHide`
|
||||
- Width now properly resizes on orientation changes
|
||||
- Suggestions box will display above keyboard when keyboard hides the box for AxisDirection.Up
|
||||
- Fix FocusNode errors
|
||||
- Fix keyboard height calculation
|
||||
|
||||
## 1.0.4/5 - 21/02/2019
|
||||
|
||||
- Fix suggestions being called on TextBox focus
|
||||
|
||||
## 1.0.3 - 12/02/2019
|
||||
|
||||
- Resize suggestion box when scrolling
|
||||
|
||||
## 1.0.2 - 07/02/2019
|
||||
|
||||
- Bug fix for `maxHeight` property
|
||||
|
||||
## 1.0.1 - 06/02/2019
|
||||
|
||||
- Added properties `hideOnLoading`, `hideOnEmpty`, and `hideOnError` to hide the suggestions box
|
||||
|
||||
## 0.6.1 - 26/01/2019
|
||||
|
||||
- Allow types <T> to properly work.
|
||||
- Add documentation for direction: option.
|
||||
|
||||
## 0.6.0 - 23/01/2019
|
||||
|
||||
- Added property `direction` to allow the suggestions to grow either up or down
|
||||
|
||||
## 0.5.2 - 19/01/2019
|
||||
|
||||
- Added contributing guidelines and reverse sorted the CHANGLELOG.md file.
|
||||
|
||||
## 0.5.1 - 10/01/2019
|
||||
|
||||
- Bug fixes
|
||||
|
||||
## 0.5.0 - 05/01/2019
|
||||
|
||||
- Added the hasScrollbar property which allows the optional display of a `Scrollbar`
|
||||
- Fixed the case where the suggestion box becomes hidden behind the keyboard
|
||||
- Fixed the bug of not disposing the animations controller
|
||||
|
||||
## 0.4.1 - 20/09/2018
|
||||
|
||||
- Added property `getImmediateSuggestions` to the form field implementation
|
||||
|
||||
## 0.4.0 - 20/09/2018
|
||||
|
||||
- Added property `getImmediateSuggestions` to allow fetching
|
||||
suggestions before the user types
|
||||
- Added assertion in the form field to disallow having `initialValue`
|
||||
and `textFieldConfiguration.controller` defined at the same time
|
||||
|
||||
## 0.3.0 - 15/09/2018
|
||||
|
||||
- Added a constraints property to the `SuggestionsBoxDecorations`
|
||||
which allows to set the height and width of the suggestions box
|
||||
|
||||
## 0.2.1 - 04/09/2018
|
||||
|
||||
- Added mention of 'autocomplete' in README and pubspec
|
||||
- Executed 'flutter format'
|
||||
|
||||
## 0.2.0 - 02/09/2018
|
||||
|
||||
- Changed the suggestions box decoration
|
||||
to decorate a material sheet instead of
|
||||
decorating a container
|
||||
- Moved the `TextField` properties inside a class
|
||||
called `TextFieldConfiguration`, which is provided
|
||||
to the `TypeAhead` widgets through a
|
||||
`textFieldConfiguration` property. This was done to
|
||||
decrease the clutter in the interface
|
||||
- Added more configuration properties to the
|
||||
`TextField`
|
||||
- Added a configurable vertical offset for the
|
||||
suggestions box
|
||||
- Changed the mechanism used to open/close the suggestions box
|
||||
- Added meta-tags to README for SEO
|
||||
- Updated the GIF to show the changes
|
||||
- Added "How you can help" section to README
|
||||
|
||||
## 0.1.2 - 31/08/2018
|
||||
|
||||
- Small fix to README
|
||||
|
||||
## 0.1.1 - 31/08/2018
|
||||
|
||||
- Fixed CHANGELOG.
|
||||
- Small fix to documentation
|
||||
|
||||
## 0.1.0 - 31/08/2018
|
||||
|
||||
- Initial Release.
|
40
CONTRIBUTING.md
Normal file
@ -0,0 +1,40 @@
|
||||
# Contributing to TypeAheadField
|
||||
|
||||
## Create a new issue
|
||||
|
||||
The easiest way to get involved is to create a [new issue](https://github.com/AbdulRahmanAlHamali/flutter_typeahead/issues/new) when you spot a bug, if the documentation is incomplete or out of date, or if you identify an implementation problem.
|
||||
|
||||
## Modifying Code
|
||||
|
||||
If you plan to submit a PR please do the following:
|
||||
|
||||
- Fork the repository
|
||||
- Create a feature branch from the **master** branch!
|
||||
- Following the coding guidlines below
|
||||
- Submit the PR against the **master** branch.
|
||||
- Update both Material and Cupertino code if applicable
|
||||
|
||||
## General coding guidlines
|
||||
|
||||
If you'd like to add a feature or fix a bug, we're more than happy to accept pull requests! We only ask a few things:
|
||||
|
||||
- Ensure your code contains no analyzer errors, e.g.
|
||||
- Code is strong-mode compliant
|
||||
- Code is free of lint errors
|
||||
- Format your code with `dartfmt`
|
||||
- Write helpful documentation
|
||||
- Write new tests that cover your code base changes
|
||||
- Make sure all current tests pass
|
||||
- If you would like to make a bigger / fundamental change to the codebase, please file a lightweight example PR / issue.
|
||||
|
||||
## Before Uploading
|
||||
|
||||
If you are an uploader here are some additional steps before publishing.
|
||||
|
||||
- Update the version:
|
||||
- if it is only a bug fix, increase the last digit.
|
||||
- if it provides a new feature, increase the middle digit.
|
||||
- if it has breaking changes, you should increase the first digit.
|
||||
- Then, mention all the changes in the CHANGELOG.md.
|
||||
- Merge into master.
|
||||
- Upload to Pub Dart.
|
25
LICENSE
Normal file
@ -0,0 +1,25 @@
|
||||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2018, AbdulRahmanAlHamali
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
327
README.md
Normal file
@ -0,0 +1,327 @@
|
||||
<meta name='keywords' content='flutter, typeahead, autocomplete, customizable, floating'>
|
||||
|
||||
[](https://pub.dev/packages/flutter_typeahead)
|
||||
|
||||
# Flutter TypeAhead
|
||||
A TypeAhead (autocomplete) widget for Flutter, where you can show suggestions to
|
||||
users as they type
|
||||
|
||||
<img src="https://raw.githubusercontent.com/AbdulRahmanAlHamali/flutter_typeahead/master/flutter_typeahead.gif">
|
||||
|
||||
## Features
|
||||
* Shows suggestions in an overlay that floats on top of other widgets
|
||||
* Allows you to specify what the suggestions will look like through a
|
||||
builder function
|
||||
* Allows you to specify what happens when the user taps a suggestion
|
||||
* Accepts all the parameters that traditional TextFields accept, like
|
||||
decoration, custom TextEditingController, text styling, etc.
|
||||
* Provides two versions, a normal version and a [FormField](https://docs.flutter.io/flutter/widgets/FormField-class.html)
|
||||
version that accepts validation, submitting, etc.
|
||||
* Provides high customizable; you can customize the suggestion box decoration,
|
||||
the loading bar, the animation, the debounce duration, etc.
|
||||
|
||||
## Installation
|
||||
See the [installation instructions on pub](https://pub.dartlang.org/packages/flutter_typeahead#-installing-tab-).
|
||||
|
||||
Note: As for Typeahead 3.X this package is based on Dart 2.12 (null-safety). You may also want to explore the new built in Flutter 2 widgets that have similar behavior.
|
||||
|
||||
## Usage examples
|
||||
You can import the package with:
|
||||
```dart
|
||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||
```
|
||||
|
||||
For Cupertino users import:
|
||||
```dart
|
||||
import 'package:flutter_typeahead/cupertino_flutter_typeahead.dart';
|
||||
```
|
||||
|
||||
Use it as follows:
|
||||
|
||||
### Material Example 1:
|
||||
```dart
|
||||
TypeAheadField(
|
||||
textFieldConfiguration: TextFieldConfiguration(
|
||||
autofocus: true,
|
||||
style: DefaultTextStyle.of(context).style.copyWith(
|
||||
fontStyle: FontStyle.italic
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder()
|
||||
)
|
||||
),
|
||||
suggestionsCallback: (pattern) async {
|
||||
return await BackendService.getSuggestions(pattern);
|
||||
},
|
||||
itemBuilder: (context, suggestion) {
|
||||
return ListTile(
|
||||
leading: Icon(Icons.shopping_cart),
|
||||
title: Text(suggestion['name']),
|
||||
subtitle: Text('\$${suggestion['price']}'),
|
||||
);
|
||||
},
|
||||
onSuggestionSelected: (suggestion) {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => ProductPage(product: suggestion)
|
||||
));
|
||||
},
|
||||
)
|
||||
```
|
||||
In the code above, the `textFieldConfiguration` property allows us to
|
||||
configure the displayed `TextField` as we want. In this example, we are
|
||||
configuring the `autofocus`, `style` and `decoration` properties.
|
||||
|
||||
The `suggestionsCallback` is called with the search string that the user
|
||||
types, and is expected to return a `List` of data either synchronously or
|
||||
asynchronously. In this example, we are calling an asynchronous function
|
||||
called `BackendService.getSuggestions` which fetches the list of
|
||||
suggestions.
|
||||
|
||||
The `itemBuilder` is called to build a widget for each suggestion.
|
||||
In this example, we build a simple `ListTile` that shows the name and the
|
||||
price of the item. Please note that you shouldn't provide an `onTap`
|
||||
callback here. The TypeAhead widget takes care of that.
|
||||
|
||||
The `onSuggestionSelected` is a callback called when the user taps a
|
||||
suggestion. In this example, when the user taps a
|
||||
suggestion, we navigate to a page that shows us the information of the
|
||||
tapped product.
|
||||
|
||||
### Material Example 2:
|
||||
Here's another example, where we use the TypeAheadFormField inside a `Form`:
|
||||
```dart
|
||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||
final TextEditingController _typeAheadController = TextEditingController();
|
||||
String _selectedCity;
|
||||
...
|
||||
Form(
|
||||
key: this._formKey,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'What is your favorite city?'
|
||||
),
|
||||
TypeAheadFormField(
|
||||
textFieldConfiguration: TextFieldConfiguration(
|
||||
controller: this._typeAheadController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'City'
|
||||
)
|
||||
),
|
||||
suggestionsCallback: (pattern) {
|
||||
return CitiesService.getSuggestions(pattern);
|
||||
},
|
||||
itemBuilder: (context, suggestion) {
|
||||
return ListTile(
|
||||
title: Text(suggestion),
|
||||
);
|
||||
},
|
||||
transitionBuilder: (context, suggestionsBox, controller) {
|
||||
return suggestionsBox;
|
||||
},
|
||||
onSuggestionSelected: (suggestion) {
|
||||
this._typeAheadController.text = suggestion;
|
||||
},
|
||||
validator: (value) {
|
||||
if (value.isEmpty) {
|
||||
return 'Please select a city';
|
||||
}
|
||||
},
|
||||
onSaved: (value) => this._selectedCity = value,
|
||||
),
|
||||
SizedBox(height: 10.0,),
|
||||
RaisedButton(
|
||||
child: Text('Submit'),
|
||||
onPressed: () {
|
||||
if (this._formKey.currentState.validate()) {
|
||||
this._formKey.currentState.save();
|
||||
Scaffold.of(context).showSnackBar(SnackBar(
|
||||
content: Text('Your Favorite City is ${this._selectedCity}')
|
||||
));
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
Here, we assign to the `controller` property of the `textFieldConfiguration`
|
||||
a `TextEditingController` that we call `_typeAheadController`.
|
||||
We use this controller in the `onSuggestionSelected` callback to set the
|
||||
value of the `TextField` to the selected suggestion.
|
||||
|
||||
The `validator` callback can be used like any `FormField.validator`
|
||||
function. In our example, it checks whether a value has been entered,
|
||||
and displays an error message if not. The `onSaved` callback is used to
|
||||
save the value of the field to the `_selectedCity` member variable.
|
||||
|
||||
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.
|
||||
|
||||
### Cupertino Example:
|
||||
Please see the Cupertino code in the example project.
|
||||
|
||||
## Known Issues
|
||||
|
||||
### 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();
|
||||
}
|
||||
```
|
||||
|
||||
#### Dialogs
|
||||
There is a known issue with opening dialogs where the suggestions box will sometimes appear too small. This is a timing issue caused by the animations described above. Currently, `showDialog` has a duration of 150 ms for the animations. TypeAheadField has a delay of 170 ms to compensate for this. Until the end of the animation can be properly detected and fixed using the solution above, this temporary fix will work most of the time. If the suggestions box is too small, closing and reopening the keyboard will usually fix the issue.
|
||||
|
||||
### Cupertino
|
||||
The Cupertino classes in TypeAhead are still new. There are also differences in the Cupertino widgets vs the Material ones. Some behavior will not translate when moving between the two.
|
||||
|
||||
## Customizations
|
||||
TypeAhead widgets consist of a TextField and a suggestion box that shows
|
||||
as the user types. Both are highly customizable
|
||||
|
||||
### Customizing the TextField
|
||||
You can customize the text field using the `textFieldConfiguration` property.
|
||||
You provide this property with an instance of `TextFieldConfiguration`,
|
||||
which allows you to configure all the usual properties of `TextField`, like
|
||||
`decoration`, `style`, `controller`, `focusNode`, `autofocus`, `enabled`,
|
||||
etc.
|
||||
|
||||
### 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.
|
||||
|
||||
Use the `offsetX` property in `SuggestionsBoxDecoration` to shift the suggestions box along the x-axis.
|
||||
You may also pass BoxConstraints to `constraints` in `SuggestionsBoxDecoration` to adjust the width
|
||||
and height of the suggestions box. Using the two together will allow the suggestions box to be placed
|
||||
almost anywhere.
|
||||
|
||||
#### Customizing the loader, the error and the "no items found" message
|
||||
You can use the `loadingBuilder`, `errorBuilder` and `noItemsFoundBuilder` to
|
||||
customize their corresponding widgets. For example, to show a custom error
|
||||
widget:
|
||||
```dart
|
||||
errorBuilder: (BuildContext context, Object error) =>
|
||||
Text(
|
||||
'$error',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).errorColor
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
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
|
||||
There are three scenarios when you can hide the suggestions box.
|
||||
|
||||
Set `hideOnLoading` to true to hide the box while suggestions are being
|
||||
retrieved. This will also ignore the `loadingBuilder`. Set `hideOnEmpty`
|
||||
to true to hide the box when there are no suggestions. This will also ignore
|
||||
the `noItemsFoundBuilder`. Set `hideOnError` to true to hide the box when there
|
||||
is an error retrieving suggestions. This will also ignore the `errorBuilder`.
|
||||
|
||||
By default, the suggestions box will automatically hide when the keyboard is hidden.
|
||||
To change this behavior, set `hideSuggestionsOnKeyboardHide` to false.
|
||||
|
||||
#### Customizing the animation
|
||||
You can customize the suggestion box animation through 3 parameters: the
|
||||
`animationDuration`, the `animationStart`, and the `transitionBuilder`.
|
||||
|
||||
The `animationDuration` specifies how long the animation should take, while the
|
||||
`animationStart` specified what point (between 0.0 and 1.0) the animation
|
||||
should start from. The `transitionBuilder` accepts the `suggestionsBox` and
|
||||
`animationController` as parameters, and should return a widget that uses
|
||||
the `animationController` to animate the display of the `suggestionsBox`.
|
||||
For example:
|
||||
```dart
|
||||
transitionBuilder: (context, suggestionsBox, animationController) =>
|
||||
FadeTransition(
|
||||
child: suggestionsBox,
|
||||
opacity: CurvedAnimation(
|
||||
parent: animationController,
|
||||
curve: Curves.fastOutSlowIn
|
||||
),
|
||||
)
|
||||
```
|
||||
This uses [FadeTransition](https://docs.flutter.io/flutter/widgets/FadeTransition-class.html)
|
||||
to fade the `suggestionsBox` into the view. Note how the
|
||||
`animationController` was provided as the parent of the animation.
|
||||
|
||||
In order to fully remove the animation, `transitionBuilder` should simply
|
||||
return the `suggestionsBox`. This callback could also be used to wrap the
|
||||
`suggestionsBox` with any desired widgets, not necessarily for animation.
|
||||
|
||||
#### Customizing the debounce duration
|
||||
The suggestions box does not fire for each character the user types. Instead,
|
||||
we wait until the user is idle for a duration of time, and then call the
|
||||
`suggestionsCallback`. The duration defaults to 300 milliseconds, but can be
|
||||
configured using the `debounceDuration` parameter.
|
||||
|
||||
#### Customizing the offset of the suggestions box
|
||||
By default, the suggestions box is displayed 5 pixels below the `TextField`.
|
||||
You can change this by changing the `suggestionsBoxVerticalOffset` property.
|
||||
|
||||
#### Customizing the decoration of the suggestions box
|
||||
You can also customize the decoration of the suggestions box using the
|
||||
`suggestionsBoxDecoration` property. For example, to remove the elevation
|
||||
of the suggestions box, you can write:
|
||||
```dart
|
||||
suggestionsBoxDecoration: SuggestionsBoxDecoration(
|
||||
elevation: 0.0
|
||||
)
|
||||
```
|
||||
|
||||
#### Customizing the growth direction of the suggestions list
|
||||
By default, the list grows towards the bottom. However, you can use the `direction` property to customize the growth direction to be one of `AxisDirection.down` or `AxisDirection.up`, the latter of which will cause the list to grow up, where the first suggestion is at the bottom of the list, and the last suggestion is at the top.
|
||||
|
||||
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/)
|
||||
|
||||
## Team:
|
||||
| [<img src="https://avatars.githubusercontent.com/u/16646600?v=3" width="100px;"/>](https://github.com/AbdulRahmanAlHamali)|[<img src="https://avatars.githubusercontent.com/u/2034925?v=3" width="100px;"/>](https://github.com/sjmcdowall)|[<img src="https://avatars.githubusercontent.com/u/5499214?v=3" width="100px;"/>](https://github.com/KaYBlitZ)|
|
||||
|---|---|---|
|
||||
|AbdulRahman AlHamali|S McDowall|Kenneth Liang|
|
||||
|
||||
## Shout out to the contributors!
|
||||
This project is the result of the collective effort of contributors who participated effectively by submitting pull requests, reporting issues, and answering questions. Thank you for your proactiveness, and we hope flutter_typeahead made your lifes at least a little easier!
|
||||
|
||||
## How you can help
|
||||
[Contribution Guidelines](https://github.com/AbdulRahmanAlHamali/flutter_typeahead/blob/master/CONTRIBUTING.md)
|
4
analysis_options.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
analyzer:
|
||||
strong-mode:
|
||||
implicit-casts: false
|
||||
implicit-dynamic: false
|
46
example/.gitignore
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.packages
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
# Web related
|
||||
lib/generated_plugin_registrant.dart
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
30
example/.metadata
Normal file
@ -0,0 +1,30 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled.
|
||||
|
||||
version:
|
||||
revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||
channel: stable
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||
- platform: web
|
||||
create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||
base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
16
example/README.md
Normal file
@ -0,0 +1,16 @@
|
||||
# example
|
||||
|
||||
A new Flutter project.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
|
||||
- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
|
||||
- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
|
||||
|
||||
For help getting started with Flutter, view our
|
||||
[online documentation](https://flutter.dev/docs), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
11
example/android/.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
|
||||
key.properties
|
59
example/android/app/build.gradle
Normal file
@ -0,0 +1,59 @@
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||
localProperties.load(reader)
|
||||
}
|
||||
}
|
||||
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
if (flutterRoot == null) {
|
||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '1'
|
||||
}
|
||||
|
||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||
if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "com.example.example"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 31
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source '../..'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
}
|
7
example/android/app/src/debug/AndroidManifest.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.example">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
42
example/android/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,42 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.example">
|
||||
<application
|
||||
android:label="example"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="true">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<!-- Displays an Android View that continues showing the launch screen
|
||||
Drawable until Flutter paints its first frame, then this splash
|
||||
screen fades out. A splash screen is useful to avoid any visual
|
||||
gap between the end of Android's launch screen and the painting of
|
||||
Flutter's first frame. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.SplashScreenDrawable"
|
||||
android:resource="@drawable/launch_background"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
</manifest>
|
@ -0,0 +1,6 @@
|
||||
package com.example.example
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
BIN
example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 544 B |
BIN
example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 442 B |
BIN
example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 721 B |
BIN
example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
18
example/android/app/src/main/res/values-night/styles.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
18
example/android/app/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
7
example/android/app/src/profile/AndroidManifest.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.example">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
31
example/android/build.gradle
Normal file
@ -0,0 +1,31 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.7.10'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.1.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.buildDir = '../build'
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
3
example/android/gradle.properties
Normal file
@ -0,0 +1,3 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
6
example/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
#Fri Jun 23 08:50:38 CEST 2017
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
|
11
example/android/settings.gradle
Normal file
@ -0,0 +1,11 @@
|
||||
include ':app'
|
||||
|
||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||
def properties = new Properties()
|
||||
|
||||
assert localPropertiesFile.exists()
|
||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
32
example/ios/.gitignore
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
*.perspectivev3
|
||||
**/*sync/
|
||||
.sconsign.dblite
|
||||
.tags*
|
||||
**/.vagrant/
|
||||
**/DerivedData/
|
||||
Icon?
|
||||
**/Pods/
|
||||
**/.symlinks/
|
||||
profile
|
||||
xcuserdata
|
||||
**/.generated/
|
||||
Flutter/App.framework
|
||||
Flutter/Flutter.framework
|
||||
Flutter/Flutter.podspec
|
||||
Flutter/Generated.xcconfig
|
||||
Flutter/app.flx
|
||||
Flutter/app.zip
|
||||
Flutter/flutter_assets/
|
||||
Flutter/flutter_export_environment.sh
|
||||
ServiceDefinitions.json
|
||||
Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
# Exceptions to above rules.
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
!default.pbxuser
|
||||
!default.perspectivev3
|
26
example/ios/Flutter/AppFrameworkInfo.plist
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>App</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>io.flutter.flutter.app</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>App</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>11.0</string>
|
||||
</dict>
|
||||
</plist>
|
2
example/ios/Flutter/Debug.xcconfig
Normal file
@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
2
example/ios/Flutter/Release.xcconfig
Normal file
@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
41
example/ios/Podfile
Normal file
@ -0,0 +1,41 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '11.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
'Profile' => :release,
|
||||
'Release' => :release,
|
||||
}
|
||||
|
||||
def flutter_root
|
||||
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
|
||||
unless File.exist?(generated_xcode_build_settings_path)
|
||||
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
||||
end
|
||||
|
||||
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||
return matches[1].strip if matches
|
||||
end
|
||||
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
|
||||
end
|
||||
|
||||
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||
|
||||
flutter_ios_podfile_setup
|
||||
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
use_modular_headers!
|
||||
|
||||
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
end
|
||||
end
|
22
example/ios/Podfile.lock
Normal file
@ -0,0 +1,22 @@
|
||||
PODS:
|
||||
- Flutter (1.0.0)
|
||||
- flutter_keyboard_visibility (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_keyboard_visibility:
|
||||
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
||||
|
||||
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
|
||||
|
||||
COCOAPODS: 1.11.3
|
550
example/ios/Runner.xcodeproj/project.pbxproj
Normal file
@ -0,0 +1,550 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
0AA0FD8F2BCB1D02A381FA2C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 070C64CE3E3829B4B69F6D65 /* Pods_Runner.framework */; };
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
05D429632D8FD2830C666F9D /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
070C64CE3E3829B4B69F6D65 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
5AF13D361E4357D2A725E8B9 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
D80F953CFD9EF627D227F3B1 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
0AA0FD8F2BCB1D02A381FA2C /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
78354FCE6CD6437F846D905F /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
070C64CE3E3829B4B69F6D65 /* Pods_Runner.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||
);
|
||||
name = Flutter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146E51CF9000F007C117D = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
DEA2152EEB1D8575D5DFDC3C /* Pods */,
|
||||
78354FCE6CD6437F846D905F /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146EF1CF9000F007C117D /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
97C147021CF9000F007C117D /* Info.plist */,
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DEA2152EEB1D8575D5DFDC3C /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D80F953CFD9EF627D227F3B1 /* Pods-Runner.debug.xcconfig */,
|
||||
5AF13D361E4357D2A725E8B9 /* Pods-Runner.release.xcconfig */,
|
||||
05D429632D8FD2830C666F9D /* Pods-Runner.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
23424392682879E6F903E036 /* [CP] Check Pods Manifest.lock */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
B873A209970BF6D2B0E8E9D1 /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1300;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
LastSwiftMigration = 1100;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
97C146ED1CF9000F007C117D /* Runner */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
23424392682879E6F903E036 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
B873A209970BF6D2B0E8E9D1 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C146FB1CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C147001CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
97C147031CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147041CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
97C147061CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147071CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147031CF9000F007C117D /* Debug */,
|
||||
97C147041CF9000F007C117D /* Release */,
|
||||
249021D3217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147061CF9000F007C117D /* Debug */,
|
||||
97C147071CF9000F007C117D /* Release */,
|
||||
249021D4217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
}
|
7
example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1300"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
10
example/ios/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
13
example/ios/Runner/AppDelegate.swift
Normal file
@ -0,0 +1,13 @@
|
||||
import UIKit
|
||||
import Flutter
|
||||
|
||||
@UIApplicationMain
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "Icon-App-1024x1024@1x.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 564 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 3.5 KiB |
23
example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
vendored
Normal file
After Width: | Height: | Size: 68 B |
BIN
example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
vendored
Normal file
After Width: | Height: | Size: 68 B |
BIN
example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
vendored
Normal file
After Width: | Height: | Size: 68 B |
5
example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# Launch Screen Assets
|
||||
|
||||
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||
|
||||
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
37
example/ios/Runner/Base.lproj/LaunchScreen.storyboard
Normal file
@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="185"/>
|
||||
</resources>
|
||||
</document>
|
26
example/ios/Runner/Base.lproj/Main.storyboard
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Flutter View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
49
example/ios/Runner/Info.plist
Normal file
@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>example</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
1
example/ios/Runner/Runner-Bridging-Header.h
Normal file
@ -0,0 +1 @@
|
||||
#import "GeneratedPluginRegistrant.h"
|
383
example/lib/main.dart
Normal file
@ -0,0 +1,383 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io' show Platform;
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||
|
||||
void main() => runApp(MyApp());
|
||||
|
||||
class MyApp extends StatefulWidget {
|
||||
@override
|
||||
State<MyApp> createState() => _MyAppState();
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> {
|
||||
bool isCupertino = !kIsWeb && Platform.isIOS;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!isCupertino) {
|
||||
return MaterialApp(
|
||||
title: 'flutter_typeahead demo',
|
||||
scrollBehavior:
|
||||
MaterialScrollBehavior().copyWith(dragDevices: {PointerDeviceKind.mouse, PointerDeviceKind.touch}),
|
||||
home: DefaultTabController(
|
||||
length: 3,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.phone_iphone),
|
||||
onPressed: () => setState(() {
|
||||
isCupertino = true;
|
||||
}),
|
||||
),
|
||||
title: TabBar(tabs: [
|
||||
Tab(text: 'Example 1: Navigation'),
|
||||
Tab(text: 'Example 2: Form'),
|
||||
Tab(text: 'Example 3: Scroll')
|
||||
]),
|
||||
),
|
||||
body: GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: TabBarView(children: [
|
||||
NavigationExample(),
|
||||
FormExample(),
|
||||
ScrollExample(),
|
||||
]),
|
||||
)),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return CupertinoApp(
|
||||
title: 'Cupertino demo',
|
||||
home: Scaffold(
|
||||
appBar: CupertinoNavigationBar(
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.android),
|
||||
onPressed: () => setState(() {
|
||||
isCupertino = false;
|
||||
}),
|
||||
),
|
||||
middle: Text('Cupertino demo'),
|
||||
),
|
||||
body: CupertinoPageScaffold(
|
||||
child: FavoriteCitiesPage(),
|
||||
),
|
||||
), //MyHomePage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NavigationExample extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: 10.0,
|
||||
),
|
||||
TypeAheadField(
|
||||
textFieldConfiguration: TextFieldConfiguration(
|
||||
autofocus: true,
|
||||
style: DefaultTextStyle.of(context).style.copyWith(fontStyle: FontStyle.italic),
|
||||
decoration: InputDecoration(border: OutlineInputBorder(), hintText: 'What are you looking for?'),
|
||||
),
|
||||
suggestionsCallback: (pattern) async {
|
||||
return await BackendService.getSuggestions(pattern);
|
||||
},
|
||||
itemBuilder: (context, Map<String, String> suggestion) {
|
||||
return ListTile(
|
||||
leading: Icon(Icons.shopping_cart),
|
||||
title: Text(suggestion['name']!),
|
||||
subtitle: Text('\$${suggestion['price']}'),
|
||||
);
|
||||
},
|
||||
onSuggestionSelected: (Map<String, String> suggestion) {
|
||||
Navigator.of(context)
|
||||
.push<void>(MaterialPageRoute(builder: (context) => ProductPage(product: suggestion)));
|
||||
},
|
||||
suggestionsBoxDecoration: SuggestionsBoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
elevation: 8.0,
|
||||
color: Theme.of(context).cardColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FormExample extends StatefulWidget {
|
||||
@override
|
||||
_FormExampleState createState() => _FormExampleState();
|
||||
}
|
||||
|
||||
class _FormExampleState extends State<FormExample> {
|
||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||
final TextEditingController _typeAheadController = TextEditingController();
|
||||
|
||||
String? _selectedCity;
|
||||
|
||||
SuggestionsBoxController suggestionBoxController = SuggestionsBoxController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
// close the suggestions box when the user taps outside of it
|
||||
onTap: () {
|
||||
suggestionBoxController.close();
|
||||
},
|
||||
child: Container(
|
||||
// Add zero opacity to make the gesture detector work
|
||||
color: Colors.amber.withOpacity(0),
|
||||
// Create the form for the user to enter their favorite city
|
||||
child: Form(
|
||||
key: this._formKey,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Text('What is your favorite city?'),
|
||||
TypeAheadFormField(
|
||||
textFieldConfiguration: TextFieldConfiguration(
|
||||
decoration: InputDecoration(labelText: 'City'),
|
||||
controller: this._typeAheadController,
|
||||
),
|
||||
suggestionsCallback: (pattern) {
|
||||
return CitiesService.getSuggestions(pattern);
|
||||
},
|
||||
itemBuilder: (context, String suggestion) {
|
||||
return ListTile(
|
||||
title: Text(suggestion),
|
||||
);
|
||||
},
|
||||
transitionBuilder: (context, suggestionsBox, controller) {
|
||||
return suggestionsBox;
|
||||
},
|
||||
onSuggestionSelected: (String suggestion) {
|
||||
this._typeAheadController.text = suggestion;
|
||||
},
|
||||
suggestionsBoxController: suggestionBoxController,
|
||||
validator: (value) => value!.isEmpty ? 'Please select a city' : null,
|
||||
onSaved: (value) => this._selectedCity = value,
|
||||
),
|
||||
Spacer(),
|
||||
ElevatedButton(
|
||||
child: Text('Submit'),
|
||||
onPressed: () {
|
||||
if (this._formKey.currentState!.validate()) {
|
||||
this._formKey.currentState!.save();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Your Favorite City is ${this._selectedCity}'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ProductPage extends StatelessWidget {
|
||||
final Map<String, String> product;
|
||||
|
||||
ProductPage({required this.product});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(50.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
this.product['name']!,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
Text(
|
||||
this.product['price']! + ' USD',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// This example shows how to use the [TypeAheadField] in a [ListView] that
|
||||
/// scrolls. The [TypeAheadField] will resize to fit the suggestions box when
|
||||
/// scrolling.
|
||||
class ScrollExample extends StatelessWidget {
|
||||
final List<String> items = List.generate(50, (index) => "Item $index");
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(children: [
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text("Suggestion box will resize when scrolling"),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 200),
|
||||
TypeAheadField<String>(
|
||||
getImmediateSuggestions: true,
|
||||
textFieldConfiguration: TextFieldConfiguration(
|
||||
decoration: InputDecoration(border: OutlineInputBorder(), hintText: 'What are you looking for?'),
|
||||
),
|
||||
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");
|
||||
},
|
||||
),
|
||||
SizedBox(height: 500),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/// This is a fake service that mimics a backend service.
|
||||
/// It returns a list of suggestions after a 1 second delay.
|
||||
/// In a real app, this would be a service that makes a network request.
|
||||
class BackendService {
|
||||
static Future<List<Map<String, String>>> getSuggestions(String query) async {
|
||||
await Future<void>.delayed(Duration(seconds: 1));
|
||||
|
||||
return List.generate(3, (index) {
|
||||
return {'name': query + index.toString(), 'price': Random().nextInt(100).toString()};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// A fake service to filter cities based on a query.
|
||||
class CitiesService {
|
||||
static final List<String> cities = [
|
||||
'Beirut',
|
||||
'Damascus',
|
||||
'San Fransisco',
|
||||
'Rome',
|
||||
'Los Angeles',
|
||||
'Madrid',
|
||||
'Bali',
|
||||
'Barcelona',
|
||||
'Paris',
|
||||
'Bucharest',
|
||||
'New York City',
|
||||
'Philadelphia',
|
||||
'Sydney',
|
||||
];
|
||||
|
||||
static List<String> getSuggestions(String query) {
|
||||
List<String> matches = <String>[];
|
||||
matches.addAll(cities);
|
||||
|
||||
matches.retainWhere((s) => s.toLowerCase().contains(query.toLowerCase()));
|
||||
return matches;
|
||||
}
|
||||
}
|
||||
|
||||
class FavoriteCitiesPage extends StatefulWidget {
|
||||
@override
|
||||
_FavoriteCitiesPage createState() => _FavoriteCitiesPage();
|
||||
}
|
||||
|
||||
class _FavoriteCitiesPage extends State<FavoriteCitiesPage> {
|
||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||
final TextEditingController _typeAheadController = TextEditingController();
|
||||
CupertinoSuggestionsBoxController _suggestionsBoxController = CupertinoSuggestionsBoxController();
|
||||
String favoriteCity = 'Unavailable';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => _suggestionsBoxController.close(),
|
||||
child: Container(
|
||||
color: Colors.amber.withOpacity(0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: 100.0,
|
||||
),
|
||||
Text('What is your favorite city?'),
|
||||
CupertinoTypeAheadFormField(
|
||||
getImmediateSuggestions: true,
|
||||
suggestionsBoxController: _suggestionsBoxController,
|
||||
suggestionsBoxDecoration: CupertinoSuggestionsBoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
textFieldConfiguration: CupertinoTextFieldConfiguration(
|
||||
controller: _typeAheadController,
|
||||
),
|
||||
suggestionsCallback: (pattern) {
|
||||
return Future.delayed(
|
||||
Duration(seconds: 1),
|
||||
() => CitiesService.getSuggestions(pattern),
|
||||
);
|
||||
},
|
||||
itemBuilder: (context, String suggestion) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Text(
|
||||
suggestion,
|
||||
),
|
||||
);
|
||||
},
|
||||
onSuggestionSelected: (String suggestion) {
|
||||
_typeAheadController.text = suggestion;
|
||||
},
|
||||
validator: (value) => value!.isEmpty ? 'Please select a city' : null,
|
||||
),
|
||||
SizedBox(
|
||||
height: 10.0,
|
||||
),
|
||||
CupertinoButton(
|
||||
child: Text('Submit'),
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
_formKey.currentState!.save();
|
||||
setState(() {
|
||||
favoriteCity = _typeAheadController.text;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
height: 10.0,
|
||||
),
|
||||
Text(
|
||||
'Your favorite city is $favoriteCity!',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
1
example/linux/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
flutter/ephemeral
|
116
example/linux/CMakeLists.txt
Normal file
@ -0,0 +1,116 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(runner LANGUAGES CXX)
|
||||
|
||||
set(BINARY_NAME "example")
|
||||
set(APPLICATION_ID "com.example.example")
|
||||
|
||||
cmake_policy(SET CMP0063 NEW)
|
||||
|
||||
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
|
||||
|
||||
# Root filesystem for cross-building.
|
||||
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
|
||||
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
|
||||
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
endif()
|
||||
|
||||
# Configure build options.
|
||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
||||
STRING "Flutter build mode" FORCE)
|
||||
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||
"Debug" "Profile" "Release")
|
||||
endif()
|
||||
|
||||
# Compilation settings that should be applied to most targets.
|
||||
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
||||
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
||||
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
||||
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
||||
endfunction()
|
||||
|
||||
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||
|
||||
# Flutter library and tool build rules.
|
||||
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||
|
||||
# System-level dependencies.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||
|
||||
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
|
||||
|
||||
# Application build
|
||||
add_executable(${BINARY_NAME}
|
||||
"main.cc"
|
||||
"my_application.cc"
|
||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||
)
|
||||
apply_standard_settings(${BINARY_NAME})
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
|
||||
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||
# Only the install-generated bundle's copy of the executable will launch
|
||||
# correctly, since the resources must in the right relative locations. To avoid
|
||||
# people trying to run the unbundled copy, put it in a subdirectory instead of
|
||||
# the default top-level location.
|
||||
set_target_properties(${BINARY_NAME}
|
||||
PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
|
||||
)
|
||||
|
||||
# Generated plugin build rules, which manage building the plugins and adding
|
||||
# them to the application.
|
||||
include(flutter/generated_plugins.cmake)
|
||||
|
||||
|
||||
# === Installation ===
|
||||
# By default, "installing" just makes a relocatable bundle in the build
|
||||
# directory.
|
||||
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
|
||||
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||
endif()
|
||||
|
||||
# Start with a clean build bundle directory every time.
|
||||
install(CODE "
|
||||
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
|
||||
" COMPONENT Runtime)
|
||||
|
||||
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
|
||||
|
||||
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
if(PLUGIN_BUNDLED_LIBRARIES)
|
||||
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endif()
|
||||
|
||||
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||
# from a previous install.
|
||||
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||
install(CODE "
|
||||
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
||||
" COMPONENT Runtime)
|
||||
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
||||
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
||||
|
||||
# Install the AOT library on non-Debug builds only.
|
||||
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
|
||||
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endif()
|
91
example/linux/flutter/CMakeLists.txt
Normal file
@ -0,0 +1,91 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||
|
||||
# Configuration provided via flutter tool.
|
||||
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||
|
||||
# TODO: Move the rest of this into files in ephemeral. See
|
||||
# https://github.com/flutter/flutter/issues/57146.
|
||||
|
||||
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
|
||||
# which isn't available in 3.10.
|
||||
function(list_prepend LIST_NAME PREFIX)
|
||||
set(NEW_LIST "")
|
||||
foreach(element ${${LIST_NAME}})
|
||||
list(APPEND NEW_LIST "${PREFIX}${element}")
|
||||
endforeach(element)
|
||||
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# === Flutter Library ===
|
||||
# System-level dependencies.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
|
||||
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
|
||||
pkg_check_modules(BLKID REQUIRED IMPORTED_TARGET blkid)
|
||||
pkg_check_modules(LZMA REQUIRED IMPORTED_TARGET liblzma)
|
||||
|
||||
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
|
||||
|
||||
# Published to parent scope for install step.
|
||||
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
|
||||
|
||||
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||
"fl_basic_message_channel.h"
|
||||
"fl_binary_codec.h"
|
||||
"fl_binary_messenger.h"
|
||||
"fl_dart_project.h"
|
||||
"fl_engine.h"
|
||||
"fl_json_message_codec.h"
|
||||
"fl_json_method_codec.h"
|
||||
"fl_message_codec.h"
|
||||
"fl_method_call.h"
|
||||
"fl_method_channel.h"
|
||||
"fl_method_codec.h"
|
||||
"fl_method_response.h"
|
||||
"fl_plugin_registrar.h"
|
||||
"fl_plugin_registry.h"
|
||||
"fl_standard_message_codec.h"
|
||||
"fl_standard_method_codec.h"
|
||||
"fl_string_codec.h"
|
||||
"fl_value.h"
|
||||
"fl_view.h"
|
||||
"flutter_linux.h"
|
||||
)
|
||||
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
|
||||
add_library(flutter INTERFACE)
|
||||
target_include_directories(flutter INTERFACE
|
||||
"${EPHEMERAL_DIR}"
|
||||
)
|
||||
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
|
||||
target_link_libraries(flutter INTERFACE
|
||||
PkgConfig::GTK
|
||||
PkgConfig::GLIB
|
||||
PkgConfig::GIO
|
||||
PkgConfig::BLKID
|
||||
PkgConfig::LZMA
|
||||
)
|
||||
add_dependencies(flutter flutter_assemble)
|
||||
|
||||
# === Flutter tool backend ===
|
||||
# _phony_ is a non-existent file to force this command to run every time,
|
||||
# since currently there's no way to get a full input/output list from the
|
||||
# flutter tool.
|
||||
add_custom_command(
|
||||
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/_phony_
|
||||
COMMAND ${CMAKE_COMMAND} -E env
|
||||
${FLUTTER_TOOL_ENVIRONMENT}
|
||||
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
|
||||
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(flutter_assemble DEPENDS
|
||||
"${FLUTTER_LIBRARY}"
|
||||
${FLUTTER_LIBRARY_HEADERS}
|
||||
)
|
11
example/linux/flutter/generated_plugin_registrant.cc
Normal file
@ -0,0 +1,11 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
}
|
15
example/linux/flutter/generated_plugin_registrant.h
Normal file
@ -0,0 +1,15 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||
#define GENERATED_PLUGIN_REGISTRANT_
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
|
||||
// Registers Flutter plugins.
|
||||
void fl_register_plugins(FlPluginRegistry* registry);
|
||||
|
||||
#endif // GENERATED_PLUGIN_REGISTRANT_
|
23
example/linux/flutter/generated_plugins.cmake
Normal file
@ -0,0 +1,23 @@
|
||||
#
|
||||
# Generated file, do not edit.
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||
endforeach(plugin)
|
||||
|
||||
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||
endforeach(ffi_plugin)
|
6
example/linux/main.cc
Normal file
@ -0,0 +1,6 @@
|
||||
#include "my_application.h"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
g_autoptr(MyApplication) app = my_application_new();
|
||||
return g_application_run(G_APPLICATION(app), argc, argv);
|
||||
}
|
105
example/linux/my_application.cc
Normal file
@ -0,0 +1,105 @@
|
||||
#include "my_application.h"
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
#include <gdk/gdkx.h>
|
||||
#endif
|
||||
|
||||
#include "flutter/generated_plugin_registrant.h"
|
||||
|
||||
struct _MyApplication {
|
||||
GtkApplication parent_instance;
|
||||
char** dart_entrypoint_arguments;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||
|
||||
// Implements GApplication::activate.
|
||||
static void my_application_activate(GApplication* application) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
GtkWindow* window =
|
||||
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||
|
||||
// Use a header bar when running in GNOME as this is the common style used
|
||||
// by applications and is the setup most users will be using (e.g. Ubuntu
|
||||
// desktop).
|
||||
// If running on X and not using GNOME then just use a traditional title bar
|
||||
// in case the window manager does more exotic layout, e.g. tiling.
|
||||
// If running on Wayland assume the header bar will work (may need changing
|
||||
// if future cases occur).
|
||||
gboolean use_header_bar = TRUE;
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
GdkScreen *screen = gtk_window_get_screen(window);
|
||||
if (GDK_IS_X11_SCREEN(screen)) {
|
||||
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
|
||||
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
|
||||
use_header_bar = FALSE;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (use_header_bar) {
|
||||
GtkHeaderBar *header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||
gtk_header_bar_set_title(header_bar, "example");
|
||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||
}
|
||||
else {
|
||||
gtk_window_set_title(window, "example");
|
||||
}
|
||||
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
gtk_widget_show(GTK_WIDGET(window));
|
||||
|
||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
|
||||
|
||||
FlView* view = fl_view_new(project);
|
||||
gtk_widget_show(GTK_WIDGET(view));
|
||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||
|
||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||
|
||||
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||
}
|
||||
|
||||
// Implements GApplication::local_command_line.
|
||||
static gboolean my_application_local_command_line(GApplication* application, gchar ***arguments, int *exit_status) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
// Strip out the first argument as it is the binary name.
|
||||
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
|
||||
|
||||
g_autoptr(GError) error = nullptr;
|
||||
if (!g_application_register(application, nullptr, &error)) {
|
||||
g_warning("Failed to register: %s", error->message);
|
||||
*exit_status = 1;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
g_application_activate(application);
|
||||
*exit_status = 0;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Implements GObject::dispose.
|
||||
static void my_application_dispose(GObject *object) {
|
||||
MyApplication* self = MY_APPLICATION(object);
|
||||
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
||||
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
||||
}
|
||||
|
||||
static void my_application_class_init(MyApplicationClass* klass) {
|
||||
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
|
||||
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
|
||||
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
|
||||
}
|
||||
|
||||
static void my_application_init(MyApplication* self) {}
|
||||
|
||||
MyApplication* my_application_new() {
|
||||
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
||||
"application-id", APPLICATION_ID,
|
||||
"flags", G_APPLICATION_NON_UNIQUE,
|
||||
nullptr));
|
||||
}
|
18
example/linux/my_application.h
Normal file
@ -0,0 +1,18 @@
|
||||
#ifndef FLUTTER_MY_APPLICATION_H_
|
||||
#define FLUTTER_MY_APPLICATION_H_
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
|
||||
GtkApplication)
|
||||
|
||||
/**
|
||||
* my_application_new:
|
||||
*
|
||||
* Creates a new Flutter-based application.
|
||||
*
|
||||
* Returns: a new #MyApplication.
|
||||
*/
|
||||
MyApplication* my_application_new();
|
||||
|
||||
#endif // FLUTTER_MY_APPLICATION_H_
|
78
example/pubspec.yaml
Normal file
@ -0,0 +1,78 @@
|
||||
name: example
|
||||
description: A new Flutter project.
|
||||
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `pub publish`. This is preferred for private packages.
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
|
||||
# The following defines the version and build number for your application.
|
||||
# A version number is three numbers separated by dots, like 1.2.43
|
||||
# followed by an optional build number separated by a +.
|
||||
# Both the version and the builder number may be overridden in flutter
|
||||
# build by specifying --build-name and --build-number, respectively.
|
||||
# In Android, build-name is used as versionName while build-number used as versionCode.
|
||||
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: ">=2.12.0-0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.2
|
||||
flutter_typeahead:
|
||||
path: ../
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter.
|
||||
flutter:
|
||||
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware.
|
||||
|
||||
# For details regarding adding assets from package dependencies, see
|
||||
# https://flutter.dev/assets-and-images/#from-packages
|
||||
|
||||
# To add custom fonts to your application, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/custom-fonts/#from-packages
|
BIN
example/web/favicon.png
Normal file
After Width: | Height: | Size: 917 B |
BIN
example/web/icons/Icon-192.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
example/web/icons/Icon-512.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
example/web/icons/Icon-maskable-192.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
example/web/icons/Icon-maskable-512.png
Normal file
After Width: | Height: | Size: 20 KiB |
58
example/web/index.html
Normal file
@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!--
|
||||
If you are serving your web app in a path other than the root, change the
|
||||
href value below to reflect the base path you are serving from.
|
||||
|
||||
The path provided below has to start and end with a slash "/" in order for
|
||||
it to work correctly.
|
||||
|
||||
For more details:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
||||
|
||||
This is a placeholder for base href that will be replaced by the value of
|
||||
the `--base-href` argument provided to `flutter build`.
|
||||
-->
|
||||
<base href="$FLUTTER_BASE_HREF">
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="A new Flutter project.">
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="example">
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
|
||||
<title>example</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
|
||||
<script>
|
||||
// The value below is injected by flutter build, do not touch.
|
||||
var serviceWorkerVersion = null;
|
||||
</script>
|
||||
<!-- This script adds the flutter initialization JS code -->
|
||||
<script src="flutter.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.addEventListener('load', function(ev) {
|
||||
// Download main.dart.js
|
||||
_flutter.loader.loadEntrypoint({
|
||||
serviceWorker: {
|
||||
serviceWorkerVersion: serviceWorkerVersion,
|
||||
}
|
||||
}).then(function(engineInitializer) {
|
||||
return engineInitializer.initializeEngine();
|
||||
}).then(function(appRunner) {
|
||||
return appRunner.runApp();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
35
example/web/manifest.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "example",
|
||||
"short_name": "example",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#0175C2",
|
||||
"theme_color": "#0175C2",
|
||||
"description": "A new Flutter project.",
|
||||
"orientation": "portrait-primary",
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/Icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-maskable-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-maskable-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
BIN
flutter_typeahead.gif
Normal file
After Width: | Height: | Size: 347 KiB |
22
flutter_typeahead.iml
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/lib" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.idea" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/example/.dart_tool" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/example/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/example/build" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Android API 25 Platform" jdkType="Android SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="Dart Packages" level="project" />
|
||||
<orderEntry type="library" name="Dart SDK" level="project" />
|
||||
<orderEntry type="library" name="Flutter Plugins" level="project" />
|
||||
</component>
|
||||
</module>
|
17
lib/flutter_typeahead.dart
Normal file
@ -0,0 +1,17 @@
|
||||
library flutter_typeahead;
|
||||
|
||||
export 'package:flutter_typeahead/src/typedef.dart';
|
||||
export 'package:flutter_typeahead/src/material/field/typeahead_field.dart';
|
||||
export 'package:flutter_typeahead/src/material/field/text_field_configuration.dart';
|
||||
export 'package:flutter_typeahead/src/material/field/typeahead_form_field.dart';
|
||||
export 'package:flutter_typeahead/src/material/suggestions_box/suggestions_box_controller.dart';
|
||||
export 'package:flutter_typeahead/src/material/suggestions_box/suggestions_box_decoration.dart';
|
||||
export 'package:flutter_typeahead/src/material/suggestions_box/suggestions_list.dart';
|
||||
export 'package:flutter_typeahead/src/material/suggestions_box/suggestions_box.dart';
|
||||
export 'package:flutter_typeahead/src/cupertino/field/cupertino_typeahead_field.dart';
|
||||
export 'package:flutter_typeahead/src/cupertino/field/cupertino_text_field_configuration.dart';
|
||||
export 'package:flutter_typeahead/src/cupertino/field/cupertino_typeahead_form_field.dart';
|
||||
export 'package:flutter_typeahead/src/cupertino/suggestions_box/cupertino_suggestions_list.dart';
|
||||
export 'package:flutter_typeahead/src/cupertino/suggestions_box/cupertino_suggestions_box_controller.dart';
|
||||
export 'package:flutter_typeahead/src/cupertino/suggestions_box/cupertino_suggestions_box_decoration.dart';
|
||||
export 'package:flutter_typeahead/src/cupertino/suggestions_box/cupertino_suggestions_box.dart';
|
185
lib/src/cupertino/field/cupertino_text_field_configuration.dart
Normal file
@ -0,0 +1,185 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
// Cupertino BoxDecoration taken from flutter/lib/src/cupertino/text_field.dart
|
||||
const BorderSide _kDefaultRoundedBorderSide = BorderSide(
|
||||
color: CupertinoDynamicColor.withBrightness(
|
||||
color: Color(0x33000000),
|
||||
darkColor: Color(0x33FFFFFF),
|
||||
),
|
||||
style: BorderStyle.solid,
|
||||
width: 0.0,
|
||||
);
|
||||
|
||||
const Border _kDefaultRoundedBorder = Border(
|
||||
top: _kDefaultRoundedBorderSide,
|
||||
bottom: _kDefaultRoundedBorderSide,
|
||||
left: _kDefaultRoundedBorderSide,
|
||||
right: _kDefaultRoundedBorderSide,
|
||||
);
|
||||
|
||||
const BoxDecoration _kDefaultRoundedBorderDecoration = BoxDecoration(
|
||||
color: CupertinoDynamicColor.withBrightness(
|
||||
color: CupertinoColors.white,
|
||||
darkColor: CupertinoColors.black,
|
||||
),
|
||||
border: _kDefaultRoundedBorder,
|
||||
borderRadius: BorderRadius.all(Radius.circular(5.0)),
|
||||
);
|
||||
|
||||
/// Supply an instance of this class to the [TypeAhead.textFieldConfiguration]
|
||||
/// property to configure the displayed text field. See [documentation](https://docs.flutter.io/flutter/cupertino/CupertinoTextField-class.html)
|
||||
/// for more information on properties.
|
||||
class CupertinoTextFieldConfiguration {
|
||||
final TextEditingController? controller;
|
||||
final FocusNode? focusNode;
|
||||
final BoxDecoration decoration;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final String? placeholder;
|
||||
final Widget? prefix;
|
||||
final OverlayVisibilityMode prefixMode;
|
||||
final Widget? suffix;
|
||||
final OverlayVisibilityMode suffixMode;
|
||||
final OverlayVisibilityMode clearButtonMode;
|
||||
final TextInputType? keyboardType;
|
||||
final TextInputAction? textInputAction;
|
||||
final TextCapitalization textCapitalization;
|
||||
final TextStyle? style;
|
||||
final TextAlign textAlign;
|
||||
final bool autofocus;
|
||||
final bool obscureText;
|
||||
final bool autocorrect;
|
||||
final int maxLines;
|
||||
final int? minLines;
|
||||
final int? maxLength;
|
||||
final MaxLengthEnforcement? maxLengthEnforcement;
|
||||
final ValueChanged<String>? onChanged;
|
||||
final VoidCallback? onEditingComplete;
|
||||
final GestureTapCallback? onTap;
|
||||
final ValueChanged<String>? onSubmitted;
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
final bool enabled;
|
||||
final bool enableSuggestions;
|
||||
final double cursorWidth;
|
||||
final Radius cursorRadius;
|
||||
final Color? cursorColor;
|
||||
final Brightness? keyboardAppearance;
|
||||
final EdgeInsets scrollPadding;
|
||||
final bool enableInteractiveSelection;
|
||||
|
||||
/// Creates a CupertinoTextFieldConfiguration
|
||||
const CupertinoTextFieldConfiguration({
|
||||
this.controller,
|
||||
this.focusNode,
|
||||
this.decoration = _kDefaultRoundedBorderDecoration,
|
||||
this.padding = const EdgeInsets.all(6.0),
|
||||
this.placeholder,
|
||||
this.prefix,
|
||||
this.prefixMode = OverlayVisibilityMode.always,
|
||||
this.suffix,
|
||||
this.suffixMode = OverlayVisibilityMode.always,
|
||||
this.clearButtonMode = OverlayVisibilityMode.never,
|
||||
this.keyboardType,
|
||||
this.textInputAction,
|
||||
this.textCapitalization = TextCapitalization.none,
|
||||
this.style,
|
||||
this.textAlign = TextAlign.start,
|
||||
this.autofocus = false,
|
||||
this.obscureText = false,
|
||||
this.autocorrect = true,
|
||||
this.maxLines = 1,
|
||||
this.minLines,
|
||||
this.maxLength,
|
||||
this.maxLengthEnforcement,
|
||||
this.onChanged,
|
||||
this.onEditingComplete,
|
||||
this.onTap,
|
||||
this.onSubmitted,
|
||||
this.inputFormatters,
|
||||
this.enabled = true,
|
||||
this.enableSuggestions = true,
|
||||
this.cursorWidth = 2.0,
|
||||
this.cursorRadius = const Radius.circular(2.0),
|
||||
this.cursorColor,
|
||||
this.keyboardAppearance,
|
||||
this.scrollPadding = const EdgeInsets.all(20.0),
|
||||
this.enableInteractiveSelection = true,
|
||||
});
|
||||
|
||||
/// Copies the [CupertinoTextFieldConfiguration] and only changes the specified properties
|
||||
CupertinoTextFieldConfiguration copyWith({
|
||||
TextEditingController? controller,
|
||||
FocusNode? focusNode,
|
||||
BoxDecoration? decoration,
|
||||
EdgeInsetsGeometry? padding,
|
||||
String? placeholder,
|
||||
Widget? prefix,
|
||||
OverlayVisibilityMode? prefixMode,
|
||||
Widget? suffix,
|
||||
OverlayVisibilityMode? suffixMode,
|
||||
OverlayVisibilityMode? clearButtonMode,
|
||||
TextInputType? keyboardType,
|
||||
TextInputAction? textInputAction,
|
||||
TextCapitalization? textCapitalization,
|
||||
TextStyle? style,
|
||||
TextAlign? textAlign,
|
||||
bool? autofocus,
|
||||
bool? obscureText,
|
||||
bool? autocorrect,
|
||||
int? maxLines,
|
||||
int? minLines,
|
||||
int? maxLength,
|
||||
MaxLengthEnforcement? maxLengthEnforcement,
|
||||
ValueChanged<String>? onChanged,
|
||||
VoidCallback? onEditingComplete,
|
||||
GestureTapCallback? onTap,
|
||||
ValueChanged<String>? onSubmitted,
|
||||
List<TextInputFormatter>? inputFormatters,
|
||||
bool? enabled,
|
||||
bool? enableSuggestions,
|
||||
double? cursorWidth,
|
||||
Radius? cursorRadius,
|
||||
Color? cursorColor,
|
||||
Brightness? keyboardAppearance,
|
||||
EdgeInsets? scrollPadding,
|
||||
bool? enableInteractiveSelection,
|
||||
}) {
|
||||
return CupertinoTextFieldConfiguration(
|
||||
controller: controller ?? this.controller,
|
||||
focusNode: focusNode ?? this.focusNode,
|
||||
decoration: decoration ?? this.decoration,
|
||||
padding: padding ?? this.padding,
|
||||
placeholder: placeholder ?? this.placeholder,
|
||||
prefix: prefix ?? this.prefix,
|
||||
prefixMode: prefixMode ?? this.prefixMode,
|
||||
suffix: suffix ?? this.suffix,
|
||||
suffixMode: suffixMode ?? this.suffixMode,
|
||||
clearButtonMode: clearButtonMode ?? this.clearButtonMode,
|
||||
keyboardType: keyboardType ?? this.keyboardType,
|
||||
textInputAction: textInputAction ?? this.textInputAction,
|
||||
textCapitalization: textCapitalization ?? this.textCapitalization,
|
||||
style: style ?? this.style,
|
||||
textAlign: textAlign ?? this.textAlign,
|
||||
autofocus: autofocus ?? this.autofocus,
|
||||
obscureText: obscureText ?? this.obscureText,
|
||||
autocorrect: autocorrect ?? this.autocorrect,
|
||||
maxLines: maxLines ?? this.maxLines,
|
||||
minLines: minLines ?? this.minLines,
|
||||
maxLength: maxLength ?? this.maxLength,
|
||||
maxLengthEnforcement: maxLengthEnforcement ?? this.maxLengthEnforcement,
|
||||
onChanged: onChanged ?? this.onChanged,
|
||||
onEditingComplete: onEditingComplete ?? this.onEditingComplete,
|
||||
onTap: onTap ?? this.onTap,
|
||||
onSubmitted: onSubmitted ?? this.onSubmitted,
|
||||
inputFormatters: inputFormatters ?? this.inputFormatters,
|
||||
enabled: enabled ?? this.enabled,
|
||||
enableSuggestions: enableSuggestions ?? this.enableSuggestions,
|
||||
cursorWidth: cursorWidth ?? this.cursorWidth,
|
||||
cursorRadius: cursorRadius ?? this.cursorRadius,
|
||||
cursorColor: cursorColor ?? this.cursorColor,
|
||||
keyboardAppearance: keyboardAppearance ?? this.keyboardAppearance,
|
||||
scrollPadding: scrollPadding ?? this.scrollPadding,
|
||||
enableInteractiveSelection: enableInteractiveSelection ?? this.enableInteractiveSelection,
|
||||
);
|
||||
}
|
||||
}
|
560
lib/src/cupertino/field/cupertino_typeahead_field.dart
Normal file
@ -0,0 +1,560 @@
|
||||
import 'dart:async';
|
||||
import 'dart:core';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||
import 'package:flutter_typeahead/src/cupertino/field/cupertino_text_field_configuration.dart';
|
||||
import 'package:flutter_typeahead/src/cupertino/suggestions_box/cupertino_suggestions_box.dart';
|
||||
import 'package:flutter_typeahead/src/cupertino/suggestions_box/cupertino_suggestions_box_controller.dart';
|
||||
import 'package:flutter_typeahead/src/cupertino/suggestions_box/cupertino_suggestions_box_decoration.dart';
|
||||
import 'package:flutter_typeahead/src/cupertino/suggestions_box/cupertino_suggestions_list.dart';
|
||||
import 'package:flutter_typeahead/src/typedef.dart';
|
||||
import 'package:flutter_typeahead/src/utils.dart';
|
||||
|
||||
/// A [CupertinoTextField](https://docs.flutter.io/flutter/cupertino/CupertinoTextField-class.html)
|
||||
/// that displays a list of suggestions as the user types
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TypeAheadFormField], a [FormField](https://docs.flutter.io/flutter/widgets/FormField-class.html)
|
||||
/// implementation of [TypeAheadField] that allows the value to be saved,
|
||||
/// validated, etc.
|
||||
class CupertinoTypeAheadField<T> extends StatefulWidget {
|
||||
/// Called with the search pattern to get the search suggestions.
|
||||
///
|
||||
/// This callback must not be null. It is be called by the TypeAhead widget
|
||||
/// and provided with the search pattern. It should return a [List](https://api.dartlang.org/stable/2.0.0/dart-core/List-class.html)
|
||||
/// of suggestions either synchronously, or asynchronously (as the result of a
|
||||
/// [Future](https://api.dartlang.org/stable/dart-async/Future-class.html)).
|
||||
/// Typically, the list of suggestions should not contain more than 4 or 5
|
||||
/// entries. These entries will then be provided to [itemBuilder] to display
|
||||
/// the suggestions.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// suggestionsCallback: (pattern) async {
|
||||
/// return await _getSuggestions(pattern);
|
||||
/// }
|
||||
/// ```
|
||||
final SuggestionsCallback<T> suggestionsCallback;
|
||||
|
||||
/// Called when a suggestion is tapped.
|
||||
///
|
||||
/// This callback must not be null. It is called by the TypeAhead widget and
|
||||
/// provided with the value of the tapped suggestion.
|
||||
///
|
||||
/// For example, you might want to navigate to a specific view when the user
|
||||
/// tabs a suggestion:
|
||||
/// ```dart
|
||||
/// onSuggestionSelected: (suggestion) {
|
||||
/// Navigator.of(context).push(MaterialPageRoute(
|
||||
/// builder: (context) => SearchResult(
|
||||
/// searchItem: suggestion
|
||||
/// )
|
||||
/// ));
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Or to set the value of the text field:
|
||||
/// ```dart
|
||||
/// onSuggestionSelected: (suggestion) {
|
||||
/// _controller.text = suggestion['name'];
|
||||
/// }
|
||||
/// ```
|
||||
final SuggestionSelectionCallback<T> onSuggestionSelected;
|
||||
|
||||
/// Called for each suggestion returned by [suggestionsCallback] to build the
|
||||
/// corresponding widget.
|
||||
///
|
||||
/// This callback must not be null. It is called by the TypeAhead widget for
|
||||
/// each suggestion, and expected to build a widget to display this
|
||||
/// suggestion's info. For example:
|
||||
///
|
||||
/// ```dart
|
||||
/// itemBuilder: (context, suggestion) {
|
||||
/// return Padding(
|
||||
/// padding: const EdgeInsets.all(4.0),
|
||||
/// child: Text(
|
||||
/// suggestion,
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
final ItemBuilder<T> itemBuilder;
|
||||
|
||||
/// The decoration of the material sheet that contains the suggestions.
|
||||
final CupertinoSuggestionsBoxDecoration suggestionsBoxDecoration;
|
||||
|
||||
/// Used to control the `_SuggestionsBox`. Allows manual control to
|
||||
/// open, close, toggle, or resize the `_SuggestionsBox`.
|
||||
final CupertinoSuggestionsBoxController? suggestionsBoxController;
|
||||
|
||||
/// The duration to wait after the user stops typing before calling
|
||||
/// [suggestionsCallback]
|
||||
///
|
||||
/// This is useful, because, if not set, a request for suggestions will be
|
||||
/// sent for every character that the user types.
|
||||
///
|
||||
/// This duration is set by default to 300 milliseconds
|
||||
final Duration debounceDuration;
|
||||
|
||||
/// Called when waiting for [suggestionsCallback] to return.
|
||||
///
|
||||
/// It is expected to return a widget to display while waiting.
|
||||
/// For example:
|
||||
/// ```dart
|
||||
/// (BuildContext context) {
|
||||
/// return Text('Loading...');
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// If not specified, a [CupertinoActivityIndicator](https://docs.flutter.io/flutter/cupertino/CupertinoActivityIndicator-class.html) is shown
|
||||
final WidgetBuilder? loadingBuilder;
|
||||
|
||||
/// Called when [suggestionsCallback] returns an empty array.
|
||||
///
|
||||
/// It is expected to return a widget to display when no suggestions are
|
||||
/// avaiable.
|
||||
/// For example:
|
||||
/// ```dart
|
||||
/// (BuildContext context) {
|
||||
/// return Text('No Items Found!');
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// If not specified, a simple text is shown
|
||||
final WidgetBuilder? noItemsFoundBuilder;
|
||||
|
||||
/// Called when [suggestionsCallback] throws an exception.
|
||||
///
|
||||
/// It is called with the error object, and expected to return a widget to
|
||||
/// display when an exception is thrown
|
||||
/// For example:
|
||||
/// ```dart
|
||||
/// (BuildContext context, error) {
|
||||
/// return Text('$error');
|
||||
/// }
|
||||
/// ```
|
||||
final ErrorBuilder? errorBuilder;
|
||||
|
||||
/// Called to display animations when [suggestionsCallback] returns suggestions
|
||||
///
|
||||
/// It is provided with the suggestions box instance and the animation
|
||||
/// controller, and expected to return some animation that uses the controller
|
||||
/// to display the suggestion box.
|
||||
///
|
||||
/// For example:
|
||||
/// ```dart
|
||||
/// transitionBuilder: (context, suggestionsBox, animationController) {
|
||||
/// return FadeTransition(
|
||||
/// child: suggestionsBox,
|
||||
/// opacity: CurvedAnimation(
|
||||
/// parent: animationController,
|
||||
/// curve: Curves.fastOutSlowIn
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
/// This argument is best used with [animationDuration] and [animationStart]
|
||||
/// to fully control the animation.
|
||||
///
|
||||
/// To fully remove the animation, just return `suggestionsBox`
|
||||
///
|
||||
/// If not specified, a [SizeTransition](https://docs.flutter.io/flutter/widgets/SizeTransition-class.html) is shown.
|
||||
final AnimationTransitionBuilder? transitionBuilder;
|
||||
|
||||
/// The duration that [transitionBuilder] animation takes.
|
||||
///
|
||||
/// This argument is best used with [transitionBuilder] and [animationStart]
|
||||
/// to fully control the animation.
|
||||
///
|
||||
/// Defaults to 500 milliseconds.
|
||||
final Duration animationDuration;
|
||||
|
||||
/// Determine the [SuggestionBox]'s direction.
|
||||
///
|
||||
/// If [AxisDirection.down], the [SuggestionBox] will be below the [TextField]
|
||||
/// and the [_SuggestionsList] will grow **down**.
|
||||
///
|
||||
/// If [AxisDirection.up], the [SuggestionBox] will be above the [TextField]
|
||||
/// and the [_SuggestionsList] will grow **up**.
|
||||
///
|
||||
/// [AxisDirection.left] and [AxisDirection.right] are not allowed.
|
||||
final AxisDirection direction;
|
||||
|
||||
/// The value at which the [transitionBuilder] animation starts.
|
||||
///
|
||||
/// This argument is best used with [transitionBuilder] and [animationDuration]
|
||||
/// to fully control the animation.
|
||||
///
|
||||
/// Defaults to 0.25.
|
||||
final double animationStart;
|
||||
|
||||
/// The configuration of the [CupertinoTextField](https://docs.flutter.io/flutter/cupertino/CupertinoTextField-class.html)
|
||||
/// that the TypeAhead widget displays
|
||||
final CupertinoTextFieldConfiguration textFieldConfiguration;
|
||||
|
||||
/// How far below the text field should the suggestions box be
|
||||
///
|
||||
/// Defaults to 5.0
|
||||
final double suggestionsBoxVerticalOffset;
|
||||
|
||||
/// If set to true, suggestions will be fetched immediately when the field is
|
||||
/// added to the view.
|
||||
///
|
||||
/// But the suggestions box will only be shown when the field receives focus.
|
||||
/// To make the field receive focus immediately, you can set the `autofocus`
|
||||
/// property in the [textFieldConfiguration] to true
|
||||
///
|
||||
/// Defaults to false
|
||||
final bool getImmediateSuggestions;
|
||||
|
||||
/// If set to true, no loading box will be shown while suggestions are
|
||||
/// being fetched. [loadingBuilder] will also be ignored.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool hideOnLoading;
|
||||
|
||||
/// If set to true, nothing will be shown if there are no results.
|
||||
/// [noItemsFoundBuilder] will also be ignored.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool hideOnEmpty;
|
||||
|
||||
/// If set to true, nothing will be shown if there is an error.
|
||||
/// [errorBuilder] will also be ignored.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool hideOnError;
|
||||
|
||||
/// If set to false, the suggestions box will stay opened after
|
||||
/// the keyboard is closed.
|
||||
///
|
||||
/// Defaults to true.
|
||||
final bool hideSuggestionsOnKeyboardHide;
|
||||
|
||||
/// If set to false, the suggestions box will show a circular
|
||||
/// progress indicator when retrieving suggestions.
|
||||
///
|
||||
/// Defaults to true.
|
||||
final bool keepSuggestionsOnLoading;
|
||||
|
||||
/// If set to true, the suggestions box will remain opened even after
|
||||
/// selecting a suggestion.
|
||||
///
|
||||
/// Note that if this is enabled, the only way
|
||||
/// to close the suggestions box is either manually via the
|
||||
/// `SuggestionsBoxController` or when the user closes the software
|
||||
/// keyboard if `hideSuggestionsOnKeyboardHide` is set to true. Users
|
||||
/// with a physical keyboard will be unable to close the
|
||||
/// box without a manual way via `SuggestionsBoxController`.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool keepSuggestionsOnSuggestionSelected;
|
||||
|
||||
/// If set to true, in the case where the suggestions box has less than
|
||||
/// _SuggestionsBoxController.minOverlaySpace to grow in the desired [direction], the direction axis
|
||||
/// will be temporarily flipped if there's more room available in the opposite
|
||||
/// direction.
|
||||
///
|
||||
/// Defaults to false
|
||||
final bool autoFlipDirection;
|
||||
|
||||
/// If set to false, suggestion list will not be reversed according to the
|
||||
/// [autoFlipDirection] property.
|
||||
///
|
||||
/// Defaults to true.
|
||||
final bool autoFlipListDirection;
|
||||
|
||||
/// The minimum number of characters which must be entered before
|
||||
/// [suggestionsCallback] is triggered.
|
||||
///
|
||||
/// Defaults to 0.
|
||||
final int minCharsForSuggestions;
|
||||
|
||||
/// If set to true and if the user scrolls through the suggestion list, hide the keyboard automatically.
|
||||
/// If set to false, the keyboard remains visible.
|
||||
/// Throws an exception, if hideKeyboardOnDrag and hideSuggestionsOnKeyboardHide are both set to true as
|
||||
/// they are mutual exclusive.
|
||||
///
|
||||
/// Defaults to false
|
||||
final bool hideKeyboardOnDrag;
|
||||
|
||||
/// Creates a [CupertinoTypeAheadField]
|
||||
CupertinoTypeAheadField({
|
||||
Key? key,
|
||||
required this.suggestionsCallback,
|
||||
required this.itemBuilder,
|
||||
required this.onSuggestionSelected,
|
||||
this.textFieldConfiguration = const CupertinoTextFieldConfiguration(),
|
||||
this.suggestionsBoxDecoration = const CupertinoSuggestionsBoxDecoration(),
|
||||
this.debounceDuration = const Duration(milliseconds: 300),
|
||||
this.suggestionsBoxController,
|
||||
this.loadingBuilder,
|
||||
this.noItemsFoundBuilder,
|
||||
this.errorBuilder,
|
||||
this.transitionBuilder,
|
||||
this.animationStart = 0.25,
|
||||
this.animationDuration = const Duration(milliseconds: 500),
|
||||
this.getImmediateSuggestions = false,
|
||||
this.suggestionsBoxVerticalOffset = 5.0,
|
||||
this.direction = AxisDirection.down,
|
||||
this.hideOnLoading = false,
|
||||
this.hideOnEmpty = false,
|
||||
this.hideOnError = false,
|
||||
this.hideSuggestionsOnKeyboardHide = true,
|
||||
this.keepSuggestionsOnLoading = true,
|
||||
this.keepSuggestionsOnSuggestionSelected = false,
|
||||
this.autoFlipDirection = false,
|
||||
this.autoFlipListDirection = true,
|
||||
this.minCharsForSuggestions = 0,
|
||||
this.hideKeyboardOnDrag = true,
|
||||
}) : assert(animationStart >= 0.0 && animationStart <= 1.0),
|
||||
assert(direction == AxisDirection.down || direction == AxisDirection.up),
|
||||
assert(minCharsForSuggestions >= 0),
|
||||
assert(!hideKeyboardOnDrag || hideKeyboardOnDrag && !hideSuggestionsOnKeyboardHide),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
_CupertinoTypeAheadFieldState<T> createState() => _CupertinoTypeAheadFieldState<T>();
|
||||
}
|
||||
|
||||
class _CupertinoTypeAheadFieldState<T> extends State<CupertinoTypeAheadField<T>> with WidgetsBindingObserver {
|
||||
FocusNode? _focusNode;
|
||||
TextEditingController? _textEditingController;
|
||||
CupertinoSuggestionsBox? _suggestionsBox;
|
||||
|
||||
TextEditingController? get _effectiveController => widget.textFieldConfiguration.controller ?? _textEditingController;
|
||||
|
||||
FocusNode? get _effectiveFocusNode => widget.textFieldConfiguration.focusNode ?? _focusNode;
|
||||
late VoidCallback _focusNodeListener;
|
||||
|
||||
final LayerLink _layerLink = LayerLink();
|
||||
|
||||
// Timer that resizes the suggestion box on each tick. Only active when the user is scrolling.
|
||||
Timer? _resizeOnScrollTimer;
|
||||
|
||||
// The rate at which the suggestion box will resize when the user is scrolling
|
||||
final Duration _resizeOnScrollRefreshRate = const Duration(milliseconds: 500);
|
||||
|
||||
// Will have a value if the typeahead is inside a scrollable widget
|
||||
ScrollPosition? _scrollPosition;
|
||||
|
||||
// Keyboard detection
|
||||
final Stream<bool>? _keyboardVisibility = (supportedPlatform) ? KeyboardVisibilityController().onChange : null;
|
||||
late StreamSubscription<bool>? _keyboardVisibilitySubscription;
|
||||
|
||||
@override
|
||||
void didChangeMetrics() {
|
||||
// Catch keyboard event and orientation change; resize suggestions list
|
||||
this._suggestionsBox!.onChangeMetrics();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
this._suggestionsBox!.close();
|
||||
this._suggestionsBox!.widgetMounted = false;
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_keyboardVisibilitySubscription?.cancel();
|
||||
_effectiveFocusNode!.removeListener(_focusNodeListener);
|
||||
_focusNode?.dispose();
|
||||
_resizeOnScrollTimer?.cancel();
|
||||
_scrollPosition?.removeListener(_scrollResizeListener);
|
||||
_textEditingController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
if (widget.textFieldConfiguration.controller == null) {
|
||||
this._textEditingController = TextEditingController();
|
||||
}
|
||||
|
||||
if (widget.textFieldConfiguration.focusNode == null) {
|
||||
this._focusNode = FocusNode();
|
||||
}
|
||||
|
||||
this._suggestionsBox = CupertinoSuggestionsBox(
|
||||
context,
|
||||
widget.direction,
|
||||
widget.autoFlipDirection,
|
||||
widget.autoFlipListDirection,
|
||||
);
|
||||
|
||||
widget.suggestionsBoxController?.suggestionsBox = this._suggestionsBox;
|
||||
widget.suggestionsBoxController?.effectiveFocusNode = this._effectiveFocusNode;
|
||||
|
||||
this._focusNodeListener = () {
|
||||
if (_effectiveFocusNode!.hasFocus) {
|
||||
this._suggestionsBox!.open();
|
||||
} else {
|
||||
this._suggestionsBox!.close();
|
||||
}
|
||||
};
|
||||
|
||||
this._effectiveFocusNode!.addListener(_focusNodeListener);
|
||||
|
||||
// hide suggestions box on keyboard closed
|
||||
this._keyboardVisibilitySubscription = _keyboardVisibility?.listen((bool isVisible) {
|
||||
if (widget.hideSuggestionsOnKeyboardHide && !isVisible) {
|
||||
_effectiveFocusNode!.unfocus();
|
||||
}
|
||||
});
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((duration) {
|
||||
if (mounted) {
|
||||
this._initOverlayEntry();
|
||||
// calculate initial suggestions list size
|
||||
this._suggestionsBox!.resize();
|
||||
|
||||
// in case we already missed the focus event
|
||||
if (this._effectiveFocusNode!.hasFocus) {
|
||||
this._suggestionsBox!.open();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final scrollableState = Scrollable.maybeOf(context);
|
||||
if (scrollableState != null) {
|
||||
// The TypeAheadField is inside a scrollable widget
|
||||
_scrollPosition = scrollableState.position;
|
||||
|
||||
_scrollPosition!.removeListener(_scrollResizeListener);
|
||||
_scrollPosition!.isScrollingNotifier.addListener(_scrollResizeListener);
|
||||
}
|
||||
}
|
||||
|
||||
void _scrollResizeListener() {
|
||||
bool isScrolling = _scrollPosition!.isScrollingNotifier.value;
|
||||
_resizeOnScrollTimer?.cancel();
|
||||
if (isScrolling) {
|
||||
// Scroll started
|
||||
_resizeOnScrollTimer = Timer.periodic(_resizeOnScrollRefreshRate, (timer) {
|
||||
_suggestionsBox!.resize();
|
||||
});
|
||||
} else {
|
||||
// Scroll finished
|
||||
_suggestionsBox!.resize();
|
||||
}
|
||||
}
|
||||
|
||||
void _initOverlayEntry() {
|
||||
this._suggestionsBox!.overlayEntry = OverlayEntry(builder: (context) {
|
||||
final suggestionsList = CupertinoSuggestionsList<T>(
|
||||
suggestionsBox: _suggestionsBox,
|
||||
decoration: widget.suggestionsBoxDecoration,
|
||||
debounceDuration: widget.debounceDuration,
|
||||
controller: this._effectiveController,
|
||||
loadingBuilder: widget.loadingBuilder,
|
||||
noItemsFoundBuilder: widget.noItemsFoundBuilder,
|
||||
errorBuilder: widget.errorBuilder,
|
||||
transitionBuilder: widget.transitionBuilder,
|
||||
suggestionsCallback: widget.suggestionsCallback,
|
||||
animationDuration: widget.animationDuration,
|
||||
animationStart: widget.animationStart,
|
||||
getImmediateSuggestions: widget.getImmediateSuggestions,
|
||||
onSuggestionSelected: (T selection) {
|
||||
if (!widget.keepSuggestionsOnSuggestionSelected) {
|
||||
this._effectiveFocusNode!.unfocus();
|
||||
this._suggestionsBox!.close();
|
||||
}
|
||||
widget.onSuggestionSelected(selection);
|
||||
},
|
||||
itemBuilder: widget.itemBuilder,
|
||||
direction: _suggestionsBox!.direction,
|
||||
hideOnLoading: widget.hideOnLoading,
|
||||
hideOnEmpty: widget.hideOnEmpty,
|
||||
hideOnError: widget.hideOnError,
|
||||
keepSuggestionsOnLoading: widget.keepSuggestionsOnLoading,
|
||||
minCharsForSuggestions: widget.minCharsForSuggestions,
|
||||
hideKeyboardOnDrag: widget.hideKeyboardOnDrag,
|
||||
);
|
||||
|
||||
double w = _suggestionsBox!.textBoxWidth;
|
||||
if (widget.suggestionsBoxDecoration.constraints != null) {
|
||||
if (widget.suggestionsBoxDecoration.constraints!.minWidth != 0.0 &&
|
||||
widget.suggestionsBoxDecoration.constraints!.maxWidth != double.infinity) {
|
||||
w = (widget.suggestionsBoxDecoration.constraints!.minWidth +
|
||||
widget.suggestionsBoxDecoration.constraints!.maxWidth) /
|
||||
2;
|
||||
} else if (widget.suggestionsBoxDecoration.constraints!.minWidth != 0.0 &&
|
||||
widget.suggestionsBoxDecoration.constraints!.minWidth > w) {
|
||||
w = widget.suggestionsBoxDecoration.constraints!.minWidth;
|
||||
} else if (widget.suggestionsBoxDecoration.constraints!.maxWidth != double.infinity &&
|
||||
widget.suggestionsBoxDecoration.constraints!.maxWidth < w) {
|
||||
w = widget.suggestionsBoxDecoration.constraints!.maxWidth;
|
||||
}
|
||||
}
|
||||
|
||||
return Positioned(
|
||||
width: w,
|
||||
child: CompositedTransformFollower(
|
||||
link: this._layerLink,
|
||||
showWhenUnlinked: false,
|
||||
offset: Offset(
|
||||
widget.suggestionsBoxDecoration.offsetX,
|
||||
_suggestionsBox!.direction == AxisDirection.down
|
||||
? _suggestionsBox!.textBoxHeight + widget.suggestionsBoxVerticalOffset
|
||||
: _suggestionsBox!.directionUpOffset),
|
||||
child: _suggestionsBox!.direction == AxisDirection.down
|
||||
? suggestionsList
|
||||
: FractionalTranslation(
|
||||
translation: Offset(0.0, -1.0), // visually flips list to go up
|
||||
child: suggestionsList,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CompositedTransformTarget(
|
||||
link: this._layerLink,
|
||||
child: CupertinoTextField(
|
||||
controller: this._effectiveController,
|
||||
focusNode: this._effectiveFocusNode,
|
||||
decoration: widget.textFieldConfiguration.decoration,
|
||||
padding: widget.textFieldConfiguration.padding,
|
||||
placeholder: widget.textFieldConfiguration.placeholder,
|
||||
prefix: widget.textFieldConfiguration.prefix,
|
||||
prefixMode: widget.textFieldConfiguration.prefixMode,
|
||||
suffix: widget.textFieldConfiguration.suffix,
|
||||
suffixMode: widget.textFieldConfiguration.suffixMode,
|
||||
clearButtonMode: widget.textFieldConfiguration.clearButtonMode,
|
||||
keyboardType: widget.textFieldConfiguration.keyboardType,
|
||||
textInputAction: widget.textFieldConfiguration.textInputAction,
|
||||
textCapitalization: widget.textFieldConfiguration.textCapitalization,
|
||||
style: widget.textFieldConfiguration.style,
|
||||
textAlign: widget.textFieldConfiguration.textAlign,
|
||||
autofocus: widget.textFieldConfiguration.autofocus,
|
||||
obscureText: widget.textFieldConfiguration.obscureText,
|
||||
autocorrect: widget.textFieldConfiguration.autocorrect,
|
||||
maxLines: widget.textFieldConfiguration.maxLines,
|
||||
minLines: widget.textFieldConfiguration.minLines,
|
||||
maxLength: widget.textFieldConfiguration.maxLength,
|
||||
maxLengthEnforcement: widget.textFieldConfiguration.maxLengthEnforcement,
|
||||
onChanged: widget.textFieldConfiguration.onChanged,
|
||||
onEditingComplete: widget.textFieldConfiguration.onEditingComplete,
|
||||
onTap: widget.textFieldConfiguration.onTap,
|
||||
// onTapOutside: (_){},
|
||||
onSubmitted: widget.textFieldConfiguration.onSubmitted,
|
||||
inputFormatters: widget.textFieldConfiguration.inputFormatters,
|
||||
enabled: widget.textFieldConfiguration.enabled,
|
||||
cursorWidth: widget.textFieldConfiguration.cursorWidth,
|
||||
cursorRadius: widget.textFieldConfiguration.cursorRadius,
|
||||
cursorColor: widget.textFieldConfiguration.cursorColor,
|
||||
keyboardAppearance: widget.textFieldConfiguration.keyboardAppearance,
|
||||
scrollPadding: widget.textFieldConfiguration.scrollPadding,
|
||||
enableInteractiveSelection: widget.textFieldConfiguration.enableInteractiveSelection,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
190
lib/src/cupertino/field/cupertino_typeahead_form_field.dart
Normal file
@ -0,0 +1,190 @@
|
||||
import 'dart:core';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_typeahead/src/cupertino/field/cupertino_text_field_configuration.dart';
|
||||
import 'package:flutter_typeahead/src/cupertino/field/cupertino_typeahead_field.dart';
|
||||
import 'package:flutter_typeahead/src/cupertino/suggestions_box/cupertino_suggestions_box_controller.dart';
|
||||
import 'package:flutter_typeahead/src/cupertino/suggestions_box/cupertino_suggestions_box_decoration.dart';
|
||||
import 'package:flutter_typeahead/src/typedef.dart';
|
||||
|
||||
|
||||
/// A [FormField](https://docs.flutter.io/flutter/widgets/FormField-class.html)
|
||||
/// implementation of [TypeAheadField], that allows the value to be saved,
|
||||
/// validated, etc.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TypeAheadField], A [CupertinoTextField](https://docs.flutter.io/flutter/cupertino/CupertinoTextField-class.html)
|
||||
/// that displays a list of suggestions as the user types
|
||||
class CupertinoTypeAheadFormField<T> extends FormField<String> {
|
||||
/// The configuration of the [CupertinoTextField](https://docs.flutter.io/flutter/cupertino/CupertinoTextField-class.html)
|
||||
/// that the TypeAhead widget displays
|
||||
final CupertinoTextFieldConfiguration textFieldConfiguration;
|
||||
|
||||
/// Creates a [CupertinoTypeAheadFormField]
|
||||
CupertinoTypeAheadFormField(
|
||||
{Key? key,
|
||||
String? initialValue,
|
||||
bool getImmediateSuggestions = false,
|
||||
@Deprecated('Use autoValidateMode parameter which provides more specific '
|
||||
'behavior related to auto validation. '
|
||||
'This feature was deprecated after Flutter v1.19.0.')
|
||||
bool autovalidate = false,
|
||||
bool enabled = true,
|
||||
AutovalidateMode? autovalidateMode,
|
||||
FormFieldSetter<String>? onSaved,
|
||||
FormFieldValidator<String>? validator,
|
||||
ErrorBuilder? errorBuilder,
|
||||
WidgetBuilder? noItemsFoundBuilder,
|
||||
WidgetBuilder? loadingBuilder,
|
||||
Duration debounceDuration = const Duration(milliseconds: 300),
|
||||
CupertinoSuggestionsBoxDecoration suggestionsBoxDecoration =
|
||||
const CupertinoSuggestionsBoxDecoration(),
|
||||
CupertinoSuggestionsBoxController? suggestionsBoxController,
|
||||
required SuggestionSelectionCallback<T> onSuggestionSelected,
|
||||
required ItemBuilder<T> itemBuilder,
|
||||
required SuggestionsCallback<T> suggestionsCallback,
|
||||
double suggestionsBoxVerticalOffset = 5.0,
|
||||
this.textFieldConfiguration = const CupertinoTextFieldConfiguration(),
|
||||
AnimationTransitionBuilder? transitionBuilder,
|
||||
Duration animationDuration = const Duration(milliseconds: 500),
|
||||
double animationStart = 0.25,
|
||||
AxisDirection direction = AxisDirection.down,
|
||||
bool hideOnLoading = false,
|
||||
bool hideOnEmpty = false,
|
||||
bool hideOnError = false,
|
||||
bool hideSuggestionsOnKeyboardHide = true,
|
||||
bool keepSuggestionsOnLoading = true,
|
||||
bool keepSuggestionsOnSuggestionSelected = false,
|
||||
bool autoFlipDirection = false,
|
||||
bool autoFlipListDirection = true,
|
||||
int minCharsForSuggestions = 0,
|
||||
bool hideKeyboardOnDrag = false})
|
||||
: assert(
|
||||
initialValue == null || textFieldConfiguration.controller == null),
|
||||
assert(minCharsForSuggestions >= 0),
|
||||
super(
|
||||
key: key,
|
||||
onSaved: onSaved,
|
||||
validator: validator,
|
||||
initialValue: textFieldConfiguration.controller != null
|
||||
? textFieldConfiguration.controller!.text
|
||||
: (initialValue ?? ''),
|
||||
enabled: enabled,
|
||||
autovalidateMode: autovalidateMode,
|
||||
builder: (FormFieldState<String> field) {
|
||||
final CupertinoTypeAheadFormFieldState state =
|
||||
field as CupertinoTypeAheadFormFieldState<dynamic>;
|
||||
|
||||
return CupertinoTypeAheadField(
|
||||
getImmediateSuggestions: getImmediateSuggestions,
|
||||
transitionBuilder: transitionBuilder,
|
||||
errorBuilder: errorBuilder,
|
||||
noItemsFoundBuilder: noItemsFoundBuilder,
|
||||
loadingBuilder: loadingBuilder,
|
||||
debounceDuration: debounceDuration,
|
||||
suggestionsBoxDecoration: suggestionsBoxDecoration,
|
||||
suggestionsBoxController: suggestionsBoxController,
|
||||
textFieldConfiguration: textFieldConfiguration.copyWith(
|
||||
onChanged: (text) {
|
||||
state.didChange(text);
|
||||
textFieldConfiguration.onChanged?.call(text);
|
||||
},
|
||||
controller: state._effectiveController,
|
||||
),
|
||||
suggestionsBoxVerticalOffset: suggestionsBoxVerticalOffset,
|
||||
onSuggestionSelected: onSuggestionSelected,
|
||||
itemBuilder: itemBuilder,
|
||||
suggestionsCallback: suggestionsCallback,
|
||||
animationStart: animationStart,
|
||||
animationDuration: animationDuration,
|
||||
direction: direction,
|
||||
hideOnLoading: hideOnLoading,
|
||||
hideOnEmpty: hideOnEmpty,
|
||||
hideOnError: hideOnError,
|
||||
hideSuggestionsOnKeyboardHide: hideSuggestionsOnKeyboardHide,
|
||||
keepSuggestionsOnLoading: keepSuggestionsOnLoading,
|
||||
keepSuggestionsOnSuggestionSelected:
|
||||
keepSuggestionsOnSuggestionSelected,
|
||||
autoFlipDirection: autoFlipDirection,
|
||||
autoFlipListDirection: autoFlipListDirection,
|
||||
minCharsForSuggestions: minCharsForSuggestions,
|
||||
hideKeyboardOnDrag: hideKeyboardOnDrag,
|
||||
);
|
||||
});
|
||||
|
||||
@override
|
||||
CupertinoTypeAheadFormFieldState<T> createState() =>
|
||||
CupertinoTypeAheadFormFieldState<T>();
|
||||
}
|
||||
|
||||
class CupertinoTypeAheadFormFieldState<T> extends FormFieldState<String> {
|
||||
TextEditingController? _controller;
|
||||
|
||||
TextEditingController? get _effectiveController =>
|
||||
widget.textFieldConfiguration.controller ?? _controller;
|
||||
|
||||
@override
|
||||
CupertinoTypeAheadFormField get widget =>
|
||||
super.widget as CupertinoTypeAheadFormField<dynamic>;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.textFieldConfiguration.controller == null) {
|
||||
_controller = TextEditingController(text: widget.initialValue);
|
||||
} else {
|
||||
widget.textFieldConfiguration.controller!
|
||||
.addListener(_handleControllerChanged);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CupertinoTypeAheadFormField oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.textFieldConfiguration.controller !=
|
||||
oldWidget.textFieldConfiguration.controller) {
|
||||
oldWidget.textFieldConfiguration.controller
|
||||
?.removeListener(_handleControllerChanged);
|
||||
widget.textFieldConfiguration.controller
|
||||
?.addListener(_handleControllerChanged);
|
||||
|
||||
if (oldWidget.textFieldConfiguration.controller != null &&
|
||||
widget.textFieldConfiguration.controller == null)
|
||||
_controller = TextEditingController.fromValue(
|
||||
oldWidget.textFieldConfiguration.controller!.value);
|
||||
if (widget.textFieldConfiguration.controller != null) {
|
||||
setValue(widget.textFieldConfiguration.controller!.text);
|
||||
if (oldWidget.textFieldConfiguration.controller == null)
|
||||
_controller = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.textFieldConfiguration.controller
|
||||
?.removeListener(_handleControllerChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void reset() {
|
||||
super.reset();
|
||||
setState(() {
|
||||
_effectiveController!.text = widget.initialValue!;
|
||||
});
|
||||
}
|
||||
|
||||
void _handleControllerChanged() {
|
||||
// Suppress changes that originated from within this class.
|
||||
//
|
||||
// In the case where a controller has been passed in to this widget, we
|
||||
// register this change listener. In these cases, we'll also receive change
|
||||
// notifications for changes originating from within this class -- for
|
||||
// example, the reset() method. In such cases, the FormField value will
|
||||
// already have been set.
|
||||
if (_effectiveController!.text != value)
|
||||
didChange(_effectiveController!.text);
|
||||
}
|
||||
}
|
212
lib/src/cupertino/suggestions_box/cupertino_suggestions_box.dart
Normal file
@ -0,0 +1,212 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_typeahead/src/cupertino/field/cupertino_typeahead_field.dart';
|
||||
|
||||
class CupertinoSuggestionsBox {
|
||||
static const int waitMetricsTimeoutMillis = 1000;
|
||||
static const double minOverlaySpace = 64.0;
|
||||
|
||||
final BuildContext context;
|
||||
final AxisDirection desiredDirection;
|
||||
final bool autoFlipDirection;
|
||||
final bool autoFlipListDirection;
|
||||
|
||||
OverlayEntry? overlayEntry;
|
||||
AxisDirection direction;
|
||||
|
||||
bool isOpened = false;
|
||||
bool widgetMounted = true;
|
||||
double maxHeight = 300.0;
|
||||
double textBoxWidth = 100.0;
|
||||
double textBoxHeight = 100.0;
|
||||
late double directionUpOffset;
|
||||
|
||||
CupertinoSuggestionsBox(
|
||||
this.context,
|
||||
this.direction,
|
||||
this.autoFlipDirection,
|
||||
this.autoFlipListDirection,
|
||||
) : desiredDirection = direction;
|
||||
|
||||
void open() {
|
||||
if (this.isOpened) return;
|
||||
assert(this.overlayEntry != null);
|
||||
resize();
|
||||
Overlay.of(context).insert(this.overlayEntry!);
|
||||
this.isOpened = true;
|
||||
}
|
||||
|
||||
void close() {
|
||||
if (!this.isOpened) return;
|
||||
assert(this.overlayEntry != null);
|
||||
this.overlayEntry!.remove();
|
||||
this.isOpened = false;
|
||||
}
|
||||
|
||||
void toggle() {
|
||||
if (this.isOpened) {
|
||||
this.close();
|
||||
} else {
|
||||
this.open();
|
||||
}
|
||||
}
|
||||
|
||||
MediaQuery? _findRootMediaQuery() {
|
||||
MediaQuery? rootMediaQuery;
|
||||
context.visitAncestorElements((element) {
|
||||
if (element.widget is MediaQuery) {
|
||||
rootMediaQuery = element.widget as MediaQuery;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return rootMediaQuery;
|
||||
}
|
||||
|
||||
/// Delays until the keyboard has toggled or the orientation has fully changed
|
||||
Future<bool> _waitChangeMetrics() async {
|
||||
if (widgetMounted) {
|
||||
// initial viewInsets which are before the keyboard is toggled
|
||||
EdgeInsets initial = MediaQuery.of(context).viewInsets;
|
||||
// initial MediaQuery for orientation change
|
||||
MediaQuery? initialRootMediaQuery = _findRootMediaQuery();
|
||||
|
||||
int timer = 0;
|
||||
// viewInsets or MediaQuery have changed once keyboard has toggled or orientation has changed
|
||||
while (widgetMounted && timer < waitMetricsTimeoutMillis) {
|
||||
// TODO: reduce delay if showDialog ever exposes detection of animation end
|
||||
await Future<void>.delayed(const Duration(milliseconds: 170));
|
||||
timer += 170;
|
||||
|
||||
if (widgetMounted &&
|
||||
(MediaQuery.of(context).viewInsets != initial ||
|
||||
_findRootMediaQuery() != initialRootMediaQuery)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void resize() {
|
||||
// check to see if widget is still mounted
|
||||
// user may have closed the widget with the keyboard still open
|
||||
if (widgetMounted) {
|
||||
_adjustMaxHeightAndOrientation();
|
||||
overlayEntry!.markNeedsBuild();
|
||||
}
|
||||
}
|
||||
|
||||
// See if there's enough room in the desired direction for the overlay to display
|
||||
// correctly. If not, try the opposite direction if things look more roomy there
|
||||
void _adjustMaxHeightAndOrientation() {
|
||||
CupertinoTypeAheadField widget = context.widget as CupertinoTypeAheadField;
|
||||
|
||||
RenderBox box = context.findRenderObject() as RenderBox;
|
||||
textBoxWidth = box.size.width;
|
||||
textBoxHeight = box.size.height;
|
||||
|
||||
// top of text box
|
||||
double textBoxAbsY = box.localToGlobal(Offset.zero).dy;
|
||||
|
||||
// height of window
|
||||
double windowHeight = MediaQuery.of(context).size.height;
|
||||
|
||||
// we need to find the root MediaQuery for the unsafe area height
|
||||
// we cannot use BuildContext.ancestorWidgetOfExactType because
|
||||
// widgets like SafeArea creates a new MediaQuery with the padding removed
|
||||
MediaQuery rootMediaQuery = _findRootMediaQuery()!;
|
||||
|
||||
// height of keyboard
|
||||
double keyboardHeight = rootMediaQuery.data.viewInsets.bottom;
|
||||
|
||||
double maxHDesired = _calculateMaxHeight(desiredDirection, box, widget,
|
||||
windowHeight, rootMediaQuery, keyboardHeight, textBoxAbsY);
|
||||
|
||||
// if there's enough room in the desired direction, update the direction and the max height
|
||||
if (maxHDesired >= minOverlaySpace || !autoFlipDirection) {
|
||||
direction = desiredDirection;
|
||||
maxHeight = maxHDesired;
|
||||
} else {
|
||||
// There's not enough room in the desired direction so see how much room is in the opposite direction
|
||||
AxisDirection flipped = flipAxisDirection(desiredDirection);
|
||||
double maxHFlipped = _calculateMaxHeight(flipped, box, widget,
|
||||
windowHeight, rootMediaQuery, keyboardHeight, textBoxAbsY);
|
||||
|
||||
// if there's more room in this opposite direction, update the direction and maxHeight
|
||||
if (maxHFlipped > maxHDesired) {
|
||||
direction = flipped;
|
||||
maxHeight = maxHFlipped;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxHeight < 0) maxHeight = 0;
|
||||
}
|
||||
|
||||
double _calculateMaxHeight(
|
||||
AxisDirection direction,
|
||||
RenderBox box,
|
||||
CupertinoTypeAheadField widget,
|
||||
double windowHeight,
|
||||
MediaQuery rootMediaQuery,
|
||||
double keyboardHeight,
|
||||
double textBoxAbsY) {
|
||||
return direction == AxisDirection.down
|
||||
? _calculateMaxHeightDown(box, widget, windowHeight, rootMediaQuery,
|
||||
keyboardHeight, textBoxAbsY)
|
||||
: _calculateMaxHeightUp(box, widget, windowHeight, rootMediaQuery,
|
||||
keyboardHeight, textBoxAbsY);
|
||||
}
|
||||
|
||||
double _calculateMaxHeightDown(
|
||||
RenderBox box,
|
||||
CupertinoTypeAheadField widget,
|
||||
double windowHeight,
|
||||
MediaQuery rootMediaQuery,
|
||||
double keyboardHeight,
|
||||
double textBoxAbsY) {
|
||||
// unsafe area, ie: iPhone X 'home button'
|
||||
// keyboardHeight includes unsafeAreaHeight, if keyboard is showing, set to 0
|
||||
double unsafeAreaHeight =
|
||||
keyboardHeight == 0 ? rootMediaQuery.data.padding.bottom : 0;
|
||||
|
||||
return windowHeight -
|
||||
keyboardHeight -
|
||||
unsafeAreaHeight -
|
||||
textBoxHeight -
|
||||
textBoxAbsY -
|
||||
2 * widget.suggestionsBoxVerticalOffset;
|
||||
}
|
||||
|
||||
double _calculateMaxHeightUp(
|
||||
RenderBox box,
|
||||
CupertinoTypeAheadField widget,
|
||||
double windowHeight,
|
||||
MediaQuery rootMediaQuery,
|
||||
double keyboardHeight,
|
||||
double textBoxAbsY) {
|
||||
// recalculate keyboard absolute y value
|
||||
double keyboardAbsY = windowHeight - keyboardHeight;
|
||||
|
||||
directionUpOffset = textBoxAbsY > keyboardAbsY
|
||||
? keyboardAbsY - textBoxAbsY - widget.suggestionsBoxVerticalOffset
|
||||
: -widget.suggestionsBoxVerticalOffset;
|
||||
|
||||
// unsafe area, ie: iPhone X notch
|
||||
double unsafeAreaHeight = rootMediaQuery.data.padding.top;
|
||||
|
||||
return textBoxAbsY > keyboardAbsY
|
||||
? keyboardAbsY -
|
||||
unsafeAreaHeight -
|
||||
2 * widget.suggestionsBoxVerticalOffset
|
||||
: textBoxAbsY -
|
||||
unsafeAreaHeight -
|
||||
2 * widget.suggestionsBoxVerticalOffset;
|
||||
}
|
||||
|
||||
Future<void> onChangeMetrics() async {
|
||||
if (await _waitChangeMetrics()) {
|
||||
resize();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_typeahead/src/cupertino/suggestions_box/cupertino_suggestions_box.dart';
|
||||
|
||||
/// Supply an instance of this class to the [TypeAhead.suggestionsBoxController]
|
||||
/// property to manually control the suggestions box
|
||||
class CupertinoSuggestionsBoxController {
|
||||
CupertinoSuggestionsBox? suggestionsBox;
|
||||
FocusNode? effectiveFocusNode;
|
||||
|
||||
/// Opens the suggestions box
|
||||
void open() {
|
||||
effectiveFocusNode!.requestFocus();
|
||||
}
|
||||
|
||||
/// Closes the suggestions box
|
||||
void close() {
|
||||
effectiveFocusNode!.unfocus();
|
||||
}
|
||||
|
||||
/// Opens the suggestions box if closed and vice-versa
|
||||
void toggle() {
|
||||
if (suggestionsBox!.isOpened) {
|
||||
close();
|
||||
} else {
|
||||
open();
|
||||
}
|
||||
}
|
||||
|
||||
/// Recalculates the height of the suggestions box
|
||||
void resize() {
|
||||
suggestionsBox!.resize();
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
/// Supply an instance of this class to the [TypeAhead.suggestionsBoxDecoration]
|
||||
/// property to configure the suggestions box decoration
|
||||
class CupertinoSuggestionsBoxDecoration {
|
||||
/// Defines if a scrollbar will be displayed or not.
|
||||
final bool hasScrollbar;
|
||||
|
||||
/// The constraints to be applied to the suggestions box
|
||||
final BoxConstraints? constraints;
|
||||
final Color? color;
|
||||
final BoxBorder? border;
|
||||
final BorderRadiusGeometry? borderRadius;
|
||||
|
||||
/// Adds an offset to the suggestions box
|
||||
final double offsetX;
|
||||
|
||||
/// Creates a [CupertinoSuggestionsBoxDecoration]
|
||||
const CupertinoSuggestionsBoxDecoration(
|
||||
{this.hasScrollbar = true,
|
||||
this.constraints,
|
||||
this.color,
|
||||
this.border,
|
||||
this.borderRadius,
|
||||
this.offsetX = 0.0});
|
||||
}
|
@ -0,0 +1,376 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_typeahead/src/cupertino/suggestions_box/cupertino_suggestions_box.dart';
|
||||
import 'package:flutter_typeahead/src/cupertino/suggestions_box/cupertino_suggestions_box_decoration.dart';
|
||||
import 'package:flutter_typeahead/src/typedef.dart';
|
||||
|
||||
class CupertinoSuggestionsList<T> extends StatefulWidget {
|
||||
final CupertinoSuggestionsBox? suggestionsBox;
|
||||
final TextEditingController? controller;
|
||||
final bool getImmediateSuggestions;
|
||||
final SuggestionSelectionCallback<T>? onSuggestionSelected;
|
||||
final SuggestionsCallback<T>? suggestionsCallback;
|
||||
final ItemBuilder<T>? itemBuilder;
|
||||
final CupertinoSuggestionsBoxDecoration? decoration;
|
||||
final Duration? debounceDuration;
|
||||
final WidgetBuilder? loadingBuilder;
|
||||
final WidgetBuilder? noItemsFoundBuilder;
|
||||
final ErrorBuilder? errorBuilder;
|
||||
final AnimationTransitionBuilder? transitionBuilder;
|
||||
final Duration? animationDuration;
|
||||
final double? animationStart;
|
||||
final AxisDirection? direction;
|
||||
final bool? hideOnLoading;
|
||||
final bool? hideOnEmpty;
|
||||
final bool? hideOnError;
|
||||
final bool? keepSuggestionsOnLoading;
|
||||
final int? minCharsForSuggestions;
|
||||
final bool hideKeyboardOnDrag;
|
||||
|
||||
CupertinoSuggestionsList({
|
||||
required this.suggestionsBox,
|
||||
this.controller,
|
||||
this.getImmediateSuggestions = false,
|
||||
this.onSuggestionSelected,
|
||||
this.suggestionsCallback,
|
||||
this.itemBuilder,
|
||||
this.decoration,
|
||||
this.debounceDuration,
|
||||
this.loadingBuilder,
|
||||
this.noItemsFoundBuilder,
|
||||
this.errorBuilder,
|
||||
this.transitionBuilder,
|
||||
this.animationDuration,
|
||||
this.animationStart,
|
||||
this.direction,
|
||||
this.hideOnLoading,
|
||||
this.hideOnEmpty,
|
||||
this.hideOnError,
|
||||
this.keepSuggestionsOnLoading,
|
||||
this.minCharsForSuggestions,
|
||||
this.hideKeyboardOnDrag = false,
|
||||
});
|
||||
|
||||
@override
|
||||
_CupertinoSuggestionsListState<T> createState() => _CupertinoSuggestionsListState<T>();
|
||||
}
|
||||
|
||||
class _CupertinoSuggestionsListState<T> extends State<CupertinoSuggestionsList<T>>
|
||||
with SingleTickerProviderStateMixin {
|
||||
Iterable<T>? _suggestions;
|
||||
late bool _suggestionsValid;
|
||||
late VoidCallback _controllerListener;
|
||||
Timer? _debounceTimer;
|
||||
bool? _isLoading, _isQueued;
|
||||
Object? _error;
|
||||
AnimationController? _animationController;
|
||||
String? _lastTextValue;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CupertinoSuggestionsList<T> oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_getSuggestions();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_getSuggestions();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
this._animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: widget.animationDuration,
|
||||
);
|
||||
|
||||
this._suggestionsValid = widget.minCharsForSuggestions! > 0 ? true : false;
|
||||
this._isLoading = false;
|
||||
this._isQueued = false;
|
||||
this._lastTextValue = widget.controller!.text;
|
||||
|
||||
if (widget.getImmediateSuggestions) {
|
||||
this._getSuggestions();
|
||||
}
|
||||
|
||||
this._controllerListener = () {
|
||||
// If we came here because of a change in selected text, not because of
|
||||
// actual change in text
|
||||
if (widget.controller!.text == this._lastTextValue) return;
|
||||
|
||||
this._lastTextValue = widget.controller!.text;
|
||||
|
||||
this._debounceTimer?.cancel();
|
||||
if (widget.controller!.text.length < widget.minCharsForSuggestions!) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_suggestions = null;
|
||||
_suggestionsValid = true;
|
||||
});
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
this._debounceTimer = Timer(widget.debounceDuration!, () async {
|
||||
if (this._debounceTimer!.isActive) return;
|
||||
if (_isLoading!) {
|
||||
_isQueued = true;
|
||||
return;
|
||||
}
|
||||
|
||||
await this.invalidateSuggestions();
|
||||
while (_isQueued!) {
|
||||
_isQueued = false;
|
||||
await this.invalidateSuggestions();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
widget.controller!.addListener(this._controllerListener);
|
||||
}
|
||||
|
||||
Future<void> invalidateSuggestions() async {
|
||||
_suggestionsValid = false;
|
||||
_getSuggestions();
|
||||
}
|
||||
|
||||
Future<void> _getSuggestions() async {
|
||||
if (_suggestionsValid) return;
|
||||
_suggestionsValid = true;
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
this._animationController!.forward(from: 1.0);
|
||||
|
||||
this._isLoading = true;
|
||||
this._error = null;
|
||||
});
|
||||
|
||||
Iterable<T>? suggestions;
|
||||
Object? error;
|
||||
|
||||
try {
|
||||
suggestions =
|
||||
await widget.suggestionsCallback!(widget.controller!.text);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (this.mounted) {
|
||||
// if it wasn't removed in the meantime
|
||||
setState(() {
|
||||
double? animationStart = widget.animationStart;
|
||||
// allow suggestionsCallback to return null and not throw error here
|
||||
if (error != null || suggestions?.isEmpty == true) {
|
||||
animationStart = 1.0;
|
||||
}
|
||||
this._animationController!.forward(from: animationStart);
|
||||
|
||||
this._error = error;
|
||||
this._isLoading = false;
|
||||
this._suggestions = suggestions;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController!.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
bool isEmpty =
|
||||
this._suggestions?.length == 0 && widget.controller!.text == "";
|
||||
if ((this._suggestions == null || isEmpty) && this._isLoading == false)
|
||||
return Container();
|
||||
|
||||
Widget child;
|
||||
if (this._isLoading!) {
|
||||
if (widget.hideOnLoading!) {
|
||||
child = Container(height: 0);
|
||||
} else {
|
||||
child = createLoadingWidget();
|
||||
}
|
||||
} else if (this._error != null) {
|
||||
if (widget.hideOnError!) {
|
||||
child = Container(height: 0);
|
||||
} else {
|
||||
child = createErrorWidget();
|
||||
}
|
||||
} else if (this._suggestions!.isEmpty) {
|
||||
if (widget.hideOnEmpty!) {
|
||||
child = Container(height: 0);
|
||||
} else {
|
||||
child = createNoItemsFoundWidget();
|
||||
}
|
||||
} else {
|
||||
child = createSuggestionsWidget();
|
||||
}
|
||||
|
||||
var animationChild = widget.transitionBuilder != null
|
||||
? widget.transitionBuilder!(context, child, this._animationController)
|
||||
: SizeTransition(
|
||||
axisAlignment: -1.0,
|
||||
sizeFactor: CurvedAnimation(
|
||||
parent: this._animationController!,
|
||||
curve: Curves.fastOutSlowIn),
|
||||
child: child,
|
||||
);
|
||||
|
||||
BoxConstraints constraints;
|
||||
if (widget.decoration!.constraints == null) {
|
||||
constraints = BoxConstraints(
|
||||
maxHeight: widget.suggestionsBox!.maxHeight,
|
||||
);
|
||||
} else {
|
||||
double maxHeight = min(widget.decoration!.constraints!.maxHeight,
|
||||
widget.suggestionsBox!.maxHeight);
|
||||
constraints = widget.decoration!.constraints!.copyWith(
|
||||
minHeight: min(widget.decoration!.constraints!.minHeight, maxHeight),
|
||||
maxHeight: maxHeight,
|
||||
);
|
||||
}
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: constraints,
|
||||
child: animationChild,
|
||||
);
|
||||
}
|
||||
|
||||
Widget createLoadingWidget() {
|
||||
Widget child;
|
||||
|
||||
if (widget.keepSuggestionsOnLoading! && this._suggestions != null) {
|
||||
if (this._suggestions!.isEmpty) {
|
||||
child = createNoItemsFoundWidget();
|
||||
} else {
|
||||
child = createSuggestionsWidget();
|
||||
}
|
||||
} else {
|
||||
child = widget.loadingBuilder != null
|
||||
? widget.loadingBuilder!(context)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoColors.white,
|
||||
border: Border.all(
|
||||
color: CupertinoColors.extraLightBackgroundGray,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: CupertinoActivityIndicator(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
Widget createErrorWidget() {
|
||||
return widget.errorBuilder != null
|
||||
? widget.errorBuilder!(context, this._error)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoColors.white,
|
||||
border: Border.all(
|
||||
color: CupertinoColors.extraLightBackgroundGray,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Text(
|
||||
'Error: ${this._error}',
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
color: CupertinoColors.destructiveRed,
|
||||
fontSize: 18.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget createNoItemsFoundWidget() {
|
||||
return widget.noItemsFoundBuilder != null
|
||||
? widget.noItemsFoundBuilder!(context)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoColors.white,
|
||||
border: Border.all(
|
||||
color: CupertinoColors.extraLightBackgroundGray,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Text(
|
||||
'No Items Found!',
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
color: CupertinoColors.inactiveGray,
|
||||
fontSize: 18.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget createSuggestionsWidget() {
|
||||
Widget child = Container(
|
||||
decoration: BoxDecoration(
|
||||
color: widget.decoration!.color != null
|
||||
? widget.decoration!.color
|
||||
: CupertinoColors.white,
|
||||
border: widget.decoration!.border != null
|
||||
? widget.decoration!.border
|
||||
: Border.all(
|
||||
color: CupertinoColors.extraLightBackgroundGray,
|
||||
width: 1.0,
|
||||
),
|
||||
borderRadius: widget.decoration!.borderRadius != null
|
||||
? widget.decoration!.borderRadius
|
||||
: null,
|
||||
),
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
primary: false,
|
||||
shrinkWrap: true,
|
||||
keyboardDismissBehavior: widget.hideKeyboardOnDrag
|
||||
? ScrollViewKeyboardDismissBehavior.onDrag
|
||||
: ScrollViewKeyboardDismissBehavior.manual,
|
||||
reverse: widget.suggestionsBox!.direction == AxisDirection.down
|
||||
? false
|
||||
: widget.suggestionsBox!.autoFlipListDirection,
|
||||
children: this._suggestions!.map((T suggestion) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: widget.itemBuilder!(context, suggestion),
|
||||
onTap: () {
|
||||
widget.onSuggestionSelected!(suggestion);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.decoration!.hasScrollbar) {
|
||||
child = CupertinoScrollbar(child: child);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
}
|
21
lib/src/keyboard_suggestion_selection_notifier.dart
Normal file
@ -0,0 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class KeyboardSuggestionSelectionNotifier
|
||||
extends ValueNotifier<LogicalKeyboardKey?> {
|
||||
KeyboardSuggestionSelectionNotifier() : super(null);
|
||||
|
||||
void onKeyboardEvent(RawKeyEvent event) {
|
||||
// * we only handle key down event
|
||||
if (event.runtimeType == RawKeyUpEvent) return;
|
||||
|
||||
if (event.logicalKey == LogicalKeyboardKey.arrowDown ||
|
||||
event.logicalKey == LogicalKeyboardKey.arrowUp) {
|
||||
if (value == event.logicalKey) {
|
||||
notifyListeners();
|
||||
} else {
|
||||
value = event.logicalKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
274
lib/src/material/field/text_field_configuration.dart
Normal file
@ -0,0 +1,274 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Supply an instance of this class to the [TypeAhead.textFieldConfiguration]
|
||||
/// property to configure the displayed text field
|
||||
class TextFieldConfiguration {
|
||||
/// The decoration to show around the text field.
|
||||
///
|
||||
/// Same as [TextField.decoration](https://docs.flutter.io/flutter/material/TextField/decoration.html)
|
||||
final InputDecoration decoration;
|
||||
|
||||
/// Controls the text being edited.
|
||||
///
|
||||
/// If null, this widget will create its own [TextEditingController](https://docs.flutter.io/flutter/widgets/TextEditingController-class.html).
|
||||
/// A typical use case for this field in the TypeAhead widget is to set the
|
||||
/// text of the widget when a suggestion is selected. For example:
|
||||
///
|
||||
/// ```dart
|
||||
/// final _controller = TextEditingController();
|
||||
/// ...
|
||||
/// ...
|
||||
/// TypeAheadField(
|
||||
/// controller: _controller,
|
||||
/// ...
|
||||
/// ...
|
||||
/// onSuggestionSelected: (suggestion) {
|
||||
/// _controller.text = suggestion['city_name'];
|
||||
/// }
|
||||
/// )
|
||||
/// ```
|
||||
final TextEditingController? controller;
|
||||
|
||||
/// Controls whether this widget has keyboard focus.
|
||||
///
|
||||
/// Same as [TextField.focusNode](https://docs.flutter.io/flutter/material/TextField/focusNode.html)
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// The style to use for the text being edited.
|
||||
///
|
||||
/// Same as [TextField.style](https://docs.flutter.io/flutter/material/TextField/style.html)
|
||||
final TextStyle? style;
|
||||
|
||||
/// How the text being edited should be aligned horizontally.
|
||||
///
|
||||
/// Same as [TextField.textAlign](https://docs.flutter.io/flutter/material/TextField/textAlign.html)
|
||||
final TextAlign textAlign;
|
||||
|
||||
/// Same as [TextField.textDirection](https://docs.flutter.io/flutter/material/TextField/textDirection.html)
|
||||
///
|
||||
/// Defaults to null
|
||||
final TextDirection? textDirection;
|
||||
|
||||
/// Same as [TextField.textAlignVertical](https://api.flutter.dev/flutter/material/TextField/textAlignVertical.html)
|
||||
final TextAlignVertical? textAlignVertical;
|
||||
|
||||
/// If false the textfield is "disabled": it ignores taps and its
|
||||
/// [decoration] is rendered in grey.
|
||||
///
|
||||
/// Same as [TextField.enabled](https://docs.flutter.io/flutter/material/TextField/enabled.html)
|
||||
final bool enabled;
|
||||
|
||||
/// Whether to show input suggestions as the user types.
|
||||
///
|
||||
/// Same as [TextField.enableSuggestions](https://api.flutter.dev/flutter/material/TextField/enableSuggestions.html)
|
||||
final bool enableSuggestions;
|
||||
|
||||
/// The type of keyboard to use for editing the text.
|
||||
///
|
||||
/// Same as [TextField.keyboardType](https://docs.flutter.io/flutter/material/TextField/keyboardType.html)
|
||||
final TextInputType keyboardType;
|
||||
|
||||
/// Whether this text field should focus itself if nothing else is already
|
||||
/// focused.
|
||||
///
|
||||
/// Same as [TextField.autofocus](https://docs.flutter.io/flutter/material/TextField/autofocus.html)
|
||||
final bool autofocus;
|
||||
|
||||
/// Optional input validation and formatting overrides.
|
||||
///
|
||||
/// Same as [TextField.inputFormatters](https://docs.flutter.io/flutter/material/TextField/inputFormatters.html)
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
|
||||
/// Whether to enable autocorrection.
|
||||
///
|
||||
/// Same as [TextField.autocorrect](https://docs.flutter.io/flutter/material/TextField/autocorrect.html)
|
||||
final bool autocorrect;
|
||||
|
||||
/// The maximum number of lines for the text to span, wrapping if necessary.
|
||||
///
|
||||
/// Same as [TextField.maxLines](https://docs.flutter.io/flutter/material/TextField/maxLines.html)
|
||||
final int? maxLines;
|
||||
|
||||
/// The minimum number of lines to occupy when the content spans fewer lines.
|
||||
///
|
||||
/// Same as [TextField.minLines](https://docs.flutter.io/flutter/material/TextField/minLines.html)
|
||||
final int? minLines;
|
||||
|
||||
/// The maximum number of characters (Unicode scalar values) to allow in the
|
||||
/// text field.
|
||||
///
|
||||
/// Same as [TextField.maxLength](https://docs.flutter.io/flutter/material/TextField/maxLength.html)
|
||||
final int? maxLength;
|
||||
|
||||
/// If true, prevents the field from allowing more than [maxLength]
|
||||
/// characters.
|
||||
///
|
||||
/// Same as [TextField.maxLengthEnforcement](https://api.flutter.dev/flutter/material/TextField/maxLengthEnforcement.html)
|
||||
final MaxLengthEnforcement? maxLengthEnforcement;
|
||||
|
||||
/// Whether to hide the text being edited (e.g., for passwords).
|
||||
///
|
||||
/// Same as [TextField.obscureText](https://docs.flutter.io/flutter/material/TextField/obscureText.html)
|
||||
final bool obscureText;
|
||||
|
||||
/// Called when the text being edited changes.
|
||||
///
|
||||
/// Same as [TextField.onChanged](https://docs.flutter.io/flutter/material/TextField/onChanged.html)
|
||||
final ValueChanged<String>? onChanged;
|
||||
|
||||
/// Called when the user indicates that they are done editing the text in the
|
||||
/// field.
|
||||
///
|
||||
/// Same as [TextField.onSubmitted](https://docs.flutter.io/flutter/material/TextField/onSubmitted.html)
|
||||
final ValueChanged<String>? onSubmitted;
|
||||
|
||||
/// The color to use when painting the cursor.
|
||||
///
|
||||
/// Same as [TextField.cursorColor](https://docs.flutter.io/flutter/material/TextField/cursorColor.html)
|
||||
final Color? cursorColor;
|
||||
|
||||
/// How rounded the corners of the cursor should be. By default, the cursor has a null Radius
|
||||
///
|
||||
/// Same as [TextField.cursorRadius](https://docs.flutter.io/flutter/material/TextField/cursorRadius.html)
|
||||
final Radius? cursorRadius;
|
||||
|
||||
/// How thick the cursor will be.
|
||||
///
|
||||
/// Same as [TextField.cursorWidth](https://docs.flutter.io/flutter/material/TextField/cursorWidth.html)
|
||||
final double cursorWidth;
|
||||
|
||||
/// The appearance of the keyboard.
|
||||
///
|
||||
/// Same as [TextField.keyboardAppearance](https://docs.flutter.io/flutter/material/TextField/keyboardAppearance.html)
|
||||
final Brightness? keyboardAppearance;
|
||||
|
||||
/// Called when the user submits editable content (e.g., user presses the "done" button on the keyboard).
|
||||
///
|
||||
/// Same as [TextField.onEditingComplete](https://docs.flutter.io/flutter/material/TextField/onEditingComplete.html)
|
||||
final VoidCallback? onEditingComplete;
|
||||
|
||||
/// Called for each distinct tap except for every second tap of a double tap.
|
||||
///
|
||||
/// Same as [TextField.onTap](https://docs.flutter.io/flutter/material/TextField/onTap.html)
|
||||
final GestureTapCallback? onTap;
|
||||
|
||||
/// Configures padding to edges surrounding a Scrollable when the Textfield scrolls into view.
|
||||
///
|
||||
/// Same as [TextField.scrollPadding](https://docs.flutter.io/flutter/material/TextField/scrollPadding.html)
|
||||
final EdgeInsets scrollPadding;
|
||||
|
||||
/// Configures how the platform keyboard will select an uppercase or lowercase keyboard.
|
||||
///
|
||||
/// Same as [TextField.TextCapitalization](https://docs.flutter.io/flutter/material/TextField/textCapitalization.html)
|
||||
final TextCapitalization textCapitalization;
|
||||
|
||||
/// The type of action button to use for the keyboard.
|
||||
///
|
||||
/// Same as [TextField.textInputAction](https://docs.flutter.io/flutter/material/TextField/textInputAction.html)
|
||||
final TextInputAction? textInputAction;
|
||||
|
||||
final bool enableInteractiveSelection;
|
||||
|
||||
/// Creates a TextFieldConfiguration
|
||||
const TextFieldConfiguration({
|
||||
this.decoration = const InputDecoration(),
|
||||
this.style,
|
||||
this.controller,
|
||||
this.onChanged,
|
||||
this.onSubmitted,
|
||||
this.obscureText = false,
|
||||
this.maxLengthEnforcement,
|
||||
this.maxLength,
|
||||
this.maxLines = 1,
|
||||
this.minLines,
|
||||
this.textAlignVertical,
|
||||
this.autocorrect = true,
|
||||
this.inputFormatters,
|
||||
this.autofocus = false,
|
||||
this.keyboardType = TextInputType.text,
|
||||
this.enabled = true,
|
||||
this.enableSuggestions = true,
|
||||
this.textAlign = TextAlign.start,
|
||||
this.focusNode,
|
||||
this.cursorColor,
|
||||
this.cursorRadius,
|
||||
this.textInputAction,
|
||||
this.textCapitalization = TextCapitalization.none,
|
||||
this.cursorWidth = 2.0,
|
||||
this.keyboardAppearance,
|
||||
this.onEditingComplete,
|
||||
this.onTap,
|
||||
this.textDirection,
|
||||
this.scrollPadding = const EdgeInsets.all(20.0),
|
||||
this.enableInteractiveSelection = true,
|
||||
});
|
||||
|
||||
/// Copies the [TextFieldConfiguration] and only changes the specified
|
||||
/// properties
|
||||
TextFieldConfiguration copyWith(
|
||||
{InputDecoration? decoration,
|
||||
TextStyle? style,
|
||||
TextEditingController? controller,
|
||||
ValueChanged<String>? onChanged,
|
||||
ValueChanged<String>? onSubmitted,
|
||||
bool? obscureText,
|
||||
MaxLengthEnforcement? maxLengthEnforcement,
|
||||
int? maxLength,
|
||||
int? maxLines,
|
||||
int? minLines,
|
||||
bool? autocorrect,
|
||||
List<TextInputFormatter>? inputFormatters,
|
||||
bool? autofocus,
|
||||
TextInputType? keyboardType,
|
||||
bool? enabled,
|
||||
bool? enableSuggestions,
|
||||
TextAlign? textAlign,
|
||||
FocusNode? focusNode,
|
||||
Color? cursorColor,
|
||||
TextAlignVertical? textAlignVertical,
|
||||
Radius? cursorRadius,
|
||||
double? cursorWidth,
|
||||
Brightness? keyboardAppearance,
|
||||
VoidCallback? onEditingComplete,
|
||||
GestureTapCallback? onTap,
|
||||
EdgeInsets? scrollPadding,
|
||||
TextCapitalization? textCapitalization,
|
||||
TextDirection? textDirection,
|
||||
TextInputAction? textInputAction,
|
||||
bool? enableInteractiveSelection}) {
|
||||
return TextFieldConfiguration(
|
||||
decoration: decoration ?? this.decoration,
|
||||
style: style ?? this.style,
|
||||
controller: controller ?? this.controller,
|
||||
onChanged: onChanged ?? this.onChanged,
|
||||
onSubmitted: onSubmitted ?? this.onSubmitted,
|
||||
obscureText: obscureText ?? this.obscureText,
|
||||
maxLengthEnforcement: maxLengthEnforcement ?? this.maxLengthEnforcement,
|
||||
maxLength: maxLength ?? this.maxLength,
|
||||
maxLines: maxLines ?? this.maxLines,
|
||||
minLines: minLines ?? this.minLines,
|
||||
autocorrect: autocorrect ?? this.autocorrect,
|
||||
inputFormatters: inputFormatters ?? this.inputFormatters,
|
||||
autofocus: autofocus ?? this.autofocus,
|
||||
keyboardType: keyboardType ?? this.keyboardType,
|
||||
enabled: enabled ?? this.enabled,
|
||||
enableSuggestions: enableSuggestions ?? this.enableSuggestions,
|
||||
textAlign: textAlign ?? this.textAlign,
|
||||
textAlignVertical: textAlignVertical ?? this.textAlignVertical,
|
||||
focusNode: focusNode ?? this.focusNode,
|
||||
cursorColor: cursorColor ?? this.cursorColor,
|
||||
cursorRadius: cursorRadius ?? this.cursorRadius,
|
||||
cursorWidth: cursorWidth ?? this.cursorWidth,
|
||||
keyboardAppearance: keyboardAppearance ?? this.keyboardAppearance,
|
||||
onEditingComplete: onEditingComplete ?? this.onEditingComplete,
|
||||
onTap: onTap ?? this.onTap,
|
||||
scrollPadding: scrollPadding ?? this.scrollPadding,
|
||||
textCapitalization: textCapitalization ?? this.textCapitalization,
|
||||
textInputAction: textInputAction ?? this.textInputAction,
|
||||
textDirection: textDirection ?? this.textDirection,
|
||||
enableInteractiveSelection:
|
||||
enableInteractiveSelection ?? this.enableInteractiveSelection,
|
||||
);
|
||||
}
|
||||
}
|
885
lib/src/material/field/typeahead_field.dart
Normal file
@ -0,0 +1,885 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||
import 'package:flutter_typeahead/src/material/field/text_field_configuration.dart';
|
||||
import 'package:flutter_typeahead/src/keyboard_suggestion_selection_notifier.dart';
|
||||
import 'package:flutter_typeahead/src/should_refresh_suggestion_focus_index_notifier.dart';
|
||||
import 'package:flutter_typeahead/src/material/suggestions_box/suggestions_box.dart';
|
||||
import 'package:flutter_typeahead/src/material/suggestions_box/suggestions_box_controller.dart';
|
||||
import 'package:flutter_typeahead/src/material/suggestions_box/suggestions_box_decoration.dart';
|
||||
import 'package:flutter_typeahead/src/material/suggestions_box/suggestions_list.dart';
|
||||
import 'package:flutter_typeahead/src/typedef.dart';
|
||||
import 'package:flutter_typeahead/src/utils.dart';
|
||||
|
||||
/// # Flutter TypeAhead
|
||||
/// A TypeAhead widget for Flutter, where you can show suggestions to
|
||||
/// users as they type
|
||||
///
|
||||
/// ## Features
|
||||
/// * Shows suggestions in an overlay that floats on top of other widgets
|
||||
/// * Allows you to specify what the suggestions will look like through a
|
||||
/// builder function
|
||||
/// * Allows you to specify what happens when the user taps a suggestion
|
||||
/// * Accepts all the parameters that traditional TextFields accept, like
|
||||
/// decoration, custom TextEditingController, text styling, etc.
|
||||
/// * Provides two versions, a normal version and a [FormField](https://docs.flutter.io/flutter/widgets/FormField-class.html)
|
||||
/// version that accepts validation, submitting, etc.
|
||||
/// * Provides high customizability; you can customize the suggestion box decoration,
|
||||
/// the loading bar, the animation, the debounce duration, etc.
|
||||
///
|
||||
/// ## Installation
|
||||
/// See the [installation instructions on pub](https://pub.dartlang.org/packages/flutter_typeahead#-installing-tab-).
|
||||
///
|
||||
/// ## Usage examples
|
||||
/// You can import the package with:
|
||||
/// ```dart
|
||||
/// import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||
/// ```
|
||||
///
|
||||
/// and then use it as follows:
|
||||
///
|
||||
/// ### Example 1:
|
||||
/// ```dart
|
||||
/// TypeAheadField(
|
||||
/// textFieldConfiguration: TextFieldConfiguration(
|
||||
/// autofocus: true,
|
||||
/// style: DefaultTextStyle.of(context).style.copyWith(
|
||||
/// fontStyle: FontStyle.italic
|
||||
/// ),
|
||||
/// decoration: InputDecoration(
|
||||
/// border: OutlineInputBorder()
|
||||
/// )
|
||||
/// ),
|
||||
/// suggestionsCallback: (pattern) async {
|
||||
/// return await BackendService.getSuggestions(pattern);
|
||||
/// },
|
||||
/// itemBuilder: (context, suggestion) {
|
||||
/// return ListTile(
|
||||
/// leading: Icon(Icons.shopping_cart),
|
||||
/// title: Text(suggestion['name']),
|
||||
/// subtitle: Text('\$${suggestion['price']}'),
|
||||
/// );
|
||||
/// },
|
||||
/// onSuggestionSelected: (suggestion) {
|
||||
/// Navigator.of(context).push(MaterialPageRoute(
|
||||
/// builder: (context) => ProductPage(product: suggestion)
|
||||
/// ));
|
||||
/// },
|
||||
/// )
|
||||
/// ```
|
||||
/// In the code above, the `textFieldConfiguration` property allows us to
|
||||
/// configure the displayed `TextField` as we want. In this example, we are
|
||||
/// configuring the `autofocus`, `style` and `decoration` properties.
|
||||
///
|
||||
/// The `suggestionsCallback` is called with the search string that the user
|
||||
/// types, and is expected to return a `List` of data either synchronously or
|
||||
/// asynchronously. In this example, we are calling an asynchronous function
|
||||
/// called `BackendService.getSuggestions` which fetches the list of
|
||||
/// suggestions.
|
||||
///
|
||||
/// The `itemBuilder` is called to build a widget for each suggestion.
|
||||
/// In this example, we build a simple `ListTile` that shows the name and the
|
||||
/// price of the item. Please note that you shouldn't provide an `onTap`
|
||||
/// callback here. The TypeAhead widget takes care of that.
|
||||
///
|
||||
/// The `onSuggestionSelected` is a callback called when the user taps a
|
||||
/// suggestion. In this example, when the user taps a
|
||||
/// suggestion, we navigate to a page that shows us the information of the
|
||||
/// tapped product.
|
||||
///
|
||||
/// ### Example 2:
|
||||
/// Here's another example, where we use the TypeAheadFormField inside a `Form`:
|
||||
/// ```dart
|
||||
/// final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||
/// final TextEditingController _typeAheadController = TextEditingController();
|
||||
/// String _selectedCity;
|
||||
/// ...
|
||||
/// Form(
|
||||
/// key: this._formKey,
|
||||
/// child: Padding(
|
||||
/// padding: EdgeInsets.all(32.0),
|
||||
/// child: Column(
|
||||
/// children: <Widget>[
|
||||
/// Text(
|
||||
/// 'What is your favorite city?'
|
||||
/// ),
|
||||
/// TypeAheadFormField(
|
||||
/// textFieldConfiguration: TextFieldConfiguration(
|
||||
/// controller: this._typeAheadController,
|
||||
/// decoration: InputDecoration(
|
||||
/// labelText: 'City'
|
||||
/// )
|
||||
/// ),
|
||||
/// suggestionsCallback: (pattern) {
|
||||
/// return CitiesService.getSuggestions(pattern);
|
||||
/// },
|
||||
/// itemBuilder: (context, suggestion) {
|
||||
/// return ListTile(
|
||||
/// title: Text(suggestion),
|
||||
/// );
|
||||
/// },
|
||||
/// transitionBuilder: (context, suggestionsBox, controller) {
|
||||
/// return suggestionsBox;
|
||||
/// },
|
||||
/// onSuggestionSelected: (suggestion) {
|
||||
/// this._typeAheadController.text = suggestion;
|
||||
/// },
|
||||
/// validator: (value) {
|
||||
/// if (value.isEmpty) {
|
||||
/// return 'Please select a city';
|
||||
/// }
|
||||
/// },
|
||||
/// onSaved: (value) => this._selectedCity = value,
|
||||
/// ),
|
||||
/// SizedBox(height: 10.0,),
|
||||
/// RaisedButton(
|
||||
/// child: Text('Submit'),
|
||||
/// onPressed: () {
|
||||
/// if (this._formKey.currentState.validate()) {
|
||||
/// this._formKey.currentState.save();
|
||||
/// Scaffold.of(context).showSnackBar(SnackBar(
|
||||
/// content: Text('Your Favorite City is ${this._selectedCity}')
|
||||
/// ));
|
||||
/// }
|
||||
/// },
|
||||
/// )
|
||||
/// ],
|
||||
/// ),
|
||||
/// ),
|
||||
/// )
|
||||
/// ```
|
||||
/// Here, we assign to the `controller` property of the `textFieldConfiguration`
|
||||
/// a `TextEditingController` that we call `_typeAheadController`.
|
||||
/// We use this controller in the `onSuggestionSelected` callback to set the
|
||||
/// value of the `TextField` to the selected suggestion.
|
||||
///
|
||||
/// The `validator` callback can be used like any `FormField.validator`
|
||||
/// function. In our example, it checks whether a value has been entered,
|
||||
/// and displays an error message if not. The `onSaved` callback is used to
|
||||
/// save the value of the field to the `_selectedCity` member variable.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// ## Customizations
|
||||
/// TypeAhead widgets consist of a TextField and a suggestion box that shows
|
||||
/// as the user types. Both are highly customizable
|
||||
///
|
||||
/// ### Customizing the TextField
|
||||
/// You can customize the text field using the `textFieldConfiguration` property.
|
||||
/// You provide this property with an instance of `TextFieldConfiguration`,
|
||||
/// which allows you to configure all the usual properties of `TextField`, like
|
||||
/// `decoration`, `style`, `controller`, `focusNode`, `autofocus`, `enabled`,
|
||||
/// etc.
|
||||
///
|
||||
/// ### Customizing the Suggestions Box
|
||||
/// TypeAhead provides default configurations for the suggestions box. You can,
|
||||
/// however, override most of them.
|
||||
///
|
||||
/// #### Customizing the loader, the error and the "no items found" message
|
||||
/// You can use the [loadingBuilder], [errorBuilder] and [noItemsFoundBuilder] to
|
||||
/// customize their corresponding widgets. For example, to show a custom error
|
||||
/// widget:
|
||||
/// ```dart
|
||||
/// errorBuilder: (BuildContext context, Object error) =>
|
||||
/// Text(
|
||||
/// '$error',
|
||||
/// style: TextStyle(
|
||||
/// color: Theme.of(context).errorColor
|
||||
/// )
|
||||
/// )
|
||||
/// ```
|
||||
/// #### Customizing the animation
|
||||
/// You can customize the suggestion box animation through 3 parameters: the
|
||||
/// `animationDuration`, the `animationStart`, and the `transitionBuilder`.
|
||||
///
|
||||
/// The `animationDuration` specifies how long the animation should take, while the
|
||||
/// `animationStart` specified what point (between 0.0 and 1.0) the animation
|
||||
/// should start from. The `transitionBuilder` accepts the `suggestionsBox` and
|
||||
/// `animationController` as parameters, and should return a widget that uses
|
||||
/// the `animationController` to animate the display of the `suggestionsBox`.
|
||||
/// For example:
|
||||
/// ```dart
|
||||
/// transitionBuilder: (context, suggestionsBox, animationController) =>
|
||||
/// FadeTransition(
|
||||
/// child: suggestionsBox,
|
||||
/// opacity: CurvedAnimation(
|
||||
/// parent: animationController,
|
||||
/// curve: Curves.fastOutSlowIn
|
||||
/// ),
|
||||
/// )
|
||||
/// ```
|
||||
/// This uses [FadeTransition](https://docs.flutter.io/flutter/widgets/FadeTransition-class.html)
|
||||
/// to fade the `suggestionsBox` into the view. Note how the
|
||||
/// `animationController` was provided as the parent of the animation.
|
||||
///
|
||||
/// In order to fully remove the animation, `transitionBuilder` should simply
|
||||
/// return the `suggestionsBox`. This callback could also be used to wrap the
|
||||
/// `suggestionsBox` with any desired widgets, not necessarily for animation.
|
||||
///
|
||||
/// #### Customizing the debounce duration
|
||||
/// The suggestions box does not fire for each character the user types. Instead,
|
||||
/// we wait until the user is idle for a duration of time, and then call the
|
||||
/// `suggestionsCallback`. The duration defaults to 300 milliseconds, but can be
|
||||
/// configured using the `debounceDuration` parameter.
|
||||
///
|
||||
/// #### Customizing the offset of the suggestions box
|
||||
/// By default, the suggestions box is displayed 5 pixels below the `TextField`.
|
||||
/// You can change this by changing the `suggestionsBoxVerticalOffset` property.
|
||||
///
|
||||
/// #### Customizing the decoration of the suggestions box
|
||||
/// You can also customize the decoration of the suggestions box using the
|
||||
/// `suggestionsBoxDecoration` property. For example, to remove the elevation
|
||||
/// of the suggestions box, you can write:
|
||||
/// ```dart
|
||||
/// suggestionsBoxDecoration: SuggestionsBoxDecoration(
|
||||
/// elevation: 0.0
|
||||
/// )
|
||||
/// ```
|
||||
/// A [FormField](https://docs.flutter.io/flutter/widgets/FormField-class.html)
|
||||
/// implementation of [TypeAheadField], that allows the value to be saved,
|
||||
/// validated, etc.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TypeAheadField], A [TextField](https://docs.flutter.io/flutter/material/TextField-class.html)
|
||||
/// that displays a list of suggestions as the user types
|
||||
class TypeAheadField<T> extends StatefulWidget {
|
||||
/// Called with the search pattern to get the search suggestions.
|
||||
///
|
||||
/// This callback must not be null. It is be called by the TypeAhead widget
|
||||
/// and provided with the search pattern. It should return a [List](https://api.dartlang.org/stable/2.0.0/dart-core/List-class.html)
|
||||
/// of suggestions either synchronously, or asynchronously (as the result of a
|
||||
/// [Future](https://api.dartlang.org/stable/dart-async/Future-class.html)).
|
||||
/// Typically, the list of suggestions should not contain more than 4 or 5
|
||||
/// entries. These entries will then be provided to [itemBuilder] to display
|
||||
/// the suggestions.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// suggestionsCallback: (pattern) async {
|
||||
/// return await _getSuggestions(pattern);
|
||||
/// }
|
||||
/// ```
|
||||
final SuggestionsCallback<T> suggestionsCallback;
|
||||
|
||||
/// Called when a suggestion is tapped.
|
||||
///
|
||||
/// This callback must not be null. It is called by the TypeAhead widget and
|
||||
/// provided with the value of the tapped suggestion.
|
||||
///
|
||||
/// For example, you might want to navigate to a specific view when the user
|
||||
/// tabs a suggestion:
|
||||
/// ```dart
|
||||
/// onSuggestionSelected: (suggestion) {
|
||||
/// Navigator.of(context).push(MaterialPageRoute(
|
||||
/// builder: (context) => SearchResult(
|
||||
/// searchItem: suggestion
|
||||
/// )
|
||||
/// ));
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Or to set the value of the text field:
|
||||
/// ```dart
|
||||
/// onSuggestionSelected: (suggestion) {
|
||||
/// _controller.text = suggestion['name'];
|
||||
/// }
|
||||
/// ```
|
||||
final SuggestionSelectionCallback<T> onSuggestionSelected;
|
||||
|
||||
/// Called for each suggestion returned by [suggestionsCallback] to build the
|
||||
/// corresponding widget.
|
||||
///
|
||||
/// This callback must not be null. It is called by the TypeAhead widget for
|
||||
/// each suggestion, and expected to build a widget to display this
|
||||
/// suggestion's info. For example:
|
||||
///
|
||||
/// ```dart
|
||||
/// itemBuilder: (context, suggestion) {
|
||||
/// return ListTile(
|
||||
/// title: Text(suggestion['name']),
|
||||
/// subtitle: Text('USD' + suggestion['price'].toString())
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
final ItemBuilder<T> itemBuilder;
|
||||
|
||||
/// used to control the scroll behavior of item-builder list
|
||||
final ScrollController? scrollController;
|
||||
|
||||
/// The decoration of the material sheet that contains the suggestions.
|
||||
///
|
||||
/// 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]
|
||||
///
|
||||
/// This is useful, because, if not set, a request for suggestions will be
|
||||
/// sent for every character that the user types.
|
||||
///
|
||||
/// This duration is set by default to 300 milliseconds
|
||||
final Duration debounceDuration;
|
||||
|
||||
/// Called when waiting for [suggestionsCallback] to return.
|
||||
///
|
||||
/// It is expected to return a widget to display while waiting.
|
||||
/// For example:
|
||||
/// ```dart
|
||||
/// (BuildContext context) {
|
||||
/// return Text('Loading...');
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// If not specified, a [CircularProgressIndicator](https://docs.flutter.io/flutter/material/CircularProgressIndicator-class.html) is shown
|
||||
final WidgetBuilder? loadingBuilder;
|
||||
|
||||
/// Called when [suggestionsCallback] returns an empty array.
|
||||
///
|
||||
/// It is expected to return a widget to display when no suggestions are
|
||||
/// avaiable.
|
||||
/// For example:
|
||||
/// ```dart
|
||||
/// (BuildContext context) {
|
||||
/// return Text('No Items Found!');
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// If not specified, a simple text is shown
|
||||
final WidgetBuilder? noItemsFoundBuilder;
|
||||
|
||||
/// Called when [suggestionsCallback] throws an exception.
|
||||
///
|
||||
/// It is called with the error object, and expected to return a widget to
|
||||
/// display when an exception is thrown
|
||||
/// For example:
|
||||
/// ```dart
|
||||
/// (BuildContext context, error) {
|
||||
/// return Text('$error');
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// If not specified, the error is shown in [ThemeData.errorColor](https://docs.flutter.io/flutter/material/ThemeData/errorColor.html)
|
||||
final ErrorBuilder? errorBuilder;
|
||||
|
||||
/// Called to display animations when [suggestionsCallback] returns suggestions
|
||||
///
|
||||
/// It is provided with the suggestions box instance and the animation
|
||||
/// controller, and expected to return some animation that uses the controller
|
||||
/// to display the suggestion box.
|
||||
///
|
||||
/// For example:
|
||||
/// ```dart
|
||||
/// transitionBuilder: (context, suggestionsBox, animationController) {
|
||||
/// return FadeTransition(
|
||||
/// child: suggestionsBox,
|
||||
/// opacity: CurvedAnimation(
|
||||
/// parent: animationController,
|
||||
/// curve: Curves.fastOutSlowIn
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
/// This argument is best used with [animationDuration] and [animationStart]
|
||||
/// to fully control the animation.
|
||||
///
|
||||
/// To fully remove the animation, just return `suggestionsBox`
|
||||
///
|
||||
/// If not specified, a [SizeTransition](https://docs.flutter.io/flutter/widgets/SizeTransition-class.html) is shown.
|
||||
final AnimationTransitionBuilder? transitionBuilder;
|
||||
|
||||
/// The duration that [transitionBuilder] animation takes.
|
||||
///
|
||||
/// This argument is best used with [transitionBuilder] and [animationStart]
|
||||
/// to fully control the animation.
|
||||
///
|
||||
/// Defaults to 500 milliseconds.
|
||||
final Duration animationDuration;
|
||||
|
||||
/// Determine the [SuggestionBox]'s direction.
|
||||
///
|
||||
/// If [AxisDirection.down], the [SuggestionBox] will be below the [TextField]
|
||||
/// and the [_SuggestionsList] will grow **down**.
|
||||
///
|
||||
/// If [AxisDirection.up], the [SuggestionBox] will be above the [TextField]
|
||||
/// and the [_SuggestionsList] will grow **up**.
|
||||
///
|
||||
/// [AxisDirection.left] and [AxisDirection.right] are not allowed.
|
||||
final AxisDirection direction;
|
||||
|
||||
/// The value at which the [transitionBuilder] animation starts.
|
||||
///
|
||||
/// This argument is best used with [transitionBuilder] and [animationDuration]
|
||||
/// to fully control the animation.
|
||||
///
|
||||
/// Defaults to 0.25.
|
||||
final double animationStart;
|
||||
|
||||
/// The configuration of the [TextField](https://docs.flutter.io/flutter/material/TextField-class.html)
|
||||
/// that the TypeAhead widget displays
|
||||
final TextFieldConfiguration textFieldConfiguration;
|
||||
|
||||
/// How far below the text field should the suggestions box be
|
||||
///
|
||||
/// Defaults to 5.0
|
||||
final double suggestionsBoxVerticalOffset;
|
||||
|
||||
/// If set to true, suggestions will be fetched immediately when the field is
|
||||
/// added to the view.
|
||||
///
|
||||
/// But the suggestions box will only be shown when the field receives focus.
|
||||
/// To make the field receive focus immediately, you can set the `autofocus`
|
||||
/// property in the [textFieldConfiguration] to true
|
||||
///
|
||||
/// Defaults to false
|
||||
final bool getImmediateSuggestions;
|
||||
|
||||
/// If set to true, no loading box will be shown while suggestions are
|
||||
/// being fetched. [loadingBuilder] will also be ignored.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool hideOnLoading;
|
||||
|
||||
/// If set to true, nothing will be shown if there are no results.
|
||||
/// [noItemsFoundBuilder] will also be ignored.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool hideOnEmpty;
|
||||
|
||||
/// If set to true, nothing will be shown if there is an error.
|
||||
/// [errorBuilder] will also be ignored.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool hideOnError;
|
||||
|
||||
/// If set to false, the suggestions box will stay opened after
|
||||
/// the keyboard is closed.
|
||||
///
|
||||
/// Defaults to true.
|
||||
final bool hideSuggestionsOnKeyboardHide;
|
||||
|
||||
/// If set to false, the suggestions box will show a circular
|
||||
/// progress indicator when retrieving suggestions.
|
||||
///
|
||||
/// Defaults to true.
|
||||
final bool keepSuggestionsOnLoading;
|
||||
|
||||
/// If set to true, the suggestions box will remain opened even after
|
||||
/// selecting a suggestion.
|
||||
///
|
||||
/// Note that if this is enabled, the only way
|
||||
/// to close the suggestions box is either manually via the
|
||||
/// `SuggestionsBoxController` or when the user closes the software
|
||||
/// keyboard if `hideSuggestionsOnKeyboardHide` is set to true. Users
|
||||
/// with a physical keyboard will be unable to close the
|
||||
/// box without a manual way via `SuggestionsBoxController`.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool keepSuggestionsOnSuggestionSelected;
|
||||
|
||||
/// If set to true, in the case where the suggestions box has less than
|
||||
/// _SuggestionsBoxController.minOverlaySpace to grow in the desired [direction], the direction axis
|
||||
/// will be temporarily flipped if there's more room available in the opposite
|
||||
/// direction.
|
||||
///
|
||||
/// Defaults to false
|
||||
final bool autoFlipDirection;
|
||||
|
||||
/// If set to false, suggestion list will not be reversed according to the
|
||||
/// [autoFlipDirection] property.
|
||||
///
|
||||
/// Defaults to true.
|
||||
final bool autoFlipListDirection;
|
||||
|
||||
final bool hideKeyboard;
|
||||
|
||||
/// The minimum number of characters which must be entered before
|
||||
/// [suggestionsCallback] is triggered.
|
||||
///
|
||||
/// Defaults to 0.
|
||||
final int minCharsForSuggestions;
|
||||
|
||||
/// If set to true and if the user scrolls through the suggestion list, hide the keyboard automatically.
|
||||
/// If set to false, the keyboard remains visible.
|
||||
/// Throws an exception, if hideKeyboardOnDrag and hideSuggestionsOnKeyboardHide are both set to true as
|
||||
/// they are mutual exclusive.
|
||||
///
|
||||
/// Defaults to false
|
||||
final bool hideKeyboardOnDrag;
|
||||
|
||||
// Adds a callback for the suggestion box opening or closing
|
||||
final void Function(bool)? onSuggestionsBoxToggle;
|
||||
|
||||
/// Creates a [TypeAheadField]
|
||||
TypeAheadField({
|
||||
required this.suggestionsCallback,
|
||||
required this.itemBuilder,
|
||||
required this.onSuggestionSelected,
|
||||
this.textFieldConfiguration = const TextFieldConfiguration(),
|
||||
this.suggestionsBoxDecoration = const SuggestionsBoxDecoration(),
|
||||
this.debounceDuration = const Duration(milliseconds: 300),
|
||||
this.suggestionsBoxController,
|
||||
this.scrollController,
|
||||
this.loadingBuilder,
|
||||
this.noItemsFoundBuilder,
|
||||
this.errorBuilder,
|
||||
this.transitionBuilder,
|
||||
this.animationStart = 0.25,
|
||||
this.animationDuration = const Duration(milliseconds: 500),
|
||||
this.getImmediateSuggestions = false,
|
||||
this.suggestionsBoxVerticalOffset = 5.0,
|
||||
this.direction = AxisDirection.down,
|
||||
this.hideOnLoading = false,
|
||||
this.hideOnEmpty = false,
|
||||
this.hideOnError = false,
|
||||
this.hideSuggestionsOnKeyboardHide = true,
|
||||
this.keepSuggestionsOnLoading = true,
|
||||
this.keepSuggestionsOnSuggestionSelected = false,
|
||||
this.autoFlipDirection = false,
|
||||
this.autoFlipListDirection = true,
|
||||
this.hideKeyboard = false,
|
||||
this.minCharsForSuggestions = 0,
|
||||
this.onSuggestionsBoxToggle,
|
||||
this.hideKeyboardOnDrag = false,
|
||||
super.key,
|
||||
}) : assert(animationStart >= 0.0 && animationStart <= 1.0),
|
||||
assert(
|
||||
direction == AxisDirection.down || direction == AxisDirection.up),
|
||||
assert(minCharsForSuggestions >= 0),
|
||||
assert(!hideKeyboardOnDrag ||
|
||||
hideKeyboardOnDrag && !hideSuggestionsOnKeyboardHide);
|
||||
|
||||
@override
|
||||
_TypeAheadFieldState<T> createState() => _TypeAheadFieldState<T>();
|
||||
}
|
||||
|
||||
class _TypeAheadFieldState<T> extends State<TypeAheadField<T>>
|
||||
with WidgetsBindingObserver {
|
||||
FocusNode? _focusNode;
|
||||
final KeyboardSuggestionSelectionNotifier
|
||||
_keyboardSuggestionSelectionNotifier =
|
||||
KeyboardSuggestionSelectionNotifier();
|
||||
TextEditingController? _textEditingController;
|
||||
SuggestionsBox? _suggestionsBox;
|
||||
|
||||
TextEditingController? get _effectiveController =>
|
||||
widget.textFieldConfiguration.controller ?? _textEditingController;
|
||||
FocusNode? get _effectiveFocusNode =>
|
||||
widget.textFieldConfiguration.focusNode ?? _focusNode;
|
||||
late VoidCallback _focusNodeListener;
|
||||
|
||||
final LayerLink _layerLink = LayerLink();
|
||||
|
||||
// Timer that resizes the suggestion box on each tick. Only active when the user is scrolling.
|
||||
Timer? _resizeOnScrollTimer;
|
||||
// The rate at which the suggestion box will resize when the user is scrolling
|
||||
final Duration _resizeOnScrollRefreshRate = const Duration(milliseconds: 500);
|
||||
// Will have a value if the typeahead is inside a scrollable widget
|
||||
ScrollPosition? _scrollPosition;
|
||||
|
||||
// Keyboard detection
|
||||
final Stream<bool>? _keyboardVisibility =
|
||||
(supportedPlatform) ? KeyboardVisibilityController().onChange : null;
|
||||
late StreamSubscription<bool>? _keyboardVisibilitySubscription;
|
||||
|
||||
bool _areSuggestionsFocused = false;
|
||||
late final _shouldRefreshSuggestionsFocusIndex =
|
||||
ShouldRefreshSuggestionFocusIndexNotifier(
|
||||
textFieldFocusNode: _effectiveFocusNode);
|
||||
|
||||
@override
|
||||
void didChangeMetrics() {
|
||||
// Catch keyboard event and orientation change; resize suggestions list
|
||||
this._suggestionsBox!.onChangeMetrics();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
this._suggestionsBox!.close();
|
||||
this._suggestionsBox!.widgetMounted = false;
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_keyboardVisibilitySubscription?.cancel();
|
||||
_effectiveFocusNode!.removeListener(_focusNodeListener);
|
||||
_focusNode?.dispose();
|
||||
_resizeOnScrollTimer?.cancel();
|
||||
_scrollPosition?.removeListener(_scrollResizeListener);
|
||||
_textEditingController?.dispose();
|
||||
_keyboardSuggestionSelectionNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
KeyEventResult _onKeyEvent(FocusNode _, RawKeyEvent event) {
|
||||
if (event.isKeyPressed(LogicalKeyboardKey.arrowUp) ||
|
||||
event.isKeyPressed(LogicalKeyboardKey.arrowDown)) {
|
||||
// do nothing to avoid puzzling users until keyboard arrow nav is implemented
|
||||
} else {
|
||||
_keyboardSuggestionSelectionNotifier.onKeyboardEvent(event);
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
if (widget.textFieldConfiguration.controller == null) {
|
||||
this._textEditingController = TextEditingController();
|
||||
}
|
||||
|
||||
final textFieldConfigurationFocusNode =
|
||||
widget.textFieldConfiguration.focusNode;
|
||||
if (textFieldConfigurationFocusNode == null) {
|
||||
this._focusNode = FocusNode(onKey: _onKeyEvent);
|
||||
} else if (textFieldConfigurationFocusNode.onKey == null) {
|
||||
// * we add the _onKeyEvent callback to the textFieldConfiguration focusNode
|
||||
textFieldConfigurationFocusNode.onKey = ((node, event) {
|
||||
final keyEventResult = _onKeyEvent(node, event);
|
||||
return keyEventResult;
|
||||
});
|
||||
} else {
|
||||
final onKeyCopy = textFieldConfigurationFocusNode.onKey!;
|
||||
textFieldConfigurationFocusNode.onKey = ((node, event) {
|
||||
_onKeyEvent(node, event);
|
||||
return onKeyCopy(node, event);
|
||||
});
|
||||
}
|
||||
|
||||
this._suggestionsBox = SuggestionsBox(
|
||||
context,
|
||||
widget.direction,
|
||||
widget.autoFlipDirection,
|
||||
widget.autoFlipListDirection,
|
||||
);
|
||||
|
||||
widget.suggestionsBoxController?.suggestionsBox = this._suggestionsBox;
|
||||
widget.suggestionsBoxController?.effectiveFocusNode =
|
||||
this._effectiveFocusNode;
|
||||
|
||||
this._focusNodeListener = () {
|
||||
if (_effectiveFocusNode!.hasFocus) {
|
||||
this._suggestionsBox!.open();
|
||||
} else if (!_areSuggestionsFocused) {
|
||||
if (widget.hideSuggestionsOnKeyboardHide) {
|
||||
this._suggestionsBox!.close();
|
||||
}
|
||||
}
|
||||
|
||||
widget.onSuggestionsBoxToggle?.call(this._suggestionsBox!.isOpened);
|
||||
};
|
||||
|
||||
this._effectiveFocusNode!.addListener(_focusNodeListener);
|
||||
|
||||
// hide suggestions box on keyboard closed
|
||||
this._keyboardVisibilitySubscription =
|
||||
_keyboardVisibility?.listen((bool isVisible) {
|
||||
if (widget.hideSuggestionsOnKeyboardHide && !isVisible) {
|
||||
_effectiveFocusNode!.unfocus();
|
||||
}
|
||||
});
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((duration) {
|
||||
if (mounted) {
|
||||
this._initOverlayEntry();
|
||||
// calculate initial suggestions list size
|
||||
this._suggestionsBox!.resize();
|
||||
|
||||
// in case we already missed the focus event
|
||||
if (this._effectiveFocusNode!.hasFocus) {
|
||||
this._suggestionsBox!.open();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final scrollableState = Scrollable.maybeOf(context);
|
||||
if (scrollableState != null) {
|
||||
// The TypeAheadField is inside a scrollable widget
|
||||
_scrollPosition = scrollableState.position;
|
||||
|
||||
_scrollPosition!.removeListener(_scrollResizeListener);
|
||||
_scrollPosition!.isScrollingNotifier.addListener(_scrollResizeListener);
|
||||
}
|
||||
}
|
||||
|
||||
void _scrollResizeListener() {
|
||||
bool isScrolling = _scrollPosition!.isScrollingNotifier.value;
|
||||
_resizeOnScrollTimer?.cancel();
|
||||
if (isScrolling) {
|
||||
// Scroll started
|
||||
_resizeOnScrollTimer =
|
||||
Timer.periodic(_resizeOnScrollRefreshRate, (timer) {
|
||||
_suggestionsBox!.resize();
|
||||
});
|
||||
} else {
|
||||
// Scroll finished
|
||||
_suggestionsBox!.resize();
|
||||
}
|
||||
}
|
||||
|
||||
void _initOverlayEntry() {
|
||||
this._suggestionsBox!.overlayEntry = OverlayEntry(builder: (context) {
|
||||
void giveTextFieldFocus() {
|
||||
_effectiveFocusNode?.requestFocus();
|
||||
_areSuggestionsFocused = false;
|
||||
}
|
||||
|
||||
void onSuggestionFocus() {
|
||||
if (!_areSuggestionsFocused) {
|
||||
_areSuggestionsFocused = true;
|
||||
}
|
||||
}
|
||||
|
||||
final suggestionsList = SuggestionsList<T>(
|
||||
suggestionsBox: _suggestionsBox,
|
||||
decoration: widget.suggestionsBoxDecoration,
|
||||
debounceDuration: widget.debounceDuration,
|
||||
controller: this._effectiveController,
|
||||
loadingBuilder: widget.loadingBuilder,
|
||||
scrollController: widget.scrollController,
|
||||
noItemsFoundBuilder: widget.noItemsFoundBuilder,
|
||||
errorBuilder: widget.errorBuilder,
|
||||
transitionBuilder: widget.transitionBuilder,
|
||||
suggestionsCallback: widget.suggestionsCallback,
|
||||
animationDuration: widget.animationDuration,
|
||||
animationStart: widget.animationStart,
|
||||
getImmediateSuggestions: widget.getImmediateSuggestions,
|
||||
onSuggestionSelected: (T selection) {
|
||||
if (!widget.keepSuggestionsOnSuggestionSelected) {
|
||||
this._effectiveFocusNode!.unfocus();
|
||||
this._suggestionsBox!.close();
|
||||
}
|
||||
widget.onSuggestionSelected(selection);
|
||||
},
|
||||
itemBuilder: widget.itemBuilder,
|
||||
direction: _suggestionsBox!.direction,
|
||||
hideOnLoading: widget.hideOnLoading,
|
||||
hideOnEmpty: widget.hideOnEmpty,
|
||||
hideOnError: widget.hideOnError,
|
||||
keepSuggestionsOnLoading: widget.keepSuggestionsOnLoading,
|
||||
minCharsForSuggestions: widget.minCharsForSuggestions,
|
||||
keyboardSuggestionSelectionNotifier:
|
||||
_keyboardSuggestionSelectionNotifier,
|
||||
shouldRefreshSuggestionFocusIndexNotifier:
|
||||
_shouldRefreshSuggestionsFocusIndex,
|
||||
giveTextFieldFocus: giveTextFieldFocus,
|
||||
onSuggestionFocus: onSuggestionFocus,
|
||||
onKeyEvent: _onKeyEvent,
|
||||
hideKeyboardOnDrag: widget.hideKeyboardOnDrag);
|
||||
|
||||
double w = _suggestionsBox!.textBoxWidth;
|
||||
if (widget.suggestionsBoxDecoration.constraints != null) {
|
||||
if (widget.suggestionsBoxDecoration.constraints!.minWidth != 0.0 &&
|
||||
widget.suggestionsBoxDecoration.constraints!.maxWidth !=
|
||||
double.infinity) {
|
||||
w = (widget.suggestionsBoxDecoration.constraints!.minWidth +
|
||||
widget.suggestionsBoxDecoration.constraints!.maxWidth) /
|
||||
2;
|
||||
} else if (widget.suggestionsBoxDecoration.constraints!.minWidth !=
|
||||
0.0 &&
|
||||
widget.suggestionsBoxDecoration.constraints!.minWidth > w) {
|
||||
w = widget.suggestionsBoxDecoration.constraints!.minWidth;
|
||||
} else if (widget.suggestionsBoxDecoration.constraints!.maxWidth !=
|
||||
double.infinity &&
|
||||
widget.suggestionsBoxDecoration.constraints!.maxWidth < w) {
|
||||
w = widget.suggestionsBoxDecoration.constraints!.maxWidth;
|
||||
}
|
||||
}
|
||||
|
||||
final Widget compositedFollower = CompositedTransformFollower(
|
||||
link: this._layerLink,
|
||||
showWhenUnlinked: false,
|
||||
offset: Offset(
|
||||
widget.suggestionsBoxDecoration.offsetX,
|
||||
_suggestionsBox!.direction == AxisDirection.down
|
||||
? _suggestionsBox!.textBoxHeight +
|
||||
widget.suggestionsBoxVerticalOffset
|
||||
: _suggestionsBox!.directionUpOffset),
|
||||
child: _suggestionsBox!.direction == AxisDirection.down
|
||||
? suggestionsList
|
||||
: FractionalTranslation(
|
||||
translation: Offset(0.0, -1.0), // visually flips list to go up
|
||||
child: suggestionsList,
|
||||
),
|
||||
);
|
||||
|
||||
// When wrapped in the Positioned widget, the suggestions box widget
|
||||
// is placed before the Scaffold semantically. In order to have the
|
||||
// suggestions box navigable from the search input or keyboard,
|
||||
// Semantics > Align > ConstrainedBox are needed. This does not change
|
||||
// the style visually. However, when VO/TB are not enabled it is
|
||||
// necessary to use the Positioned widget to allow the elements to be
|
||||
// properly tappable.
|
||||
return MediaQuery.of(context).accessibleNavigation
|
||||
? Semantics(
|
||||
container: true,
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: w),
|
||||
child: compositedFollower,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Positioned(
|
||||
width: w,
|
||||
child: compositedFollower,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CompositedTransformTarget(
|
||||
link: this._layerLink,
|
||||
child: TextField(
|
||||
focusNode: this._effectiveFocusNode,
|
||||
controller: this._effectiveController,
|
||||
decoration: widget.textFieldConfiguration.decoration,
|
||||
style: widget.textFieldConfiguration.style,
|
||||
textAlign: widget.textFieldConfiguration.textAlign,
|
||||
enabled: widget.textFieldConfiguration.enabled,
|
||||
keyboardType: widget.textFieldConfiguration.keyboardType,
|
||||
autofocus: widget.textFieldConfiguration.autofocus,
|
||||
inputFormatters: widget.textFieldConfiguration.inputFormatters,
|
||||
autocorrect: widget.textFieldConfiguration.autocorrect,
|
||||
maxLines: widget.textFieldConfiguration.maxLines,
|
||||
textAlignVertical: widget.textFieldConfiguration.textAlignVertical,
|
||||
minLines: widget.textFieldConfiguration.minLines,
|
||||
maxLength: widget.textFieldConfiguration.maxLength,
|
||||
maxLengthEnforcement:
|
||||
widget.textFieldConfiguration.maxLengthEnforcement,
|
||||
obscureText: widget.textFieldConfiguration.obscureText,
|
||||
onChanged: widget.textFieldConfiguration.onChanged,
|
||||
onSubmitted: widget.textFieldConfiguration.onSubmitted,
|
||||
onEditingComplete: widget.textFieldConfiguration.onEditingComplete,
|
||||
onTap: widget.textFieldConfiguration.onTap,
|
||||
// onTapOutside: (_) {},
|
||||
scrollPadding: widget.textFieldConfiguration.scrollPadding,
|
||||
textInputAction: widget.textFieldConfiguration.textInputAction,
|
||||
textCapitalization: widget.textFieldConfiguration.textCapitalization,
|
||||
keyboardAppearance: widget.textFieldConfiguration.keyboardAppearance,
|
||||
cursorWidth: widget.textFieldConfiguration.cursorWidth,
|
||||
cursorRadius: widget.textFieldConfiguration.cursorRadius,
|
||||
cursorColor: widget.textFieldConfiguration.cursorColor,
|
||||
textDirection: widget.textFieldConfiguration.textDirection,
|
||||
enableInteractiveSelection:
|
||||
widget.textFieldConfiguration.enableInteractiveSelection,
|
||||
readOnly: widget.hideKeyboard,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
199
lib/src/material/field/typeahead_form_field.dart
Normal file
@ -0,0 +1,199 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_typeahead/src/material/field/text_field_configuration.dart';
|
||||
import 'package:flutter_typeahead/src/material/field/typeahead_field.dart';
|
||||
import 'package:flutter_typeahead/src/material/suggestions_box/suggestions_box_controller.dart';
|
||||
import 'package:flutter_typeahead/src/material/suggestions_box/suggestions_box_decoration.dart';
|
||||
import 'package:flutter_typeahead/src/typedef.dart';
|
||||
|
||||
|
||||
/// A [FormField](https://docs.flutter.io/flutter/widgets/FormField-class.html)
|
||||
/// implementation of [TypeAheadField], that allows the value to be saved,
|
||||
/// validated, etc.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TypeAheadField], A [TextField](https://docs.flutter.io/flutter/material/TextField-class.html)
|
||||
/// that displays a list of suggestions as the user types
|
||||
class TypeAheadFormField<T> extends FormField<String> {
|
||||
/// The configuration of the [TextField](https://docs.flutter.io/flutter/material/TextField-class.html)
|
||||
/// that the TypeAhead widget displays
|
||||
final TextFieldConfiguration textFieldConfiguration;
|
||||
|
||||
// Adds a callback for resetting the form field
|
||||
final void Function()? onReset;
|
||||
|
||||
/// Creates a [TypeAheadFormField]
|
||||
TypeAheadFormField(
|
||||
{Key? key,
|
||||
String? initialValue,
|
||||
bool getImmediateSuggestions = false,
|
||||
@Deprecated('Use autovalidateMode parameter which provides more specific '
|
||||
'behavior related to auto validation. '
|
||||
'This feature was deprecated after Flutter v1.19.0.')
|
||||
bool autovalidate = false,
|
||||
bool enabled = true,
|
||||
AutovalidateMode autovalidateMode = AutovalidateMode.disabled,
|
||||
FormFieldSetter<String>? onSaved,
|
||||
this.onReset,
|
||||
FormFieldValidator<String>? validator,
|
||||
ErrorBuilder? errorBuilder,
|
||||
WidgetBuilder? noItemsFoundBuilder,
|
||||
WidgetBuilder? loadingBuilder,
|
||||
void Function(bool)? onSuggestionsBoxToggle,
|
||||
Duration debounceDuration = const Duration(milliseconds: 300),
|
||||
SuggestionsBoxDecoration suggestionsBoxDecoration =
|
||||
const SuggestionsBoxDecoration(),
|
||||
SuggestionsBoxController? suggestionsBoxController,
|
||||
required SuggestionSelectionCallback<T> onSuggestionSelected,
|
||||
required ItemBuilder<T> itemBuilder,
|
||||
required SuggestionsCallback<T> suggestionsCallback,
|
||||
double suggestionsBoxVerticalOffset = 5.0,
|
||||
this.textFieldConfiguration = const TextFieldConfiguration(),
|
||||
AnimationTransitionBuilder? transitionBuilder,
|
||||
Duration animationDuration = const Duration(milliseconds: 500),
|
||||
double animationStart = 0.25,
|
||||
AxisDirection direction = AxisDirection.down,
|
||||
bool hideOnLoading = false,
|
||||
bool hideOnEmpty = false,
|
||||
bool hideOnError = false,
|
||||
bool hideSuggestionsOnKeyboardHide = true,
|
||||
bool keepSuggestionsOnLoading = true,
|
||||
bool keepSuggestionsOnSuggestionSelected = false,
|
||||
bool autoFlipDirection = false,
|
||||
bool autoFlipListDirection = true,
|
||||
bool hideKeyboard = false,
|
||||
int minCharsForSuggestions = 0,
|
||||
bool hideKeyboardOnDrag = false})
|
||||
: assert(
|
||||
initialValue == null || textFieldConfiguration.controller == null),
|
||||
assert(minCharsForSuggestions >= 0),
|
||||
super(
|
||||
key: key,
|
||||
onSaved: onSaved,
|
||||
validator: validator,
|
||||
initialValue: textFieldConfiguration.controller != null
|
||||
? textFieldConfiguration.controller!.text
|
||||
: (initialValue ?? ''),
|
||||
enabled: enabled,
|
||||
autovalidateMode: autovalidateMode,
|
||||
builder: (FormFieldState<String> field) {
|
||||
final _TypeAheadFormFieldState state =
|
||||
field as _TypeAheadFormFieldState<dynamic>;
|
||||
|
||||
return TypeAheadField(
|
||||
getImmediateSuggestions: getImmediateSuggestions,
|
||||
transitionBuilder: transitionBuilder,
|
||||
errorBuilder: errorBuilder,
|
||||
noItemsFoundBuilder: noItemsFoundBuilder,
|
||||
loadingBuilder: loadingBuilder,
|
||||
debounceDuration: debounceDuration,
|
||||
suggestionsBoxDecoration: suggestionsBoxDecoration,
|
||||
suggestionsBoxController: suggestionsBoxController,
|
||||
textFieldConfiguration: textFieldConfiguration.copyWith(
|
||||
decoration: textFieldConfiguration.decoration
|
||||
.copyWith(errorText: state.errorText),
|
||||
onChanged: (text) {
|
||||
state.didChange(text);
|
||||
textFieldConfiguration.onChanged?.call(text);
|
||||
},
|
||||
controller: state._effectiveController,
|
||||
),
|
||||
suggestionsBoxVerticalOffset: suggestionsBoxVerticalOffset,
|
||||
onSuggestionSelected: onSuggestionSelected,
|
||||
onSuggestionsBoxToggle: onSuggestionsBoxToggle,
|
||||
itemBuilder: itemBuilder,
|
||||
suggestionsCallback: suggestionsCallback,
|
||||
animationStart: animationStart,
|
||||
animationDuration: animationDuration,
|
||||
direction: direction,
|
||||
hideOnLoading: hideOnLoading,
|
||||
hideOnEmpty: hideOnEmpty,
|
||||
hideOnError: hideOnError,
|
||||
hideSuggestionsOnKeyboardHide: hideSuggestionsOnKeyboardHide,
|
||||
keepSuggestionsOnLoading: keepSuggestionsOnLoading,
|
||||
keepSuggestionsOnSuggestionSelected:
|
||||
keepSuggestionsOnSuggestionSelected,
|
||||
autoFlipDirection: autoFlipDirection,
|
||||
autoFlipListDirection: autoFlipListDirection,
|
||||
hideKeyboard: hideKeyboard,
|
||||
minCharsForSuggestions: minCharsForSuggestions,
|
||||
hideKeyboardOnDrag: hideKeyboardOnDrag,
|
||||
);
|
||||
});
|
||||
|
||||
@override
|
||||
_TypeAheadFormFieldState<T> createState() => _TypeAheadFormFieldState<T>();
|
||||
}
|
||||
|
||||
class _TypeAheadFormFieldState<T> extends FormFieldState<String> {
|
||||
TextEditingController? _controller;
|
||||
|
||||
TextEditingController? get _effectiveController =>
|
||||
widget.textFieldConfiguration.controller ?? _controller;
|
||||
|
||||
@override
|
||||
TypeAheadFormField get widget => super.widget as TypeAheadFormField<dynamic>;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.textFieldConfiguration.controller == null) {
|
||||
_controller = TextEditingController(text: widget.initialValue);
|
||||
} else {
|
||||
widget.textFieldConfiguration.controller!
|
||||
.addListener(_handleControllerChanged);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(TypeAheadFormField oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.textFieldConfiguration.controller !=
|
||||
oldWidget.textFieldConfiguration.controller) {
|
||||
oldWidget.textFieldConfiguration.controller
|
||||
?.removeListener(_handleControllerChanged);
|
||||
widget.textFieldConfiguration.controller
|
||||
?.addListener(_handleControllerChanged);
|
||||
|
||||
if (oldWidget.textFieldConfiguration.controller != null &&
|
||||
widget.textFieldConfiguration.controller == null)
|
||||
_controller = TextEditingController.fromValue(
|
||||
oldWidget.textFieldConfiguration.controller!.value);
|
||||
if (widget.textFieldConfiguration.controller != null) {
|
||||
setValue(widget.textFieldConfiguration.controller!.text);
|
||||
if (oldWidget.textFieldConfiguration.controller == null)
|
||||
_controller = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.textFieldConfiguration.controller
|
||||
?.removeListener(_handleControllerChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void reset() {
|
||||
super.reset();
|
||||
setState(() {
|
||||
_effectiveController!.text = widget.initialValue!;
|
||||
if (widget.onReset != null) {
|
||||
widget.onReset!();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _handleControllerChanged() {
|
||||
// Suppress changes that originated from within this class.
|
||||
//
|
||||
// In the case where a controller has been passed in to this widget, we
|
||||
// register this change listener. In these cases, we'll also receive change
|
||||
// notifications for changes originating from within this class -- for
|
||||
// example, the reset() method. In such cases, the FormField value will
|
||||
// already have been set.
|
||||
if (_effectiveController!.text != value)
|
||||
didChange(_effectiveController!.text);
|
||||
}
|
||||
}
|