diff --git a/.gitignore b/.gitignore
index e87a1176..f4cd55eb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,4 +16,11 @@ ios/Runner/GeneratedPluginRegistrant.*
pubspec.lock
flutter_typeahead
-doc
\ No newline at end of file
+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`
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 57601266..b0966c8e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,6 @@
+## 4.3.4
+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
diff --git a/README.md b/README.md
index 7b849366..ca6d4a53 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ builder function
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,
+* Provides high customizable; you can customize the suggestion box decoration,
the loading bar, the animation, the debounce duration, etc.
## Installation
diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist
index 9367d483..9625e105 100644
--- a/example/ios/Flutter/AppFrameworkInfo.plist
+++ b/example/ios/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
CFBundleVersion
1.0
MinimumOSVersion
- 8.0
+ 11.0
diff --git a/example/ios/Flutter/flutter_export_environment.sh b/example/ios/Flutter/flutter_export_environment.sh
index 87d2abd0..1d2bce06 100755
--- a/example/ios/Flutter/flutter_export_environment.sh
+++ b/example/ios/Flutter/flutter_export_environment.sh
@@ -1,13 +1,14 @@
#!/bin/sh
# This is a generated file; do not edit or check into version control.
-export "FLUTTER_ROOT=C:\Flutter\flutter"
-export "FLUTTER_APPLICATION_PATH=C:\Users\FRANZ\Documents\flutter_tests\flutter_typeahead\example"
+export "FLUTTER_ROOT=/Users/ifernandes/development/flutter"
+export "FLUTTER_APPLICATION_PATH=/Users/ifernandes/Documents/GitHub/flutter_typeahead/example"
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
-export "FLUTTER_TARGET=lib\main.dart"
+export "FLUTTER_TARGET=/Users/ifernandes/Documents/GitHub/flutter_typeahead/example/lib/main.dart"
export "FLUTTER_BUILD_DIR=build"
export "FLUTTER_BUILD_NAME=1.0.0"
export "FLUTTER_BUILD_NUMBER=1"
+export "DART_DEFINES=Zmx1dHRlci5pbnNwZWN0b3Iuc3RydWN0dXJlZEVycm9ycz10cnVl,RkxVVFRFUl9XRUJfQVVUT19ERVRFQ1Q9dHJ1ZQ=="
export "DART_OBFUSCATION=false"
export "TRACK_WIDGET_CREATION=true"
export "TREE_SHAKE_ICONS=false"
-export "PACKAGE_CONFIG=.dart_tool/package_config.json"
+export "PACKAGE_CONFIG=/Users/ifernandes/Documents/GitHub/flutter_typeahead/example/.dart_tool/package_config.json"
diff --git a/example/ios/Podfile b/example/ios/Podfile
index 1e8c3c90..88359b22 100644
--- a/example/ios/Podfile
+++ b/example/ios/Podfile
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
-# platform :ios, '9.0'
+# platform :ios, '11.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
new file mode 100644
index 00000000..3091738c
--- /dev/null
+++ b/example/ios/Podfile.lock
@@ -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
diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj
index c6759a6e..a3f2352b 100644
--- a/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/example/ios/Runner.xcodeproj/project.pbxproj
@@ -3,10 +3,11 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 46;
+ 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 */; };
@@ -29,9 +30,12 @@
/* 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 = ""; };
+ 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 = ""; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 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 = ""; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
@@ -42,6 +46,7 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ 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 = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -49,12 +54,21 @@
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 = "";
+ };
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
@@ -72,6 +86,8 @@
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
+ DEA2152EEB1D8575D5DFDC3C /* Pods */,
+ 78354FCE6CD6437F846D905F /* Frameworks */,
);
sourceTree = "";
};
@@ -98,6 +114,17 @@
path = Runner;
sourceTree = "";
};
+ 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 = "";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -105,12 +132,14 @@
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 = (
);
@@ -127,7 +156,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
- LastUpgradeCheck = 1020;
+ LastUpgradeCheck = 1300;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
@@ -169,8 +198,31 @@
/* 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 = (
);
@@ -185,6 +237,7 @@
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@@ -197,6 +250,23 @@
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 */
@@ -272,7 +342,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -290,7 +360,10 @@
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
- LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ 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";
@@ -346,7 +419,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -395,7 +468,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -414,7 +487,10 @@
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
- LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ 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";
@@ -433,7 +509,10 @@
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
- LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ 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";
diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index a28140cf..3db53b6e 100644
--- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -1,6 +1,6 @@
+
+
diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist
index a060db61..4f68a2ce 100644
--- a/example/ios/Runner/Info.plist
+++ b/example/ios/Runner/Info.plist
@@ -41,5 +41,9 @@
UIViewControllerBasedStatusBarAppearance
+ CADisableMinimumFrameDurationOnPhone
+
+ UIApplicationSupportsIndirectInputEvents
+
diff --git a/example/lib/cupertino_app.dart b/example/lib/cupertino_app.dart
deleted file mode 100644
index cae38a2b..00000000
--- a/example/lib/cupertino_app.dart
+++ /dev/null
@@ -1,95 +0,0 @@
-import 'dart:async';
-
-import 'package:example/data.dart';
-import 'package:flutter/cupertino.dart';
-import 'package:flutter_typeahead/flutter_typeahead.dart';
-
-class MyCupertinoApp extends StatelessWidget {
- @override
- Widget build(BuildContext context) {
- return CupertinoApp(
- title: 'flutter_typeahead demo',
- home: CupertinoPageScaffold(
- child: FavoriteCitiesPage(),
- ), //MyHomePage(),
- );
- }
-}
-
-class FavoriteCitiesPage extends StatefulWidget {
- @override
- _FavoriteCitiesPage createState() => _FavoriteCitiesPage();
-}
-
-class _FavoriteCitiesPage extends State {
- final GlobalKey _formKey = GlobalKey();
- final TextEditingController _typeAheadController = TextEditingController();
- CupertinoSuggestionsBoxController _suggestionsBoxController =
- CupertinoSuggestionsBoxController();
- String favoriteCity = 'Unavailable';
-
- @override
- Widget build(BuildContext context) {
- return Form(
- key: _formKey,
- child: Padding(
- padding: EdgeInsets.all(32.0),
- child: Column(
- children: [
- SizedBox(
- height: 100.0,
- ),
- Text('What is your favorite city?'),
- CupertinoTypeAheadFormField(
- getImmediateSuggestions: true,
- suggestionsBoxController: _suggestionsBoxController,
- 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,
- ),
- ],
- ),
- ),
- );
- }
-}
diff --git a/example/lib/data.dart b/example/lib/data.dart
deleted file mode 100644
index e0218ddf..00000000
--- a/example/lib/data.dart
+++ /dev/null
@@ -1,41 +0,0 @@
-import 'dart:async';
-import 'dart:math';
-
-class BackendService {
- static Future>> getSuggestions(String query) async {
- await Future.delayed(Duration(seconds: 1));
-
- return List.generate(3, (index) {
- return {
- 'name': query + index.toString(),
- 'price': Random().nextInt(100).toString()
- };
- });
- }
-}
-
-class CitiesService {
- static final List cities = [
- 'Beirut',
- 'Damascus',
- 'San Fransisco',
- 'Rome',
- 'Los Angeles',
- 'Madrid',
- 'Bali',
- 'Barcelona',
- 'Paris',
- 'Bucharest',
- 'New York City',
- 'Philadelphia',
- 'Sydney',
- ];
-
- static List getSuggestions(String query) {
- List matches = [];
- matches.addAll(cities);
-
- matches.retainWhere((s) => s.toLowerCase().contains(query.toLowerCase()));
- return matches;
- }
-}
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 50a9a4f8..2f6fd6f4 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -1,9 +1,358 @@
import 'dart:io' show Platform;
+import 'dart:math';
+import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
-import 'package:example/material_app.dart';
-import 'package:example/cupertino_app.dart';
+import 'dart:async';
+
+import 'package:flutter/cupertino.dart';
+import 'package:flutter_typeahead/flutter_typeahead.dart';
+
void main() =>
runApp(!kIsWeb && Platform.isIOS ? MyCupertinoApp() : MyMaterialApp());
+
+class MyMaterialApp extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'flutter_typeahead demo',
+ scrollBehavior: MaterialScrollBehavior().copyWith(
+ dragDevices: {PointerDeviceKind.mouse, PointerDeviceKind.touch}
+ ),
+ home: DefaultTabController(
+ length: 3,
+ child: Scaffold(
+ appBar: AppBar(
+ 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(),
+ ]),
+ )),
+ ),
+ );
+ }
+}
+
+class NavigationExample extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: EdgeInsets.all(32.0),
+ child: Column(
+ children: [
+ 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 suggestion) {
+ return ListTile(
+ leading: Icon(Icons.shopping_cart),
+ title: Text(suggestion['name']!),
+ subtitle: Text('\$${suggestion['price']}'),
+ );
+ },
+ onSuggestionSelected: (Map suggestion) {
+ Navigator.of(context).push(MaterialPageRoute(
+ builder: (context) => ProductPage(product: suggestion)));
+ },
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class FormExample extends StatefulWidget {
+ @override
+ _FormExampleState createState() => _FormExampleState();
+}
+
+class _FormExampleState extends State {
+ final GlobalKey _formKey = GlobalKey();
+ final TextEditingController _typeAheadController = TextEditingController();
+
+ String? _selectedCity;
+
+ @override
+ Widget build(BuildContext context) {
+ return Form(
+ key: this._formKey,
+ child: Padding(
+ padding: EdgeInsets.all(32.0),
+ child: Column(
+ children: [
+ 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;
+ },
+ validator: (value) =>
+ value!.isEmpty ? 'Please select a city' : null,
+ onSaved: (value) => this._selectedCity = value,
+ ),
+ SizedBox(
+ height: 10.0,
+ ),
+ 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 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 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(
+ 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>> getSuggestions(String query) async {
+ await Future.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 cities = [
+ 'Beirut',
+ 'Damascus',
+ 'San Fransisco',
+ 'Rome',
+ 'Los Angeles',
+ 'Madrid',
+ 'Bali',
+ 'Barcelona',
+ 'Paris',
+ 'Bucharest',
+ 'New York City',
+ 'Philadelphia',
+ 'Sydney',
+ ];
+
+ static List getSuggestions(String query) {
+ List matches = [];
+ matches.addAll(cities);
+
+ matches.retainWhere((s) => s.toLowerCase().contains(query.toLowerCase()));
+ return matches;
+ }
+}
+
+
+/// Cupertino example
+class MyCupertinoApp extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return CupertinoApp(
+ title: 'flutter_typeahead demo',
+ home: CupertinoPageScaffold(
+ child: FavoriteCitiesPage(),
+ ), //MyHomePage(),
+ );
+ }
+}
+
+class FavoriteCitiesPage extends StatefulWidget {
+ @override
+ _FavoriteCitiesPage createState() => _FavoriteCitiesPage();
+}
+
+class _FavoriteCitiesPage extends State {
+ final GlobalKey _formKey = GlobalKey();
+ final TextEditingController _typeAheadController = TextEditingController();
+ CupertinoSuggestionsBoxController _suggestionsBoxController =
+ CupertinoSuggestionsBoxController();
+ String favoriteCity = 'Unavailable';
+
+ @override
+ Widget build(BuildContext context) {
+ return Form(
+ key: _formKey,
+ child: Padding(
+ padding: EdgeInsets.all(32.0),
+ child: Column(
+ children: [
+ SizedBox(
+ height: 100.0,
+ ),
+ Text('What is your favorite city?'),
+ CupertinoTypeAheadFormField(
+ getImmediateSuggestions: true,
+ suggestionsBoxController: _suggestionsBoxController,
+ 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,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/example/lib/material_app.dart b/example/lib/material_app.dart
deleted file mode 100644
index ba897700..00000000
--- a/example/lib/material_app.dart
+++ /dev/null
@@ -1,178 +0,0 @@
-import 'dart:ui';
-import 'package:example/scroll_example.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_typeahead/flutter_typeahead.dart';
-import 'package:example/data.dart';
-
-class MyMaterialApp extends StatelessWidget {
- @override
- Widget build(BuildContext context) {
- return MaterialApp(
- title: 'flutter_typeahead demo',
- scrollBehavior: MaterialScrollBehavior().copyWith(
- dragDevices: {PointerDeviceKind.mouse, PointerDeviceKind.touch}
- ),
- home: MyHomePage(),
- );
- }
-}
-
-class MyHomePage extends StatelessWidget {
- @override
- Widget build(BuildContext context) {
- return DefaultTabController(
- length: 3,
- child: Scaffold(
- appBar: AppBar(
- 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(),
- ]),
- )),
- );
- }
-}
-
-class NavigationExample extends StatelessWidget {
- @override
- Widget build(BuildContext context) {
- return Padding(
- padding: EdgeInsets.all(32.0),
- child: Column(
- children: [
- 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 suggestion) {
- return ListTile(
- leading: Icon(Icons.shopping_cart),
- title: Text(suggestion['name']!),
- subtitle: Text('\$${suggestion['price']}'),
- );
- },
- onSuggestionSelected: (Map suggestion) {
- Navigator.of(context).push(MaterialPageRoute(
- builder: (context) => ProductPage(product: suggestion)));
- },
- ),
- ],
- ),
- );
- }
-}
-
-class FormExample extends StatefulWidget {
- @override
- _FormExampleState createState() => _FormExampleState();
-}
-
-class _FormExampleState extends State {
- final GlobalKey _formKey = GlobalKey();
- final TextEditingController _typeAheadController = TextEditingController();
-
- String? _selectedCity;
-
- @override
- Widget build(BuildContext context) {
- return Form(
- key: this._formKey,
- child: Padding(
- padding: EdgeInsets.all(32.0),
- child: Column(
- children: [
- 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;
- },
- validator: (value) =>
- value!.isEmpty ? 'Please select a city' : null,
- onSaved: (value) => this._selectedCity = value,
- ),
- SizedBox(
- height: 10.0,
- ),
- 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 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.headline5,
- ),
- Text(
- this.product['price']! + ' USD',
- style: Theme.of(context).textTheme.subtitle1,
- )
- ],
- ),
- ),
- );
- }
-}
diff --git a/example/lib/scroll_example.dart b/example/lib/scroll_example.dart
deleted file mode 100644
index 0d26b54e..00000000
--- a/example/lib/scroll_example.dart
+++ /dev/null
@@ -1,42 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_typeahead/flutter_typeahead.dart';
-
-class ScrollExample extends StatelessWidget {
- final List 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(
- 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),
- ]);
- }
-}
diff --git a/example/pubspec.lock b/example/pubspec.lock
index 00073490..e71b68f5 100644
--- a/example/pubspec.lock
+++ b/example/pubspec.lock
@@ -121,7 +121,7 @@ packages:
path: ".."
relative: true
source: path
- version: "4.3.2"
+ version: "4.3.3"
flutter_web_plugins:
dependency: transitive
description: flutter
@@ -237,5 +237,5 @@ packages:
source: hosted
version: "2.1.4"
sdks:
- dart: ">=2.19.0 <4.0.0"
+ dart: ">=2.19.0 <3.0.0"
flutter: ">=1.20.0"
diff --git a/lib/flutter_typeahead.dart b/lib/flutter_typeahead.dart
index c95d086c..0eb0e8a6 100644
--- a/lib/flutter_typeahead.dart
+++ b/lib/flutter_typeahead.dart
@@ -1,5 +1,11 @@
library flutter_typeahead;
-export 'src/typedef.dart';
-export 'src/flutter_typeahead.dart';
-export 'src/cupertino_flutter_typeahead.dart';
+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/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_box_controller.dart';
diff --git a/lib/src/cupertino/field/cupertino_text_field_configuration.dart b/lib/src/cupertino/field/cupertino_text_field_configuration.dart
new file mode 100644
index 00000000..3cad3f16
--- /dev/null
+++ b/lib/src/cupertino/field/cupertino_text_field_configuration.dart
@@ -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? onChanged;
+ final VoidCallback? onEditingComplete;
+ final GestureTapCallback? onTap;
+ final ValueChanged? onSubmitted;
+ final List? 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? onChanged,
+ VoidCallback? onEditingComplete,
+ GestureTapCallback? onTap,
+ ValueChanged? onSubmitted,
+ List? 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,
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/src/cupertino/field/cupertino_typeahead_field.dart b/lib/src/cupertino/field/cupertino_typeahead_field.dart
new file mode 100644
index 00000000..7d57bfd6
--- /dev/null
+++ b/lib/src/cupertino/field/cupertino_typeahead_field.dart
@@ -0,0 +1,576 @@
+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 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 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 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 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 createState() =>
+ _CupertinoTypeAheadFieldState();
+}
+
+
+class _CupertinoTypeAheadFieldState extends State>
+ 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? _keyboardVisibility =
+ (supportedPlatform) ? KeyboardVisibilityController().onChange : null;
+ late StreamSubscription? _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(
+ 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,
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/src/cupertino/field/cupertino_typeahead_form_field.dart b/lib/src/cupertino/field/cupertino_typeahead_form_field.dart
new file mode 100644
index 00000000..d0ac9295
--- /dev/null
+++ b/lib/src/cupertino/field/cupertino_typeahead_form_field.dart
@@ -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 extends FormField {
+ /// 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? onSaved,
+ FormFieldValidator? validator,
+ ErrorBuilder? errorBuilder,
+ WidgetBuilder? noItemsFoundBuilder,
+ WidgetBuilder? loadingBuilder,
+ Duration debounceDuration = const Duration(milliseconds: 300),
+ CupertinoSuggestionsBoxDecoration suggestionsBoxDecoration =
+ const CupertinoSuggestionsBoxDecoration(),
+ CupertinoSuggestionsBoxController? suggestionsBoxController,
+ required SuggestionSelectionCallback onSuggestionSelected,
+ required ItemBuilder itemBuilder,
+ required SuggestionsCallback 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 field) {
+ final CupertinoTypeAheadFormFieldState state =
+ field as CupertinoTypeAheadFormFieldState;
+
+ 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 createState() =>
+ CupertinoTypeAheadFormFieldState();
+}
+
+class CupertinoTypeAheadFormFieldState extends FormFieldState {
+ TextEditingController? _controller;
+
+ TextEditingController? get _effectiveController =>
+ widget.textFieldConfiguration.controller ?? _controller;
+
+ @override
+ CupertinoTypeAheadFormField get widget =>
+ super.widget as CupertinoTypeAheadFormField;
+
+ @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);
+ }
+}
\ No newline at end of file
diff --git a/lib/src/cupertino/suggestions_box/cupertino_suggestions_box.dart b/lib/src/cupertino/suggestions_box/cupertino_suggestions_box.dart
new file mode 100644
index 00000000..0ca2ce7f
--- /dev/null
+++ b/lib/src/cupertino/suggestions_box/cupertino_suggestions_box.dart
@@ -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 _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.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 onChangeMetrics() async {
+ if (await _waitChangeMetrics()) {
+ resize();
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/src/cupertino/suggestions_box/cupertino_suggestions_box_controller.dart b/lib/src/cupertino/suggestions_box/cupertino_suggestions_box_controller.dart
new file mode 100644
index 00000000..07ce7d57
--- /dev/null
+++ b/lib/src/cupertino/suggestions_box/cupertino_suggestions_box_controller.dart
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/lib/src/cupertino/suggestions_box/cupertino_suggestions_box_decoration.dart b/lib/src/cupertino/suggestions_box/cupertino_suggestions_box_decoration.dart
new file mode 100644
index 00000000..9c0f223c
--- /dev/null
+++ b/lib/src/cupertino/suggestions_box/cupertino_suggestions_box_decoration.dart
@@ -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});
+}
\ No newline at end of file
diff --git a/lib/src/cupertino/suggestions_box/cupertino_suggestions_list.dart b/lib/src/cupertino/suggestions_box/cupertino_suggestions_list.dart
new file mode 100644
index 00000000..c3fc96a2
--- /dev/null
+++ b/lib/src/cupertino/suggestions_box/cupertino_suggestions_list.dart
@@ -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 extends StatefulWidget {
+ final CupertinoSuggestionsBox? suggestionsBox;
+ final TextEditingController? controller;
+ final bool getImmediateSuggestions;
+ final SuggestionSelectionCallback? onSuggestionSelected;
+ final SuggestionsCallback? suggestionsCallback;
+ final ItemBuilder? 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 createState() => _CupertinoSuggestionsListState();
+}
+
+class _CupertinoSuggestionsListState extends State>
+ with SingleTickerProviderStateMixin {
+ Iterable? _suggestions;
+ late bool _suggestionsValid;
+ late VoidCallback _controllerListener;
+ Timer? _debounceTimer;
+ bool? _isLoading, _isQueued;
+ Object? _error;
+ AnimationController? _animationController;
+ String? _lastTextValue;
+
+ @override
+ void didUpdateWidget(CupertinoSuggestionsList 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 invalidateSuggestions() async {
+ _suggestionsValid = false;
+ _getSuggestions();
+ }
+
+ Future _getSuggestions() async {
+ if (_suggestionsValid) return;
+ _suggestionsValid = true;
+
+ if (mounted) {
+ setState(() {
+ this._animationController!.forward(from: 1.0);
+
+ this._isLoading = true;
+ this._error = null;
+ });
+
+ Iterable? 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;
+ }
+}
\ No newline at end of file
diff --git a/lib/src/cupertino_flutter_typeahead.dart b/lib/src/cupertino_flutter_typeahead.dart
deleted file mode 100644
index d9114206..00000000
--- a/lib/src/cupertino_flutter_typeahead.dart
+++ /dev/null
@@ -1,1586 +0,0 @@
-/// # 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.
-import 'dart:async';
-import 'dart:core';
-import 'dart:math';
-
-import 'package:flutter/cupertino.dart';
-import 'package:flutter/services.dart';
-import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
-
-import 'typedef.dart';
-import 'utils.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)),
-);
-
-/// 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 extends FormField {
- /// 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? onSaved,
- FormFieldValidator? validator,
- ErrorBuilder? errorBuilder,
- WidgetBuilder? noItemsFoundBuilder,
- WidgetBuilder? loadingBuilder,
- Duration debounceDuration: const Duration(milliseconds: 300),
- CupertinoSuggestionsBoxDecoration suggestionsBoxDecoration:
- const CupertinoSuggestionsBoxDecoration(),
- CupertinoSuggestionsBoxController? suggestionsBoxController,
- required SuggestionSelectionCallback onSuggestionSelected,
- required ItemBuilder itemBuilder,
- required SuggestionsCallback 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 field) {
- final CupertinoTypeAheadFormFieldState state =
- field as CupertinoTypeAheadFormFieldState;
-
- 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 createState() =>
- CupertinoTypeAheadFormFieldState();
-}
-
-class CupertinoTypeAheadFormFieldState extends FormFieldState {
- TextEditingController? _controller;
-
- TextEditingController? get _effectiveController =>
- widget.textFieldConfiguration.controller ?? _controller;
-
- @override
- CupertinoTypeAheadFormField get widget =>
- super.widget as CupertinoTypeAheadFormField;
-
- @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);
- }
-}
-
-/// 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 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 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 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 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 createState() =>
- _CupertinoTypeAheadFieldState();
-}
-
-class _CupertinoTypeAheadFieldState extends State>
- 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? _keyboardVisibility =
- (supportedPlatform) ? KeyboardVisibilityController().onChange : null;
- late StreamSubscription? _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 = _SuggestionsList(
- 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,
- ),
- );
- }
-}
-
-class _SuggestionsList extends StatefulWidget {
- final _CupertinoSuggestionsBox? suggestionsBox;
- final TextEditingController? controller;
- final bool getImmediateSuggestions;
- final SuggestionSelectionCallback? onSuggestionSelected;
- final SuggestionsCallback? suggestionsCallback;
- final ItemBuilder? 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;
-
- _SuggestionsList({
- 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
- _SuggestionsListState createState() => _SuggestionsListState();
-}
-
-class _SuggestionsListState extends State<_SuggestionsList>
- with SingleTickerProviderStateMixin {
- Iterable? _suggestions;
- late bool _suggestionsValid;
- late VoidCallback _controllerListener;
- Timer? _debounceTimer;
- bool? _isLoading, _isQueued;
- Object? _error;
- AnimationController? _animationController;
- String? _lastTextValue;
-
- @override
- void didUpdateWidget(_SuggestionsList 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 invalidateSuggestions() async {
- _suggestionsValid = false;
- _getSuggestions();
- }
-
- Future _getSuggestions() async {
- if (_suggestionsValid) return;
- _suggestionsValid = true;
-
- if (mounted) {
- setState(() {
- this._animationController!.forward(from: 1.0);
-
- this._isLoading = true;
- this._error = null;
- });
-
- Iterable? 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;
- }
-}
-
-/// 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});
-}
-
-/// 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? onChanged;
- final VoidCallback? onEditingComplete;
- final GestureTapCallback? onTap;
- final ValueChanged? onSubmitted;
- final List? 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? onChanged,
- VoidCallback? onEditingComplete,
- GestureTapCallback? onTap,
- ValueChanged? onSubmitted,
- List? 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,
- );
- }
-}
-
-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 _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.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 onChangeMetrics() async {
- if (await _waitChangeMetrics()) {
- resize();
- }
- }
-}
-
-/// 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();
- }
-}
diff --git a/lib/src/flutter_typeahead.dart b/lib/src/flutter_typeahead.dart
deleted file mode 100644
index 7485b50e..00000000
--- a/lib/src/flutter_typeahead.dart
+++ /dev/null
@@ -1,2064 +0,0 @@
-/// # 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 _formKey = GlobalKey();
-/// final TextEditingController _typeAheadController = TextEditingController();
-/// String _selectedCity;
-/// ...
-/// Form(
-/// key: this._formKey,
-/// child: Padding(
-/// padding: EdgeInsets.all(32.0),
-/// child: Column(
-/// children: [
-/// 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
-/// )
-/// ```
-import 'dart:async';
-import 'dart:math';
-
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
-import 'package:flutter_typeahead/src/keyboard_suggestion_selection_notifier.dart';
-import 'package:flutter_typeahead/src/should_refresh_suggestion_focus_index_notifier.dart';
-
-import 'typedef.dart';
-import 'utils.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 extends FormField {
- /// 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
- 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? onSaved,
- this.onReset,
- FormFieldValidator? 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 onSuggestionSelected,
- required ItemBuilder itemBuilder,
- required SuggestionsCallback 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 field) {
- final _TypeAheadFormFieldState state =
- field as _TypeAheadFormFieldState;
-
- 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 createState() => _TypeAheadFormFieldState();
-}
-
-class _TypeAheadFormFieldState extends FormFieldState {
- TextEditingController? _controller;
-
- TextEditingController? get _effectiveController =>
- widget.textFieldConfiguration.controller ?? _controller;
-
- @override
- TypeAheadFormField get widget => super.widget as TypeAheadFormField;
-
- @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);
- }
-}
-
-/// A [TextField](https://docs.flutter.io/flutter/material/TextField-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 TypeAheadField 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 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 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 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({
- Key? key,
- 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,
- }) : 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
- _TypeAheadFieldState createState() => _TypeAheadFieldState();
-}
-
-class _TypeAheadFieldState extends State>
- 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? _keyboardVisibility =
- (supportedPlatform) ? KeyboardVisibilityController().onChange : null;
- late StreamSubscription? _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(
- 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(
- key: TestKeys.textFieldKey,
- 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,
- ),
- );
- }
-}
-
-class _SuggestionsList extends StatefulWidget {
- final _SuggestionsBox? suggestionsBox;
- final TextEditingController? controller;
- final bool getImmediateSuggestions;
- final SuggestionSelectionCallback? onSuggestionSelected;
- final SuggestionsCallback? suggestionsCallback;
- final ItemBuilder? itemBuilder;
- final ScrollController? scrollController;
- final SuggestionsBoxDecoration? 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 KeyboardSuggestionSelectionNotifier keyboardSuggestionSelectionNotifier;
- final ShouldRefreshSuggestionFocusIndexNotifier
- shouldRefreshSuggestionFocusIndexNotifier;
- final VoidCallback giveTextFieldFocus;
- final VoidCallback onSuggestionFocus;
- final KeyEventResult Function(FocusNode _, RawKeyEvent event) onKeyEvent;
- final bool hideKeyboardOnDrag;
-
- _SuggestionsList({
- required this.suggestionsBox,
- this.controller,
- this.getImmediateSuggestions: false,
- this.onSuggestionSelected,
- this.suggestionsCallback,
- this.itemBuilder,
- this.scrollController,
- 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,
- required this.keyboardSuggestionSelectionNotifier,
- required this.shouldRefreshSuggestionFocusIndexNotifier,
- required this.giveTextFieldFocus,
- required this.onSuggestionFocus,
- required this.onKeyEvent,
- required this.hideKeyboardOnDrag,
- });
-
- @override
- _SuggestionsListState createState() => _SuggestionsListState();
-}
-
-class _SuggestionsListState extends State<_SuggestionsList>
- with SingleTickerProviderStateMixin {
- Iterable? _suggestions;
- late bool _suggestionsValid;
- late VoidCallback _controllerListener;
- Timer? _debounceTimer;
- bool? _isLoading, _isQueued;
- Object? _error;
- AnimationController? _animationController;
- String? _lastTextValue;
- late final ScrollController _scrollController =
- widget.scrollController ?? ScrollController();
- List _focusNodes = [];
- int _suggestionIndex = -1;
-
- _SuggestionsListState() {
- 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();
- }
- });
- }
- };
- }
-
- @override
- void didUpdateWidget(_SuggestionsList oldWidget) {
- super.didUpdateWidget(oldWidget);
- widget.controller!.addListener(this._controllerListener);
- _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();
- }
-
- widget.controller!.addListener(this._controllerListener);
-
- widget.keyboardSuggestionSelectionNotifier.addListener(() {
- final suggestionsLength = _suggestions?.length;
- final event = widget.keyboardSuggestionSelectionNotifier.value;
- if (event == null || suggestionsLength == null) return;
-
- if (event == LogicalKeyboardKey.arrowDown &&
- _suggestionIndex < suggestionsLength - 1) {
- _suggestionIndex++;
- } else if (event == LogicalKeyboardKey.arrowUp && _suggestionIndex > -1) {
- _suggestionIndex--;
- }
-
- if (_suggestionIndex > -1 && _suggestionIndex < _focusNodes.length) {
- final focusNode = _focusNodes[_suggestionIndex];
- focusNode.requestFocus();
- widget.onSuggestionFocus();
- } else {
- widget.giveTextFieldFocus();
- }
- });
-
- widget.shouldRefreshSuggestionFocusIndexNotifier.addListener(() {
- if (_suggestionIndex != -1) {
- _suggestionIndex = -1;
- }
- });
- }
-
- Future invalidateSuggestions() async {
- _suggestionsValid = false;
- await _getSuggestions();
- }
-
- Future _getSuggestions() async {
- if (_suggestionsValid) return;
- _suggestionsValid = true;
-
- if (mounted) {
- setState(() {
- this._animationController!.forward(from: 1.0);
-
- this._isLoading = true;
- this._error = null;
- });
-
- Iterable? 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;
- _focusNodes = List.generate(
- _suggestions?.length ?? 0,
- (index) => FocusNode(onKey: (_, event) {
- return widget.onKeyEvent(_, event);
- }),
- );
- });
- }
- }
- }
-
- @override
- void dispose() {
- _animationController!.dispose();
- _debounceTimer?.cancel();
- for (final focusNode in _focusNodes) {
- focusNode.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 &&
- this._error == null) 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();
- }
-
- final 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,
- );
- }
-
- var container = Material(
- elevation: widget.decoration!.elevation,
- color: widget.decoration!.color,
- shape: widget.decoration!.shape,
- borderRadius: widget.decoration!.borderRadius,
- shadowColor: widget.decoration!.shadowColor,
- clipBehavior: widget.decoration!.clipBehavior,
- child: ConstrainedBox(
- constraints: constraints,
- child: animationChild,
- ),
- );
-
- return container;
- }
-
- 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)
- : Align(
- alignment: Alignment.center,
- child: Padding(
- padding: const EdgeInsets.symmetric(vertical: 8.0),
- child: CircularProgressIndicator(),
- ),
- );
- }
-
- return child;
- }
-
- Widget createErrorWidget() {
- return widget.errorBuilder != null
- ? widget.errorBuilder!(context, this._error)
- : Padding(
- padding: const EdgeInsets.all(8.0),
- child: Text(
- 'Error: ${this._error}',
- style: TextStyle(color: Theme.of(context).errorColor),
- ),
- );
- }
-
- Widget createNoItemsFoundWidget() {
- return widget.noItemsFoundBuilder != null
- ? widget.noItemsFoundBuilder!(context)
- : Padding(
- padding: const EdgeInsets.symmetric(vertical: 8.0),
- child: Text(
- 'No Items Found!',
- textAlign: TextAlign.center,
- style: TextStyle(
- color: Theme.of(context).disabledColor, fontSize: 18.0),
- ),
- );
- }
-
- Widget createSuggestionsWidget() {
- Widget child = ListView(
- padding: EdgeInsets.zero,
- primary: false,
- shrinkWrap: true,
- keyboardDismissBehavior: widget.hideKeyboardOnDrag
- ? ScrollViewKeyboardDismissBehavior.onDrag
- : ScrollViewKeyboardDismissBehavior.manual,
- controller: _scrollController,
- reverse: widget.suggestionsBox!.direction == AxisDirection.down
- ? false
- : widget.suggestionsBox!.autoFlipListDirection,
- children: List.generate(this._suggestions!.length, (index) {
- final suggestion = _suggestions!.elementAt(index);
- final focusNode = _focusNodes[index];
-
- return TextFieldTapRegion(
- child: InkWell(
- key: TestKeys.getSuggestionKey(index),
- focusColor: Theme.of(context).hoverColor,
- focusNode: focusNode,
- child: widget.itemBuilder!(context, suggestion),
- onTap: () {
- // * we give the focus back to the text field
- widget.giveTextFieldFocus();
-
- widget.onSuggestionSelected!(suggestion);
- },
- ),
- );
- }),
- );
-
- if (widget.decoration!.hasScrollbar) {
- child = Scrollbar(
- controller: _scrollController,
- child: child,
- );
- }
-
- return child;
- }
-}
-
-/// Supply an instance of this class to the [TypeAhead.suggestionsBoxDecoration]
-/// property to configure the suggestions box decoration
-class SuggestionsBoxDecoration {
- /// The z-coordinate at which to place the suggestions box. This controls the size
- /// of the shadow below the box.
- ///
- /// Same as [Material.elevation](https://docs.flutter.io/flutter/material/Material/elevation.html)
- final double elevation;
-
- /// The color to paint the suggestions box.
- ///
- /// Same as [Material.color](https://docs.flutter.io/flutter/material/Material/color.html)
- final Color? color;
-
- /// Defines the material's shape as well its shadow.
- ///
- /// Same as [Material.shape](https://docs.flutter.io/flutter/material/Material/shape.html)
- final ShapeBorder? shape;
-
- /// Defines if a scrollbar will be displayed or not.
- final bool hasScrollbar;
-
- /// If non-null, the corners of this box are rounded by this [BorderRadius](https://docs.flutter.io/flutter/painting/BorderRadius-class.html).
- ///
- /// Same as [Material.borderRadius](https://docs.flutter.io/flutter/material/Material/borderRadius.html)
- final BorderRadius? borderRadius;
-
- /// The color to paint the shadow below the material.
- ///
- /// Same as [Material.shadowColor](https://docs.flutter.io/flutter/material/Material/shadowColor.html)
- final Color shadowColor;
-
- /// The constraints to be applied to the suggestions box
- final BoxConstraints? constraints;
-
- /// Adds an offset to the suggestions box
- final double offsetX;
-
- /// The content will be clipped (or not) according to this option.
- ///
- /// Same as [Material.clipBehavior](https://api.flutter.dev/flutter/material/Material/clipBehavior.html)
- final Clip clipBehavior;
-
- /// Creates a SuggestionsBoxDecoration
- const SuggestionsBoxDecoration(
- {this.elevation: 4.0,
- this.color,
- this.shape,
- this.hasScrollbar: true,
- this.borderRadius,
- this.shadowColor: const Color(0xFF000000),
- this.constraints,
- this.clipBehavior: Clip.none,
- this.offsetX: 0.0});
-}
-
-/// 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? 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? 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? 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? onChanged,
- ValueChanged? onSubmitted,
- bool? obscureText,
- MaxLengthEnforcement? maxLengthEnforcement,
- int? maxLength,
- int? maxLines,
- int? minLines,
- bool? autocorrect,
- List? 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,
- );
- }
-}
-
-class _SuggestionsBox {
- 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;
-
- _SuggestionsBox(
- 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 _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.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() {
- TypeAheadField widget = context.widget as TypeAheadField;
-
- RenderBox? box = context.findRenderObject() as RenderBox?;
- if (box == null || box.hasSize == false) {
- return;
- }
-
- 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,
- TypeAheadField 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,
- TypeAheadField 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,
- TypeAheadField 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 onChangeMetrics() async {
- if (await _waitChangeMetrics()) {
- resize();
- }
- }
-}
-
-/// Supply an instance of this class to the [TypeAhead.suggestionsBoxController]
-/// property to manually control the suggestions box
-class SuggestionsBoxController {
- _SuggestionsBox? _suggestionsBox;
- FocusNode? _effectiveFocusNode;
-
- /// Opens the suggestions box
- void open() {
- _effectiveFocusNode?.requestFocus();
- }
-
- bool isOpened() {
- return _suggestionsBox?.isOpened ?? false;
- }
-
- /// Closes the suggestions box
- void close() {
- _effectiveFocusNode?.unfocus();
- }
-
- /// Opens the suggestions box if closed and vice-versa
- void toggle() {
- if (_suggestionsBox?.isOpened ?? false) {
- close();
- } else {
- open();
- }
- }
-
- /// Recalculates the height of the suggestions box
- void resize() {
- _suggestionsBox!.resize();
- }
-}
-
-@visibleForTesting
-class TestKeys {
- TestKeys._();
-
- static const textFieldKey = ValueKey("text-field");
- static ValueKey getSuggestionKey(int index) =>
- ValueKey("suggestion-$index");
-}
diff --git a/lib/src/material/field/test_keys.dart b/lib/src/material/field/test_keys.dart
new file mode 100644
index 00000000..922dbffa
--- /dev/null
+++ b/lib/src/material/field/test_keys.dart
@@ -0,0 +1,10 @@
+import 'package:flutter/material.dart';
+
+@visibleForTesting
+class TestKeys {
+ TestKeys._();
+
+ static const textFieldKey = ValueKey("text-field");
+ static ValueKey getSuggestionKey(int index) =>
+ ValueKey("suggestion-$index");
+}
\ No newline at end of file
diff --git a/lib/src/material/field/text_field_configuration.dart b/lib/src/material/field/text_field_configuration.dart
new file mode 100644
index 00000000..0bbde852
--- /dev/null
+++ b/lib/src/material/field/text_field_configuration.dart
@@ -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? 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? 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? 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? onChanged,
+ ValueChanged? onSubmitted,
+ bool? obscureText,
+ MaxLengthEnforcement? maxLengthEnforcement,
+ int? maxLength,
+ int? maxLines,
+ int? minLines,
+ bool? autocorrect,
+ List? 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,
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/src/material/field/typeahead_field.dart b/lib/src/material/field/typeahead_field.dart
new file mode 100644
index 00000000..c3256c57
--- /dev/null
+++ b/lib/src/material/field/typeahead_field.dart
@@ -0,0 +1,888 @@
+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/test_keys.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 _formKey = GlobalKey();
+/// final TextEditingController _typeAheadController = TextEditingController();
+/// String _selectedCity;
+/// ...
+/// Form(
+/// key: this._formKey,
+/// child: Padding(
+/// padding: EdgeInsets.all(32.0),
+/// child: Column(
+/// children: [
+/// 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 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 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 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 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({
+ Key? key,
+ 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,
+ }) : 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
+ _TypeAheadFieldState createState() => _TypeAheadFieldState();
+}
+
+class _TypeAheadFieldState extends State>
+ 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? _keyboardVisibility =
+ (supportedPlatform) ? KeyboardVisibilityController().onChange : null;
+ late StreamSubscription? _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(
+ 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(
+ key: TestKeys.textFieldKey,
+ 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,
+ ),
+ );
+ }
+}
diff --git a/lib/src/material/field/typeahead_form_field.dart b/lib/src/material/field/typeahead_form_field.dart
new file mode 100644
index 00000000..311bbe44
--- /dev/null
+++ b/lib/src/material/field/typeahead_form_field.dart
@@ -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 extends FormField {
+ /// 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
+ 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? onSaved,
+ this.onReset,
+ FormFieldValidator? 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 onSuggestionSelected,
+ required ItemBuilder itemBuilder,
+ required SuggestionsCallback 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 field) {
+ final _TypeAheadFormFieldState state =
+ field as _TypeAheadFormFieldState;
+
+ 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 createState() => _TypeAheadFormFieldState();
+}
+
+class _TypeAheadFormFieldState extends FormFieldState {
+ TextEditingController? _controller;
+
+ TextEditingController? get _effectiveController =>
+ widget.textFieldConfiguration.controller ?? _controller;
+
+ @override
+ TypeAheadFormField get widget => super.widget as TypeAheadFormField;
+
+ @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);
+ }
+}
\ No newline at end of file
diff --git a/lib/src/material/suggestions_box/suggestions_box.dart b/lib/src/material/suggestions_box/suggestions_box.dart
new file mode 100644
index 00000000..176abecf
--- /dev/null
+++ b/lib/src/material/suggestions_box/suggestions_box.dart
@@ -0,0 +1,223 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_typeahead/src/material/field/typeahead_field.dart';
+
+class SuggestionsBox {
+ 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;
+
+ SuggestionsBox(
+ 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 _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.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() {
+ TypeAheadField widget = context.widget as TypeAheadField;
+
+ RenderBox? box = context.findRenderObject() as RenderBox?;
+ if (box == null || box.hasSize == false) {
+ return;
+ }
+
+ 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;
+ // Sometimes textBoxAbsY is NaN, so we need to check for that
+ if(!maxHDesired.isNaN) {
+ 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;
+
+ // Not sure if this is needed, but it's here just in case
+ if(!maxHFlipped.isNaN) {
+ maxHeight = maxHFlipped;
+ }
+ }
+ }
+
+ if (maxHeight < 0) maxHeight = 0;
+ }
+
+ double _calculateMaxHeight(
+ AxisDirection direction,
+ RenderBox box,
+ TypeAheadField 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,
+ TypeAheadField 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,
+ TypeAheadField 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 onChangeMetrics() async {
+ if (await _waitChangeMetrics()) {
+ resize();
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/src/material/suggestions_box/suggestions_box_controller.dart b/lib/src/material/suggestions_box/suggestions_box_controller.dart
new file mode 100644
index 00000000..853328d6
--- /dev/null
+++ b/lib/src/material/suggestions_box/suggestions_box_controller.dart
@@ -0,0 +1,37 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_typeahead/src/material/suggestions_box/suggestions_box.dart';
+
+/// Supply an instance of this class to the [TypeAhead.suggestionsBoxController]
+/// property to manually control the suggestions box
+class SuggestionsBoxController {
+ SuggestionsBox? suggestionsBox;
+ FocusNode? effectiveFocusNode;
+
+ /// Opens the suggestions box
+ void open() {
+ effectiveFocusNode?.requestFocus();
+ }
+
+ bool isOpened() {
+ return suggestionsBox?.isOpened ?? false;
+ }
+
+ /// Closes the suggestions box
+ void close() {
+ effectiveFocusNode?.unfocus();
+ }
+
+ /// Opens the suggestions box if closed and vice-versa
+ void toggle() {
+ if (suggestionsBox?.isOpened ?? false) {
+ close();
+ } else {
+ open();
+ }
+ }
+
+ /// Recalculates the height of the suggestions box
+ void resize() {
+ suggestionsBox!.resize();
+ }
+}
\ No newline at end of file
diff --git a/lib/src/material/suggestions_box/suggestions_box_decoration.dart b/lib/src/material/suggestions_box/suggestions_box_decoration.dart
new file mode 100644
index 00000000..4f618ff9
--- /dev/null
+++ b/lib/src/material/suggestions_box/suggestions_box_decoration.dart
@@ -0,0 +1,57 @@
+import 'package:flutter/material.dart';
+
+/// Supply an instance of this class to the [TypeAhead.suggestionsBoxDecoration]
+/// property to configure the suggestions box decoration
+class SuggestionsBoxDecoration {
+ /// The z-coordinate at which to place the suggestions box. This controls the size
+ /// of the shadow below the box.
+ ///
+ /// Same as [Material.elevation](https://docs.flutter.io/flutter/material/Material/elevation.html)
+ final double elevation;
+
+ /// The color to paint the suggestions box.
+ ///
+ /// Same as [Material.color](https://docs.flutter.io/flutter/material/Material/color.html)
+ final Color? color;
+
+ /// Defines the material's shape as well its shadow.
+ ///
+ /// Same as [Material.shape](https://docs.flutter.io/flutter/material/Material/shape.html)
+ final ShapeBorder? shape;
+
+ /// Defines if a scrollbar will be displayed or not.
+ final bool hasScrollbar;
+
+ /// If non-null, the corners of this box are rounded by this [BorderRadius](https://docs.flutter.io/flutter/painting/BorderRadius-class.html).
+ ///
+ /// Same as [Material.borderRadius](https://docs.flutter.io/flutter/material/Material/borderRadius.html)
+ final BorderRadius? borderRadius;
+
+ /// The color to paint the shadow below the material.
+ ///
+ /// Same as [Material.shadowColor](https://docs.flutter.io/flutter/material/Material/shadowColor.html)
+ final Color shadowColor;
+
+ /// The constraints to be applied to the suggestions box
+ final BoxConstraints? constraints;
+
+ /// Adds an offset to the suggestions box
+ final double offsetX;
+
+ /// The content will be clipped (or not) according to this option.
+ ///
+ /// Same as [Material.clipBehavior](https://api.flutter.dev/flutter/material/Material/clipBehavior.html)
+ final Clip clipBehavior;
+
+ /// Creates a SuggestionsBoxDecoration
+ const SuggestionsBoxDecoration(
+ {this.elevation = 4.0,
+ this.color,
+ this.shape,
+ this.hasScrollbar = true,
+ this.borderRadius,
+ this.shadowColor = const Color(0xFF000000),
+ this.constraints,
+ this.clipBehavior = Clip.none,
+ this.offsetX = 0.0});
+}
\ No newline at end of file
diff --git a/lib/src/material/suggestions_box/suggestions_list.dart b/lib/src/material/suggestions_box/suggestions_list.dart
new file mode 100644
index 00000000..01fb76fa
--- /dev/null
+++ b/lib/src/material/suggestions_box/suggestions_list.dart
@@ -0,0 +1,413 @@
+import 'dart:async';
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_typeahead/src/material/field/test_keys.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_decoration.dart';
+import 'package:flutter_typeahead/src/typedef.dart';
+
+class SuggestionsList extends StatefulWidget {
+ final SuggestionsBox? suggestionsBox;
+ final TextEditingController? controller;
+ final bool getImmediateSuggestions;
+ final SuggestionSelectionCallback? onSuggestionSelected;
+ final SuggestionsCallback? suggestionsCallback;
+ final ItemBuilder? itemBuilder;
+ final ScrollController? scrollController;
+ final SuggestionsBoxDecoration? 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 KeyboardSuggestionSelectionNotifier keyboardSuggestionSelectionNotifier;
+ final ShouldRefreshSuggestionFocusIndexNotifier
+ shouldRefreshSuggestionFocusIndexNotifier;
+ final VoidCallback giveTextFieldFocus;
+ final VoidCallback onSuggestionFocus;
+ final KeyEventResult Function(FocusNode _, RawKeyEvent event) onKeyEvent;
+ final bool hideKeyboardOnDrag;
+
+ SuggestionsList({
+ required this.suggestionsBox,
+ this.controller,
+ this.getImmediateSuggestions = false,
+ this.onSuggestionSelected,
+ this.suggestionsCallback,
+ this.itemBuilder,
+ this.scrollController,
+ 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,
+ required this.keyboardSuggestionSelectionNotifier,
+ required this.shouldRefreshSuggestionFocusIndexNotifier,
+ required this.giveTextFieldFocus,
+ required this.onSuggestionFocus,
+ required this.onKeyEvent,
+ required this.hideKeyboardOnDrag,
+ });
+
+ @override
+ _SuggestionsListState createState() => _SuggestionsListState();
+}
+
+class _SuggestionsListState extends State>
+ with SingleTickerProviderStateMixin {
+ Iterable