├── .github └── workflows │ ├── master.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── .gitignore ├── .metadata ├── README.md ├── android │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── example │ │ │ │ └── MainActivity.java │ │ │ └── res │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ └── values │ │ │ └── styles.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── ios │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ └── contents.xcworkspacedata │ └── Runner │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── main.m ├── lib │ ├── content.dart │ ├── custom_input.dart │ ├── main.dart │ ├── sample.dart │ ├── sample2.dart │ ├── sample3.dart │ ├── sample4.dart │ └── sample5.dart ├── pubspec.yaml └── test │ └── widget_test.dart ├── ios └── Flutter │ └── flutter_export_environment.sh ├── lib ├── external │ ├── keyboard_avoider │ │ ├── bottom_area_avoider.dart │ │ └── keyboard_avoider.dart │ └── platform_check │ │ ├── platform_check.dart │ │ ├── platform_io.dart │ │ └── platform_web.dart ├── keyboard_actions.dart ├── keyboard_actions_config.dart ├── keyboard_actions_item.dart └── keyboard_custom.dart └── pubspec.yaml /.github/workflows/master.yml: -------------------------------------------------------------------------------- 1 | name: Package Publish dry-run 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | 9 | dry-run: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: 'Checkout' 13 | uses: actions/checkout@main 14 | - name: 'Dry-run' 15 | uses: k-paxian/dart-package-publisher@master 16 | with: 17 | accessToken: ${{ secrets.OAUTH_ACCESS_TOKEN }} 18 | refreshToken: ${{ secrets.OAUTH_REFRESH_TOKEN }} 19 | dryRunOnly: true 20 | flutter: true -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Package Publish 2 | 3 | on: 4 | push: 5 | branches: [ release ] 6 | 7 | jobs: 8 | check_version: 9 | name: "Check Version Tag" 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - 13 | uses: actions/checkout@v2 14 | - 15 | continue-on-error: true 16 | env: 17 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 18 | id: previoustag 19 | name: "Get Latest Version" 20 | uses: WyriHaximus/github-action-get-previous-tag@master 21 | - 22 | name: "Print Latest Version" 23 | run: "echo ${{ steps.previoustag.outputs.tag }}" 24 | - 25 | id: config 26 | name: "Get New Version" 27 | uses: CumulusDS/get-yaml-paths-action@v0.1.0 28 | with: 29 | file: pubspec.yaml 30 | version_name: version 31 | - 32 | name: "Print New Version" 33 | run: "echo ${{ steps.config.outputs.version_name }}" 34 | - 35 | if: "steps.config.outputs.version_name == steps.previoustag.outputs.tag" 36 | name: "Compare Version" 37 | run: | 38 | echo 'The version from your pubspec.yaml is the same as Release, Please update the version' 39 | exit 1 40 | - 41 | run: "echo ${{ steps.config.outputs.version_name }} > version.txt\n" 42 | shell: bash 43 | - 44 | name: "Upload New Version" 45 | uses: actions/upload-artifact@v1 46 | with: 47 | name: home 48 | path: version.txt 49 | publish: 50 | needs: 51 | - check_version 52 | runs-on: ubuntu-20.04 53 | steps: 54 | - 55 | name: Checkout 56 | uses: actions/checkout@v1 57 | - 58 | name: "Publish Package" 59 | uses: ilteoood/actions-flutter-pub-publisher@master 60 | with: 61 | credential: "${{secrets.CREDENTIAL_JSON}}" 62 | dry_run: false 63 | flutter_package: true 64 | skip_test: true 65 | tag: 66 | name: "Tag Version" 67 | needs: 68 | - publish 69 | runs-on: ubuntu-20.04 70 | steps: 71 | - 72 | name: "Download New Version" 73 | uses: actions/download-artifact@v1 74 | with: 75 | name: home 76 | - 77 | env: 78 | ACTIONS_ALLOW_UNSECURE_COMMANDS: "true" 79 | name: "Set and Tag the new version" 80 | run: "echo \"::set-env name=RELEASE_VERSION::$(cat home/version.txt)\"" 81 | shell: bash 82 | - 83 | uses: tvdias/github-tagger@v0.0.2 84 | with: 85 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 86 | tag: "${{env.RELEASE_VERSION}}" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Taken from https://github.com/flutter/plugins/blob/master/.gitignore 2 | 3 | # OS and editor files 4 | .DS_Store 5 | .atom/ 6 | .idea/ 7 | .vscode/ 8 | 9 | # Generated dart files 10 | .packages 11 | .pub/ 12 | .dart_tool/ 13 | pubspec.lock 14 | 15 | # Generated iOS files 16 | Podfile 17 | Podfile.lock 18 | Pods/ 19 | .symlinks/ 20 | **/Flutter/App.framework/ 21 | **/Flutter/Flutter.framework/ 22 | **/Flutter/Generated.xcconfig 23 | **/Flutter/flutter_assets/ 24 | ServiceDefinitions.json 25 | xcuserdata/ 26 | 27 | # Android tools and settings 28 | local.properties 29 | keystore.properties 30 | .gradle/ 31 | gradlew 32 | gradlew.bat 33 | gradle-wrapper.jar 34 | *.iml 35 | 36 | # Flutter generated files 37 | GeneratedPluginRegistrant.h 38 | GeneratedPluginRegistrant.m 39 | GeneratedPluginRegistrant.java 40 | build/ 41 | example/ios/Flutter/Flutter.podspec 42 | example/ios/Flutter/.last_build_id 43 | .fvm/flutter_sdk 44 | .fvm/fvm_config.json 45 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [4.2.0] 2 | * removed unfocusing for android on keyboard change (bug). Thanks `raphire08`. 3 | * minor lints for flutter 3.7.3. 4 | 5 | ## [4.1.0] 6 | * Dismiss the bar when the android back button is pressed (bug). Thanks `monster555`. 7 | ## [4.0.1] 8 | * Adds `keyboardBarElevation` to `KeyboardActionsConfig` to change the current mandatory 20 elevation. Thanks `Rooa94`. 9 | 10 | ## [4.0.0] 11 | * Update to Flutter 3.0 (Use only if you are using Flutter 3 >=). Thanks `ashim-kr-saha` and `Roaa94`. 12 | 13 | ## [3.4.6 - 3.4.7] 14 | * Bug #173 fixed. Thanks `Arenukvern`. 15 | * Bug #172 fixed. Thanks `BenjaminFarquhar`. 16 | 17 | ## [3.4.5] 18 | * `keepFocusOnTappingNode` was added. Thanks `alex-min`. 19 | * `toolbarAlignment` was added in `KeyboardActionsItem`. Thanks `f-person` (Arshak Aghakaryan). 20 | 21 | ## [3.4.4] 22 | * `defaultDoneWidget` was added in `KeyboardActionsConfig`. Thanks `peter-gy`. 23 | 24 | ## [3.4.2 - 3.4.3] 25 | * Fixed a bug that was displaying the bar at the bottom (hide behind the keyboard). 26 | 27 | ## [3.4.1] 28 | * `tapOutsideBehavior` was added in order to dismiss and allow the hitTest for background components. Thanks `crizant`. 29 | 30 | ## [3.4.0] 31 | * Null safety migration. Thanks TheManuz 32 | 33 | ## [3.3.1 - 3.3.1+1] 34 | 35 | * Fixed issue #115 when running Flutter 1.22. 36 | * Fixed issue #106 37 | * Fixed issue #121 38 | 39 | ## [3.3.0+1] 40 | 41 | * Some bugs fixed: #99, #100, #101. 42 | * `disableScroll` was added in `KeyboardActions`. 43 | 44 | 45 | ## [3.3.0] 46 | 47 | * `KeyboardAction` was renamed to `KeyboardActionsItem` to avoid mess with the main widget `KeyboardActions`. 48 | * Support Web compilation. 49 | 50 | ## [3.2.1+1] 51 | 52 | * Bug fixed when Custom Keyboard has an area above footer builder. Thanks @lzhuor 53 | 54 | ## [3.2.1] 55 | 56 | * Exposed `overscroll` property in `KeyboardActions` in case you want an extra scroll below your focused input. (default: 12). 57 | 58 | ## [3.2.0] BREAKING CHANGE 59 | 60 | * `displayArrows` property was added in `KeyboardAction`. 61 | * `closeWidget` and `displayCloseWidget` were removed. Now you can add multiple toolbar buttons using `toolbarButtons` property from `KeyboardAction` (check the sample updated). 62 | * Set `displayDoneButton` to false if you don't want the DONE button by default. 63 | 64 | ## [3.1.3] 65 | 66 | * Now you can change the size of the arrow buttons using the Theme. (Check the sample.dart file from the example folder to get more info) 67 | * `keyboardSeparatorColor` was added in `KeyboardActionsConfig` to change the color of the line separator between keyboard and content. 68 | 69 | ## [3.1.2] 70 | 71 | * fixed issue with `keyboardActionsPlatform`. 72 | 73 | ## [3.1.1] 74 | 75 | * added `tapOutsideToDismiss` property inside `KeyboardActions` in case you want to press outside the keyboard to dismiss it. 76 | 77 | ## [3.1.0] BREAKING CHANGE 78 | 79 | * API improved 80 | * `FormKeyboardActions` was renamed to `KeyboardActions`. 81 | * `KeyboardCustomInput` was added to help you to create custom keyboards in an easy way. 82 | * added `enabled` property inside `KeyboardActions` in case you don't want to use `KeyboardActions` widget (tablets for example). 83 | * added `displayActionBar` property inside `KeyboardAction` in case you want to display/hide the keyboard bar (E.g: if you use footerBuilder and add your own done button inside that) 84 | * added `isDialog` property inside `KeyboardActions`. 85 | * Material color is transparent to avoid issues with the parent container. 86 | 87 | 88 | 89 | ## [3.0.0] BREAKING CHANGE 90 | 91 | * Restore the old API with some bug fixing 92 | 93 | ## [2.1.2+2] 94 | 95 | * Keyboard dismissed when press back on Android. 96 | 97 | ## [2.1.2+1] 98 | 99 | * Fixed issue when using `CupertinoPageScaffold`. 100 | 101 | ## [2.1.2] 102 | 103 | * Now you can use the `IconTheme.of(context).color` and `Theme.of(context).disabledColor` to set the colors of the arrow icons (up/down). 104 | 105 | ## [2.1.0 - 2.1.1+1] 106 | 107 | * Custom footer widget below keyboard bar 108 | * Now you can add your custom keyboard!! 109 | * Thanks @jayjwarrick again for the contribution 110 | 111 | ## [2.0.1] 112 | 113 | * Disable next & previous buttons when there is none 114 | 115 | ## [2.0.0] ** Breaking change ** 116 | 117 | * Now `KeyboardActions` works on Dialogs 118 | * Add KeyboardActionsConfig to make parameters easily swappable 119 | * Add `FormKeyboardActions.setKeyboardActions` and `FormKeyboardActions` to allow changing the config from anywhere in the child widget tree. (Check the sample) 120 | * Thanks @jayjwarrick for the contribution 121 | 122 | ## [1.0.4] 123 | 124 | * Added `enabled` attribute for KeyboardAction to skip the prev/next when the TextField is disabled 125 | 126 | ## [1.0.3] 127 | 128 | * Fixed android issue when return from background 129 | 130 | ## [1.0.0 - 1.0.2] 131 | 132 | * First release. 133 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Diego Velásquez López 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Keyboard Actions 2 | 3 | [![pub package](https://img.shields.io/pub/v/keyboard_actions.svg)](https://pub.dartlang.org/packages/keyboard_actions) 4 | 5 | Add features to the Android / iOS keyboard in a simple way. 6 | 7 | Because the keyboard that Android / iOS offers us specifically when we are in numeric mode, does not bring the button to hide the keyboard. 8 | This causes a lot of inconvenience for users, so this package allows adding functionality to the existing keyboard. 9 | 10 | 11 |

12 | 13 |

14 | 15 | ## Features 16 | 17 | - Done button for the keyboard (You can customize the button). 18 | - Move up/down between your Textfields (You can hide for set `nextFocus: false`). 19 | - Keyboard Bar customization. 20 | - Custom footer widget below keyboard bar 21 | - Create your own Keyboard in an easy way 22 | - You can use it for Android, iOS or both platforms. 23 | - Compatible with Dialog. 24 | 25 | Example of the custom footer: 26 | 27 | Screen Shot 2019-05-22 at 5 46 50 PM 28 | 29 | For more fun, use that widget as a custom keyboard with your custom input: 30 | 31 | Screen Shot 2019-05-22 at 5 46 54 PM 32 | 33 | 34 | Even more fun: 35 | 36 | [Watch the video](https://thumbs.gfycat.com/NimbleGraveDarwinsfox-mobile.mp4) 37 | 38 | ## Getting started 39 | 40 | You should ensure that you add the dependency in your flutter project. 41 | ```yaml 42 | dependencies: 43 | keyboard_actions: "^4.1.0" 44 | ``` 45 | 46 | You should then run `flutter packages upgrade` or update your packages in IntelliJ. 47 | 48 | ## Example Project 49 | 50 | There is an example project in the `example` folder where you can get more information. Check it out. Otherwise, keep reading to get up and running. 51 | 52 | ## Usage 53 | 54 | ```dart 55 | import 'package:flutter/material.dart'; 56 | import 'package:keyboard_actions/keyboard_actions.dart'; 57 | 58 | 59 | class Content extends StatefulWidget { 60 | const Content({ 61 | Key key, 62 | }) : super(key: key); 63 | 64 | @override 65 | _ContentState createState() => _ContentState(); 66 | } 67 | 68 | class _ContentState extends State { 69 | final FocusNode _nodeText1 = FocusNode(); 70 | final FocusNode _nodeText2 = FocusNode(); 71 | final FocusNode _nodeText3 = FocusNode(); 72 | final FocusNode _nodeText4 = FocusNode(); 73 | final FocusNode _nodeText5 = FocusNode(); 74 | final FocusNode _nodeText6 = FocusNode(); 75 | 76 | /// Creates the [KeyboardActionsConfig] to hook up the fields 77 | /// and their focus nodes to our [FormKeyboardActions]. 78 | KeyboardActionsConfig _buildConfig(BuildContext context) { 79 | return KeyboardActionsConfig( 80 | keyboardActionsPlatform: KeyboardActionsPlatform.ALL, 81 | keyboardBarColor: Colors.grey[200], 82 | nextFocus: true, 83 | actions: [ 84 | KeyboardActionsItem( 85 | focusNode: _nodeText1, 86 | ), 87 | KeyboardActionsItem(focusNode: _nodeText2, toolbarButtons: [ 88 | (node) { 89 | return GestureDetector( 90 | onTap: () => node.unfocus(), 91 | child: Padding( 92 | padding: EdgeInsets.all(8.0), 93 | child: Icon(Icons.close), 94 | ), 95 | ); 96 | } 97 | ]), 98 | KeyboardActionsItem( 99 | focusNode: _nodeText3, 100 | onTapAction: () { 101 | showDialog( 102 | context: context, 103 | builder: (context) { 104 | return AlertDialog( 105 | content: Text("Custom Action"), 106 | actions: [ 107 | FlatButton( 108 | child: Text("OK"), 109 | onPressed: () => Navigator.of(context).pop(), 110 | ) 111 | ], 112 | ); 113 | }); 114 | }, 115 | ), 116 | KeyboardActionsItem( 117 | focusNode: _nodeText4, 118 | displayCloseWidget: false, 119 | ), 120 | KeyboardActionsItem( 121 | focusNode: _nodeText5, 122 | toolbarButtons: [ 123 | //button 1 124 | (node) { 125 | return GestureDetector( 126 | onTap: () => node.unfocus(), 127 | child: Container( 128 | color: Colors.white, 129 | padding: EdgeInsets.all(8.0), 130 | child: Text( 131 | "CLOSE", 132 | style: TextStyle(color: Colors.black), 133 | ), 134 | ), 135 | ); 136 | }, 137 | //button 2 138 | (node) { 139 | return GestureDetector( 140 | onTap: () => node.unfocus(), 141 | child: Container( 142 | color: Colors.black, 143 | padding: EdgeInsets.all(8.0), 144 | child: Text( 145 | "DONE", 146 | style: TextStyle(color: Colors.white), 147 | ), 148 | ), 149 | ); 150 | } 151 | ], 152 | ), 153 | KeyboardActionsItem( 154 | focusNode: _nodeText6, 155 | footerBuilder: (_) => PreferredSize( 156 | child: SizedBox( 157 | height: 40, 158 | child: Center( 159 | child: Text('Custom Footer'), 160 | )), 161 | preferredSize: Size.fromHeight(40)), 162 | ), 163 | ], 164 | ); 165 | } 166 | 167 | @override 168 | Widget build(BuildContext context) { 169 | return KeyboardActions( 170 | config: _buildConfig(context), 171 | child: Center( 172 | child: Padding( 173 | padding: const EdgeInsets.all(15.0), 174 | child: Column( 175 | crossAxisAlignment: CrossAxisAlignment.stretch, 176 | children: [ 177 | TextField( 178 | keyboardType: TextInputType.number, 179 | focusNode: _nodeText1, 180 | decoration: InputDecoration( 181 | hintText: "Input Number", 182 | ), 183 | ), 184 | TextField( 185 | keyboardType: TextInputType.text, 186 | focusNode: _nodeText2, 187 | decoration: InputDecoration( 188 | hintText: "Input Text with Custom Done Button", 189 | ), 190 | ), 191 | TextField( 192 | keyboardType: TextInputType.number, 193 | focusNode: _nodeText3, 194 | decoration: InputDecoration( 195 | hintText: "Input Number with Custom Action", 196 | ), 197 | ), 198 | TextField( 199 | keyboardType: TextInputType.text, 200 | focusNode: _nodeText4, 201 | decoration: InputDecoration( 202 | hintText: "Input Text without Done button", 203 | ), 204 | ), 205 | TextField( 206 | keyboardType: TextInputType.number, 207 | focusNode: _nodeText5, 208 | decoration: InputDecoration( 209 | hintText: "Input Number with Toolbar Buttons", 210 | ), 211 | ), 212 | TextField( 213 | keyboardType: TextInputType.number, 214 | focusNode: _nodeText6, 215 | decoration: InputDecoration( 216 | hintText: "Input Number with Custom Footer", 217 | ), 218 | ), 219 | ], 220 | ), 221 | ), 222 | ), 223 | ); 224 | } 225 | } 226 | 227 | ``` 228 | 229 | ## Using Custom Keyboard 230 | 231 | ```dart 232 | import 'package:flutter/material.dart'; 233 | import 'package:keyboard_actions/keyboard_actions.dart'; 234 | 235 | class Content extends StatelessWidget { 236 | final FocusNode _nodeText7 = FocusNode(); 237 | final FocusNode _nodeText8 = FocusNode(); 238 | //This is only for custom keyboards 239 | final custom1Notifier = ValueNotifier("0"); 240 | final custom2Notifier = ValueNotifier(Colors.blue); 241 | 242 | /// Creates the [KeyboardActionsConfig] to hook up the fields 243 | /// and their focus nodes to our [FormKeyboardActions]. 244 | KeyboardActionsConfig _buildConfig(BuildContext context) { 245 | return KeyboardActionsConfig( 246 | keyboardActionsPlatform: KeyboardActionsPlatform.ALL, 247 | keyboardBarColor: Colors.grey[200], 248 | nextFocus: true, 249 | actions: [ 250 | KeyboardActionsItem( 251 | focusNode: _nodeText7, 252 | footerBuilder: (_) => CounterKeyboard( 253 | notifier: custom1Notifier, 254 | ), 255 | ), 256 | KeyboardActionsItem( 257 | focusNode: _nodeText8, 258 | footerBuilder: (_) => ColorPickerKeyboard( 259 | notifier: custom2Notifier, 260 | ), 261 | ), 262 | ], 263 | ); 264 | } 265 | 266 | @override 267 | Widget build(BuildContext context) { 268 | return KeyboardActions( 269 | config: _buildConfig(context), 270 | child: Center( 271 | child: Container( 272 | padding: const EdgeInsets.all(15.0), 273 | child: Column( 274 | crossAxisAlignment: CrossAxisAlignment.stretch, 275 | children: [ 276 | KeyboardCustomInput( 277 | focusNode: _nodeText7, 278 | height: 65, 279 | notifier: custom1Notifier, 280 | builder: (context, val, hasFocus) { 281 | return Container( 282 | alignment: Alignment.center, 283 | color: hasFocus ? Colors.grey[300] : Colors.white, 284 | child: Text( 285 | val, 286 | style: 287 | TextStyle(fontSize: 30, fontWeight: FontWeight.bold), 288 | ), 289 | ); 290 | }, 291 | ), 292 | KeyboardCustomInput( 293 | focusNode: _nodeText8, 294 | height: 65, 295 | notifier: custom2Notifier, 296 | builder: (context, val, hasFocus) { 297 | return Container( 298 | width: double.maxFinite, 299 | color: val ?? Colors.transparent, 300 | ); 301 | }, 302 | ), 303 | ], 304 | ), 305 | ), 306 | ), 307 | ); 308 | } 309 | } 310 | 311 | 312 | /// A quick example "keyboard" widget for picking a color. 313 | class ColorPickerKeyboard extends StatelessWidget 314 | with KeyboardCustomPanelMixin 315 | implements PreferredSizeWidget { 316 | final ValueNotifier notifier; 317 | static const double _kKeyboardHeight = 200; 318 | 319 | ColorPickerKeyboard({Key key, this.notifier}) : super(key: key); 320 | 321 | @override 322 | Widget build(BuildContext context) { 323 | final double rows = 3; 324 | final double screenWidth = MediaQuery.of(context).size.width; 325 | final int colorsCount = Colors.primaries.length; 326 | final int colorsPerRow = (colorsCount / rows).ceil(); 327 | final double itemWidth = screenWidth / colorsPerRow; 328 | final double itemHeight = _kKeyboardHeight / rows; 329 | 330 | return Container( 331 | height: _kKeyboardHeight, 332 | child: Wrap( 333 | children: [ 334 | for (final color in Colors.primaries) 335 | GestureDetector( 336 | onTap: () { 337 | updateValue(color); 338 | }, 339 | child: Container( 340 | color: color, 341 | width: itemWidth, 342 | height: itemHeight, 343 | ), 344 | ) 345 | ], 346 | ), 347 | ); 348 | } 349 | 350 | @override 351 | Size get preferredSize => Size.fromHeight(_kKeyboardHeight); 352 | } 353 | 354 | /// A quick example "keyboard" widget for counter value. 355 | class CounterKeyboard extends StatelessWidget 356 | with KeyboardCustomPanelMixin 357 | implements PreferredSizeWidget { 358 | final ValueNotifier notifier; 359 | 360 | CounterKeyboard({Key key, this.notifier}) : super(key: key); 361 | 362 | @override 363 | Size get preferredSize => Size.fromHeight(200); 364 | 365 | @override 366 | Widget build(BuildContext context) { 367 | return Container( 368 | height: preferredSize.height, 369 | child: Row( 370 | children: [ 371 | Expanded( 372 | child: InkWell( 373 | onTap: () { 374 | int value = int.tryParse(notifier.value) ?? 0; 375 | value--; 376 | updateValue(value.toString()); 377 | }, 378 | child: FittedBox( 379 | child: Text( 380 | "-", 381 | style: TextStyle( 382 | fontWeight: FontWeight.bold, 383 | ), 384 | ), 385 | ), 386 | ), 387 | ), 388 | Expanded( 389 | child: InkWell( 390 | onTap: () { 391 | int value = int.tryParse(notifier.value) ?? 0; 392 | value++; 393 | updateValue(value.toString()); 394 | }, 395 | child: FittedBox( 396 | child: Text( 397 | "+", 398 | style: TextStyle( 399 | fontWeight: FontWeight.bold, 400 | ), 401 | ), 402 | ), 403 | ), 404 | ), 405 | ], 406 | ), 407 | ); 408 | } 409 | } 410 | 411 | 412 | ``` 413 | 414 | 415 | 416 | You can follow me on twitter [@diegoveloper](https://www.twitter.com/diegoveloper) 417 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .vscode/ 21 | 22 | # Flutter/Dart/Pub related 23 | **/doc/api/ 24 | .dart_tool/ 25 | .flutter-plugins 26 | .packages 27 | .pub-cache/ 28 | .pub/ 29 | build/ 30 | 31 | # Android related 32 | **/android/**/gradle-wrapper.jar 33 | **/android/.gradle 34 | **/android/captures/ 35 | **/android/gradlew 36 | **/android/gradlew.bat 37 | **/android/local.properties 38 | **/android/**/GeneratedPluginRegistrant.java 39 | 40 | # iOS/XCode related 41 | **/ios/**/*.mode1v3 42 | **/ios/**/*.mode2v3 43 | **/ios/**/*.moved-aside 44 | **/ios/**/*.pbxuser 45 | **/ios/**/*.perspectivev3 46 | **/ios/**/*sync/ 47 | **/ios/**/.sconsign.dblite 48 | **/ios/**/.tags* 49 | **/ios/**/.vagrant/ 50 | **/ios/**/DerivedData/ 51 | **/ios/**/Icon? 52 | **/ios/**/Pods/ 53 | **/ios/**/.symlinks/ 54 | **/ios/**/profile 55 | **/ios/**/xcuserdata 56 | **/ios/.generated/ 57 | **/ios/Flutter/App.framework 58 | **/ios/Flutter/Flutter.framework 59 | **/ios/Flutter/Generated.xcconfig 60 | **/ios/Flutter/app.flx 61 | **/ios/Flutter/app.zip 62 | **/ios/Flutter/flutter_assets/ 63 | **/ios/ServiceDefinitions.json 64 | **/ios/Runner/GeneratedPluginRegistrant.* 65 | **/ios/Flutter/flutter_export_environment.sh 66 | 67 | # Exceptions to above rules. 68 | !**/ios/**/default.mode1v3 69 | !**/ios/**/default.mode2v3 70 | !**/ios/**/default.pbxuser 71 | !**/ios/**/default.perspectivev3 72 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 73 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 58c8489fcdb4e4ef6c010117584c9b23d15221aa 8 | channel: dev 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.io/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.io/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.io/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | android { 28 | compileSdkVersion 31 29 | 30 | lintOptions { 31 | disable 'InvalidPackage' 32 | } 33 | 34 | defaultConfig { 35 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 36 | applicationId "com.example.example" 37 | minSdkVersion 28 38 | targetSdkVersion 31 39 | versionCode flutterVersionCode.toInteger() 40 | versionName flutterVersionName 41 | } 42 | 43 | buildTypes { 44 | release { 45 | // TODO: Add your own signing config for the release build. 46 | // Signing with the debug keys for now, so `flutter run --release` works. 47 | signingConfig signingConfigs.debug 48 | } 49 | } 50 | } 51 | 52 | flutter { 53 | source '../..' 54 | } 55 | 56 | dependencies { 57 | testImplementation 'junit:junit:4.12' 58 | } 59 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 13 | 21 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/example/example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.example; 2 | 3 | import io.flutter.embedding.android.FlutterActivity; 4 | 5 | public class MainActivity extends FlutterActivity {} 6 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegoveloper/flutter_keyboard_actions/7633812719ba04fae5526e5b564a7703e2668933/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegoveloper/flutter_keyboard_actions/7633812719ba04fae5526e5b564a7703e2668933/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegoveloper/flutter_keyboard_actions/7633812719ba04fae5526e5b564a7703e2668933/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegoveloper/flutter_keyboard_actions/7633812719ba04fae5526e5b564a7703e2668933/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegoveloper/flutter_keyboard_actions/7633812719ba04fae5526e5b564a7703e2668933/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:4.1.0' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip 7 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 13 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 14 | 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 15 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 16 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 17 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXCopyFilesBuildPhase section */ 21 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 22 | isa = PBXCopyFilesBuildPhase; 23 | buildActionMask = 2147483647; 24 | dstPath = ""; 25 | dstSubfolderSpec = 10; 26 | files = ( 27 | ); 28 | name = "Embed Frameworks"; 29 | runOnlyForDeploymentPostprocessing = 0; 30 | }; 31 | /* End PBXCopyFilesBuildPhase section */ 32 | 33 | /* Begin PBXFileReference section */ 34 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 35 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 36 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 37 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 38 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 39 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 40 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 41 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 42 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 43 | 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 44 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 45 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 46 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 47 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 48 | /* End PBXFileReference section */ 49 | 50 | /* Begin PBXFrameworksBuildPhase section */ 51 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 52 | isa = PBXFrameworksBuildPhase; 53 | buildActionMask = 2147483647; 54 | files = ( 55 | ); 56 | runOnlyForDeploymentPostprocessing = 0; 57 | }; 58 | /* End PBXFrameworksBuildPhase section */ 59 | 60 | /* Begin PBXGroup section */ 61 | 9740EEB11CF90186004384FC /* Flutter */ = { 62 | isa = PBXGroup; 63 | children = ( 64 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 65 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 66 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 67 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 68 | ); 69 | name = Flutter; 70 | sourceTree = ""; 71 | }; 72 | 97C146E51CF9000F007C117D = { 73 | isa = PBXGroup; 74 | children = ( 75 | 9740EEB11CF90186004384FC /* Flutter */, 76 | 97C146F01CF9000F007C117D /* Runner */, 77 | 97C146EF1CF9000F007C117D /* Products */, 78 | CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, 79 | ); 80 | sourceTree = ""; 81 | }; 82 | 97C146EF1CF9000F007C117D /* Products */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 97C146EE1CF9000F007C117D /* Runner.app */, 86 | ); 87 | name = Products; 88 | sourceTree = ""; 89 | }; 90 | 97C146F01CF9000F007C117D /* Runner */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, 94 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 95 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 96 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 98 | 97C147021CF9000F007C117D /* Info.plist */, 99 | 97C146F11CF9000F007C117D /* Supporting Files */, 100 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 101 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 102 | ); 103 | path = Runner; 104 | sourceTree = ""; 105 | }; 106 | 97C146F11CF9000F007C117D /* Supporting Files */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | 97C146F21CF9000F007C117D /* main.m */, 110 | ); 111 | name = "Supporting Files"; 112 | sourceTree = ""; 113 | }; 114 | /* End PBXGroup section */ 115 | 116 | /* Begin PBXNativeTarget section */ 117 | 97C146ED1CF9000F007C117D /* Runner */ = { 118 | isa = PBXNativeTarget; 119 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 120 | buildPhases = ( 121 | 9740EEB61CF901F6004384FC /* Run Script */, 122 | 97C146EA1CF9000F007C117D /* Sources */, 123 | 97C146EB1CF9000F007C117D /* Frameworks */, 124 | 97C146EC1CF9000F007C117D /* Resources */, 125 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 126 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 127 | ); 128 | buildRules = ( 129 | ); 130 | dependencies = ( 131 | ); 132 | name = Runner; 133 | productName = Runner; 134 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 135 | productType = "com.apple.product-type.application"; 136 | }; 137 | /* End PBXNativeTarget section */ 138 | 139 | /* Begin PBXProject section */ 140 | 97C146E61CF9000F007C117D /* Project object */ = { 141 | isa = PBXProject; 142 | attributes = { 143 | LastUpgradeCheck = 1300; 144 | ORGANIZATIONNAME = "The Chromium Authors"; 145 | TargetAttributes = { 146 | 97C146ED1CF9000F007C117D = { 147 | CreatedOnToolsVersion = 7.3.1; 148 | }; 149 | }; 150 | }; 151 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 152 | compatibilityVersion = "Xcode 3.2"; 153 | developmentRegion = English; 154 | hasScannedForEncodings = 0; 155 | knownRegions = ( 156 | en, 157 | Base, 158 | ); 159 | mainGroup = 97C146E51CF9000F007C117D; 160 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 161 | projectDirPath = ""; 162 | projectRoot = ""; 163 | targets = ( 164 | 97C146ED1CF9000F007C117D /* Runner */, 165 | ); 166 | }; 167 | /* End PBXProject section */ 168 | 169 | /* Begin PBXResourcesBuildPhase section */ 170 | 97C146EC1CF9000F007C117D /* Resources */ = { 171 | isa = PBXResourcesBuildPhase; 172 | buildActionMask = 2147483647; 173 | files = ( 174 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 175 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 176 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 177 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 178 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 179 | ); 180 | runOnlyForDeploymentPostprocessing = 0; 181 | }; 182 | /* End PBXResourcesBuildPhase section */ 183 | 184 | /* Begin PBXShellScriptBuildPhase section */ 185 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 186 | isa = PBXShellScriptBuildPhase; 187 | alwaysOutOfDate = 1; 188 | buildActionMask = 2147483647; 189 | files = ( 190 | ); 191 | inputPaths = ( 192 | "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", 193 | ); 194 | name = "Thin Binary"; 195 | outputPaths = ( 196 | ); 197 | runOnlyForDeploymentPostprocessing = 0; 198 | shellPath = /bin/sh; 199 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 200 | }; 201 | 9740EEB61CF901F6004384FC /* Run Script */ = { 202 | isa = PBXShellScriptBuildPhase; 203 | alwaysOutOfDate = 1; 204 | buildActionMask = 2147483647; 205 | files = ( 206 | ); 207 | inputPaths = ( 208 | ); 209 | name = "Run Script"; 210 | outputPaths = ( 211 | ); 212 | runOnlyForDeploymentPostprocessing = 0; 213 | shellPath = /bin/sh; 214 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 215 | }; 216 | /* End PBXShellScriptBuildPhase section */ 217 | 218 | /* Begin PBXSourcesBuildPhase section */ 219 | 97C146EA1CF9000F007C117D /* Sources */ = { 220 | isa = PBXSourcesBuildPhase; 221 | buildActionMask = 2147483647; 222 | files = ( 223 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, 224 | 97C146F31CF9000F007C117D /* main.m in Sources */, 225 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 226 | ); 227 | runOnlyForDeploymentPostprocessing = 0; 228 | }; 229 | /* End PBXSourcesBuildPhase section */ 230 | 231 | /* Begin PBXVariantGroup section */ 232 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 233 | isa = PBXVariantGroup; 234 | children = ( 235 | 97C146FB1CF9000F007C117D /* Base */, 236 | ); 237 | name = Main.storyboard; 238 | sourceTree = ""; 239 | }; 240 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 241 | isa = PBXVariantGroup; 242 | children = ( 243 | 97C147001CF9000F007C117D /* Base */, 244 | ); 245 | name = LaunchScreen.storyboard; 246 | sourceTree = ""; 247 | }; 248 | /* End PBXVariantGroup section */ 249 | 250 | /* Begin XCBuildConfiguration section */ 251 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 252 | isa = XCBuildConfiguration; 253 | buildSettings = { 254 | ALWAYS_SEARCH_USER_PATHS = NO; 255 | CLANG_ANALYZER_NONNULL = YES; 256 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 257 | CLANG_CXX_LIBRARY = "libc++"; 258 | CLANG_ENABLE_MODULES = YES; 259 | CLANG_ENABLE_OBJC_ARC = YES; 260 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 261 | CLANG_WARN_BOOL_CONVERSION = YES; 262 | CLANG_WARN_COMMA = YES; 263 | CLANG_WARN_CONSTANT_CONVERSION = YES; 264 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 265 | CLANG_WARN_EMPTY_BODY = YES; 266 | CLANG_WARN_ENUM_CONVERSION = YES; 267 | CLANG_WARN_INFINITE_RECURSION = YES; 268 | CLANG_WARN_INT_CONVERSION = YES; 269 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 270 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 271 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 272 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 273 | CLANG_WARN_STRICT_PROTOTYPES = YES; 274 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 275 | CLANG_WARN_UNREACHABLE_CODE = YES; 276 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 277 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 278 | COPY_PHASE_STRIP = NO; 279 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 280 | ENABLE_NS_ASSERTIONS = NO; 281 | ENABLE_STRICT_OBJC_MSGSEND = YES; 282 | GCC_C_LANGUAGE_STANDARD = gnu99; 283 | GCC_NO_COMMON_BLOCKS = YES; 284 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 285 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 286 | GCC_WARN_UNDECLARED_SELECTOR = YES; 287 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 288 | GCC_WARN_UNUSED_FUNCTION = YES; 289 | GCC_WARN_UNUSED_VARIABLE = YES; 290 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 291 | MTL_ENABLE_DEBUG_INFO = NO; 292 | SDKROOT = iphoneos; 293 | TARGETED_DEVICE_FAMILY = "1,2"; 294 | VALIDATE_PRODUCT = YES; 295 | }; 296 | name = Profile; 297 | }; 298 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 299 | isa = XCBuildConfiguration; 300 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 301 | buildSettings = { 302 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 303 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 304 | DEVELOPMENT_TEAM = S8QB4VV633; 305 | ENABLE_BITCODE = NO; 306 | FRAMEWORK_SEARCH_PATHS = ( 307 | "$(inherited)", 308 | "$(PROJECT_DIR)/Flutter", 309 | ); 310 | INFOPLIST_FILE = Runner/Info.plist; 311 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 312 | LIBRARY_SEARCH_PATHS = ( 313 | "$(inherited)", 314 | "$(PROJECT_DIR)/Flutter", 315 | ); 316 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example; 317 | PRODUCT_NAME = "$(TARGET_NAME)"; 318 | VERSIONING_SYSTEM = "apple-generic"; 319 | }; 320 | name = Profile; 321 | }; 322 | 97C147031CF9000F007C117D /* Debug */ = { 323 | isa = XCBuildConfiguration; 324 | buildSettings = { 325 | ALWAYS_SEARCH_USER_PATHS = NO; 326 | CLANG_ANALYZER_NONNULL = YES; 327 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 328 | CLANG_CXX_LIBRARY = "libc++"; 329 | CLANG_ENABLE_MODULES = YES; 330 | CLANG_ENABLE_OBJC_ARC = YES; 331 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 332 | CLANG_WARN_BOOL_CONVERSION = YES; 333 | CLANG_WARN_COMMA = YES; 334 | CLANG_WARN_CONSTANT_CONVERSION = YES; 335 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 336 | CLANG_WARN_EMPTY_BODY = YES; 337 | CLANG_WARN_ENUM_CONVERSION = YES; 338 | CLANG_WARN_INFINITE_RECURSION = YES; 339 | CLANG_WARN_INT_CONVERSION = YES; 340 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 341 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 342 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 343 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 344 | CLANG_WARN_STRICT_PROTOTYPES = YES; 345 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 346 | CLANG_WARN_UNREACHABLE_CODE = YES; 347 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 348 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 349 | COPY_PHASE_STRIP = NO; 350 | DEBUG_INFORMATION_FORMAT = dwarf; 351 | ENABLE_STRICT_OBJC_MSGSEND = YES; 352 | ENABLE_TESTABILITY = YES; 353 | GCC_C_LANGUAGE_STANDARD = gnu99; 354 | GCC_DYNAMIC_NO_PIC = NO; 355 | GCC_NO_COMMON_BLOCKS = YES; 356 | GCC_OPTIMIZATION_LEVEL = 0; 357 | GCC_PREPROCESSOR_DEFINITIONS = ( 358 | "DEBUG=1", 359 | "$(inherited)", 360 | ); 361 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 362 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 363 | GCC_WARN_UNDECLARED_SELECTOR = YES; 364 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 365 | GCC_WARN_UNUSED_FUNCTION = YES; 366 | GCC_WARN_UNUSED_VARIABLE = YES; 367 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 368 | MTL_ENABLE_DEBUG_INFO = YES; 369 | ONLY_ACTIVE_ARCH = YES; 370 | SDKROOT = iphoneos; 371 | TARGETED_DEVICE_FAMILY = "1,2"; 372 | }; 373 | name = Debug; 374 | }; 375 | 97C147041CF9000F007C117D /* Release */ = { 376 | isa = XCBuildConfiguration; 377 | buildSettings = { 378 | ALWAYS_SEARCH_USER_PATHS = NO; 379 | CLANG_ANALYZER_NONNULL = YES; 380 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 381 | CLANG_CXX_LIBRARY = "libc++"; 382 | CLANG_ENABLE_MODULES = YES; 383 | CLANG_ENABLE_OBJC_ARC = YES; 384 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 385 | CLANG_WARN_BOOL_CONVERSION = YES; 386 | CLANG_WARN_COMMA = YES; 387 | CLANG_WARN_CONSTANT_CONVERSION = YES; 388 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 389 | CLANG_WARN_EMPTY_BODY = YES; 390 | CLANG_WARN_ENUM_CONVERSION = YES; 391 | CLANG_WARN_INFINITE_RECURSION = YES; 392 | CLANG_WARN_INT_CONVERSION = YES; 393 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 394 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 395 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 396 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 397 | CLANG_WARN_STRICT_PROTOTYPES = YES; 398 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 399 | CLANG_WARN_UNREACHABLE_CODE = YES; 400 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 401 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 402 | COPY_PHASE_STRIP = NO; 403 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 404 | ENABLE_NS_ASSERTIONS = NO; 405 | ENABLE_STRICT_OBJC_MSGSEND = YES; 406 | GCC_C_LANGUAGE_STANDARD = gnu99; 407 | GCC_NO_COMMON_BLOCKS = YES; 408 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 409 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 410 | GCC_WARN_UNDECLARED_SELECTOR = YES; 411 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 412 | GCC_WARN_UNUSED_FUNCTION = YES; 413 | GCC_WARN_UNUSED_VARIABLE = YES; 414 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 415 | MTL_ENABLE_DEBUG_INFO = NO; 416 | SDKROOT = iphoneos; 417 | TARGETED_DEVICE_FAMILY = "1,2"; 418 | VALIDATE_PRODUCT = YES; 419 | }; 420 | name = Release; 421 | }; 422 | 97C147061CF9000F007C117D /* Debug */ = { 423 | isa = XCBuildConfiguration; 424 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 425 | buildSettings = { 426 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 427 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 428 | ENABLE_BITCODE = NO; 429 | FRAMEWORK_SEARCH_PATHS = ( 430 | "$(inherited)", 431 | "$(PROJECT_DIR)/Flutter", 432 | ); 433 | INFOPLIST_FILE = Runner/Info.plist; 434 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 435 | LIBRARY_SEARCH_PATHS = ( 436 | "$(inherited)", 437 | "$(PROJECT_DIR)/Flutter", 438 | ); 439 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example; 440 | PRODUCT_NAME = "$(TARGET_NAME)"; 441 | VERSIONING_SYSTEM = "apple-generic"; 442 | }; 443 | name = Debug; 444 | }; 445 | 97C147071CF9000F007C117D /* Release */ = { 446 | isa = XCBuildConfiguration; 447 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 448 | buildSettings = { 449 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 450 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 451 | ENABLE_BITCODE = NO; 452 | FRAMEWORK_SEARCH_PATHS = ( 453 | "$(inherited)", 454 | "$(PROJECT_DIR)/Flutter", 455 | ); 456 | INFOPLIST_FILE = Runner/Info.plist; 457 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 458 | LIBRARY_SEARCH_PATHS = ( 459 | "$(inherited)", 460 | "$(PROJECT_DIR)/Flutter", 461 | ); 462 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example; 463 | PRODUCT_NAME = "$(TARGET_NAME)"; 464 | VERSIONING_SYSTEM = "apple-generic"; 465 | }; 466 | name = Release; 467 | }; 468 | /* End XCBuildConfiguration section */ 469 | 470 | /* Begin XCConfigurationList section */ 471 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 472 | isa = XCConfigurationList; 473 | buildConfigurations = ( 474 | 97C147031CF9000F007C117D /* Debug */, 475 | 97C147041CF9000F007C117D /* Release */, 476 | 249021D3217E4FDB00AE95B9 /* Profile */, 477 | ); 478 | defaultConfigurationIsVisible = 0; 479 | defaultConfigurationName = Release; 480 | }; 481 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 482 | isa = XCConfigurationList; 483 | buildConfigurations = ( 484 | 97C147061CF9000F007C117D /* Debug */, 485 | 97C147071CF9000F007C117D /* Release */, 486 | 249021D4217E4FDB00AE95B9 /* Profile */, 487 | ); 488 | defaultConfigurationIsVisible = 0; 489 | defaultConfigurationName = Release; 490 | }; 491 | /* End XCConfigurationList section */ 492 | }; 493 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 494 | } 495 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application 7 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 8 | [GeneratedPluginRegistrant registerWithRegistry:self]; 9 | // Override point for customization after application launch. 10 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 11 | } 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegoveloper/flutter_keyboard_actions/7633812719ba04fae5526e5b564a7703e2668933/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegoveloper/flutter_keyboard_actions/7633812719ba04fae5526e5b564a7703e2668933/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegoveloper/flutter_keyboard_actions/7633812719ba04fae5526e5b564a7703e2668933/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegoveloper/flutter_keyboard_actions/7633812719ba04fae5526e5b564a7703e2668933/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegoveloper/flutter_keyboard_actions/7633812719ba04fae5526e5b564a7703e2668933/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegoveloper/flutter_keyboard_actions/7633812719ba04fae5526e5b564a7703e2668933/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegoveloper/flutter_keyboard_actions/7633812719ba04fae5526e5b564a7703e2668933/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegoveloper/flutter_keyboard_actions/7633812719ba04fae5526e5b564a7703e2668933/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegoveloper/flutter_keyboard_actions/7633812719ba04fae5526e5b564a7703e2668933/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegoveloper/flutter_keyboard_actions/7633812719ba04fae5526e5b564a7703e2668933/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegoveloper/flutter_keyboard_actions/7633812719ba04fae5526e5b564a7703e2668933/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegoveloper/flutter_keyboard_actions/7633812719ba04fae5526e5b564a7703e2668933/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegoveloper/flutter_keyboard_actions/7633812719ba04fae5526e5b564a7703e2668933/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegoveloper/flutter_keyboard_actions/7633812719ba04fae5526e5b564a7703e2668933/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegoveloper/flutter_keyboard_actions/7633812719ba04fae5526e5b564a7703e2668933/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegoveloper/flutter_keyboard_actions/7633812719ba04fae5526e5b564a7703e2668933/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegoveloper/flutter_keyboard_actions/7633812719ba04fae5526e5b564a7703e2668933/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diegoveloper/flutter_keyboard_actions/7633812719ba04fae5526e5b564a7703e2668933/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /example/ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/lib/content.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:keyboard_actions/keyboard_actions.dart'; 3 | import 'custom_input.dart'; 4 | 5 | //This could be StatelessWidget but it won't work on Dialogs for now until this issue is fixed: https://github.com/flutter/flutter/issues/45839 6 | class Content extends StatefulWidget { 7 | final bool isDialog; 8 | 9 | const Content({Key? key, this.isDialog = false}) : super(key: key); 10 | 11 | @override 12 | _ContentState createState() => _ContentState(); 13 | } 14 | 15 | class _ContentState extends State { 16 | final FocusNode _nodeText1 = FocusNode(); 17 | 18 | final FocusNode _nodeText2 = FocusNode(); 19 | 20 | final FocusNode _nodeText3 = FocusNode(); 21 | 22 | final FocusNode _nodeText4 = FocusNode(); 23 | 24 | final FocusNode _nodeText5 = FocusNode(); 25 | 26 | final FocusNode _nodeText6 = FocusNode(); 27 | 28 | final FocusNode _nodeText7 = FocusNode(); 29 | 30 | final FocusNode _nodeText8 = FocusNode(); 31 | 32 | final FocusNode _nodeText9 = FocusNode(); 33 | 34 | final FocusNode _nodeText10 = FocusNode(); 35 | 36 | final custom1Notifier = ValueNotifier("0"); 37 | 38 | final custom2Notifier = ValueNotifier(Colors.blue); 39 | 40 | final custom3Notifier = ValueNotifier(""); 41 | 42 | /// Creates the [KeyboardActionsConfig] to hook up the fields 43 | /// and their focus nodes to our [FormKeyboardActions]. 44 | KeyboardActionsConfig _buildConfig(BuildContext context) { 45 | return KeyboardActionsConfig( 46 | keyboardActionsPlatform: KeyboardActionsPlatform.ALL, 47 | keyboardBarColor: Colors.grey[200], 48 | nextFocus: true, 49 | actions: [ 50 | KeyboardActionsItem( 51 | focusNode: _nodeText1, 52 | ), 53 | KeyboardActionsItem(focusNode: _nodeText2, toolbarButtons: [ 54 | (node) { 55 | return GestureDetector( 56 | onTap: () => node.unfocus(), 57 | child: Padding( 58 | padding: EdgeInsets.all(8.0), 59 | child: Icon(Icons.close), 60 | ), 61 | ); 62 | } 63 | ]), 64 | KeyboardActionsItem( 65 | focusNode: _nodeText3, 66 | onTapAction: () async { 67 | await showDialog( 68 | context: context, 69 | builder: (context) { 70 | return AlertDialog( 71 | content: Text("Custom Action"), 72 | actions: [ 73 | TextButton( 74 | child: Text("OK"), 75 | onPressed: () => Navigator.of(context).pop(), 76 | ) 77 | ], 78 | ); 79 | }); 80 | }, 81 | ), 82 | KeyboardActionsItem( 83 | focusNode: _nodeText4, 84 | displayDoneButton: false, 85 | ), 86 | KeyboardActionsItem( 87 | focusNode: _nodeText5, 88 | toolbarButtons: [ 89 | //button 1 90 | (node) { 91 | return GestureDetector( 92 | onTap: () => node.unfocus(), 93 | child: Container( 94 | color: Colors.white, 95 | padding: EdgeInsets.all(8.0), 96 | child: Text( 97 | "CLOSE", 98 | style: TextStyle(color: Colors.black), 99 | ), 100 | ), 101 | ); 102 | }, 103 | //button 2 104 | (node) { 105 | return GestureDetector( 106 | onTap: () => node.unfocus(), 107 | child: Container( 108 | color: Colors.black, 109 | padding: EdgeInsets.all(8.0), 110 | child: Text( 111 | "DONE", 112 | style: TextStyle(color: Colors.white), 113 | ), 114 | ), 115 | ); 116 | } 117 | ], 118 | ), 119 | KeyboardActionsItem( 120 | focusNode: _nodeText6, 121 | footerBuilder: (_) => PreferredSize( 122 | child: SizedBox( 123 | height: 40, 124 | child: Center( 125 | child: Text('Custom Footer'), 126 | )), 127 | preferredSize: Size.fromHeight(40)), 128 | ), 129 | KeyboardActionsItem( 130 | focusNode: _nodeText7, 131 | displayActionBar: false, 132 | footerBuilder: (_) => PreferredSize( 133 | child: SizedBox( 134 | height: 40, 135 | child: Center( 136 | child: Text('Custom Footer'), 137 | )), 138 | preferredSize: Size.fromHeight(40)), 139 | ), 140 | KeyboardActionsItem( 141 | focusNode: _nodeText8, 142 | footerBuilder: (_) => CounterKeyboard( 143 | notifier: custom1Notifier, 144 | ), 145 | ), 146 | KeyboardActionsItem( 147 | focusNode: _nodeText9, 148 | footerBuilder: (_) => ColorPickerKeyboard( 149 | notifier: custom2Notifier, 150 | ), 151 | ), 152 | KeyboardActionsItem( 153 | focusNode: _nodeText10, 154 | displayActionBar: false, 155 | footerBuilder: (_) => NumericKeyboard( 156 | focusNode: _nodeText10, 157 | notifier: custom3Notifier, 158 | ), 159 | ), 160 | ], 161 | ); 162 | } 163 | 164 | @override 165 | Widget build(BuildContext context) { 166 | return KeyboardActions( 167 | isDialog: widget.isDialog, 168 | config: _buildConfig(context), 169 | child: Container( 170 | padding: const EdgeInsets.all(15.0), 171 | child: Center( 172 | child: Column( 173 | crossAxisAlignment: CrossAxisAlignment.stretch, 174 | mainAxisSize: MainAxisSize.min, 175 | children: [ 176 | TextField( 177 | keyboardType: TextInputType.number, 178 | focusNode: _nodeText1, 179 | decoration: InputDecoration( 180 | hintText: "Input Number", 181 | ), 182 | ), 183 | TextField( 184 | keyboardType: TextInputType.text, 185 | focusNode: _nodeText2, 186 | decoration: InputDecoration( 187 | hintText: "Input Text with Custom Done Widget", 188 | ), 189 | ), 190 | TextField( 191 | keyboardType: TextInputType.number, 192 | focusNode: _nodeText3, 193 | decoration: InputDecoration( 194 | hintText: "Input Number with Custom Action", 195 | ), 196 | ), 197 | TextField( 198 | keyboardType: TextInputType.text, 199 | focusNode: _nodeText4, 200 | decoration: InputDecoration( 201 | hintText: "Input Text without Done Button", 202 | ), 203 | ), 204 | TextField( 205 | keyboardType: TextInputType.number, 206 | focusNode: _nodeText5, 207 | decoration: InputDecoration( 208 | hintText: "Input Number with Toolbar Buttons", 209 | ), 210 | ), 211 | TextField( 212 | keyboardType: TextInputType.number, 213 | focusNode: _nodeText6, 214 | decoration: InputDecoration( 215 | hintText: "Input Number with Custom Footer", 216 | ), 217 | ), 218 | TextField( 219 | keyboardType: TextInputType.number, 220 | focusNode: _nodeText7, 221 | decoration: InputDecoration( 222 | hintText: "Input Number with Custom Footer without Bar", 223 | ), 224 | ), 225 | KeyboardCustomInput( 226 | focusNode: _nodeText8, 227 | height: 65, 228 | notifier: custom1Notifier, 229 | builder: (context, val, hasFocus) { 230 | return Container( 231 | alignment: Alignment.center, 232 | color: hasFocus == true ? Colors.grey[300] : Colors.white, 233 | child: Text( 234 | val, 235 | style: 236 | TextStyle(fontSize: 30, fontWeight: FontWeight.bold), 237 | ), 238 | ); 239 | }, 240 | ), 241 | KeyboardCustomInput( 242 | focusNode: _nodeText9, 243 | height: 65, 244 | notifier: custom2Notifier, 245 | builder: (context, val, hasFocus) { 246 | return Container( 247 | width: double.maxFinite, 248 | color: val, 249 | ); 250 | }, 251 | ), 252 | KeyboardCustomInput( 253 | focusNode: _nodeText10, 254 | height: 65, 255 | notifier: custom3Notifier, 256 | builder: (context, val, hasFocus) { 257 | return Container( 258 | alignment: Alignment.center, 259 | child: Text( 260 | val.isEmpty ? "Tap Here" : val, 261 | style: 262 | TextStyle(fontSize: 25, fontWeight: FontWeight.w500), 263 | ), 264 | ); 265 | }, 266 | ), 267 | ], 268 | ), 269 | ), 270 | ), 271 | ); 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /example/lib/custom_input.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:keyboard_actions/keyboard_actions.dart'; 3 | import 'package:intl/intl.dart'; 4 | 5 | /// A quick example "keyboard" widget for picking a color. 6 | class ColorPickerKeyboard extends StatelessWidget 7 | with KeyboardCustomPanelMixin 8 | implements PreferredSizeWidget { 9 | final ValueNotifier notifier; 10 | static const double _kKeyboardHeight = 200; 11 | 12 | ColorPickerKeyboard({Key? key, required this.notifier}) : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | final double rows = 3; 17 | final double screenWidth = MediaQuery.of(context).size.width; 18 | final int colorsCount = Colors.primaries.length; 19 | final int colorsPerRow = (colorsCount / rows).ceil(); 20 | final double itemWidth = screenWidth / colorsPerRow; 21 | final double itemHeight = _kKeyboardHeight / rows; 22 | 23 | return Container( 24 | height: _kKeyboardHeight, 25 | child: Wrap( 26 | children: [ 27 | for (final color in Colors.primaries) 28 | GestureDetector( 29 | onTap: () { 30 | updateValue(color); 31 | }, 32 | child: Container( 33 | color: color, 34 | width: itemWidth, 35 | height: itemHeight, 36 | ), 37 | ) 38 | ], 39 | ), 40 | ); 41 | } 42 | 43 | @override 44 | Size get preferredSize => Size.fromHeight(_kKeyboardHeight); 45 | } 46 | 47 | /// A quick example "keyboard" widget for Counter. 48 | class CounterKeyboard extends StatelessWidget 49 | with KeyboardCustomPanelMixin 50 | implements PreferredSizeWidget { 51 | final ValueNotifier notifier; 52 | 53 | CounterKeyboard({Key? key, required this.notifier}) : super(key: key); 54 | 55 | @override 56 | Size get preferredSize => Size.fromHeight(200); 57 | 58 | @override 59 | Widget build(BuildContext context) { 60 | return Container( 61 | height: preferredSize.height, 62 | child: Row( 63 | children: [ 64 | Expanded( 65 | child: InkWell( 66 | onTap: () { 67 | int value = int.tryParse(notifier.value) ?? 0; 68 | value--; 69 | updateValue(value.toString()); 70 | }, 71 | child: FittedBox( 72 | child: Text( 73 | "-", 74 | style: TextStyle( 75 | fontWeight: FontWeight.bold, 76 | ), 77 | ), 78 | ), 79 | ), 80 | ), 81 | Expanded( 82 | child: InkWell( 83 | onTap: () { 84 | int value = int.tryParse(notifier.value) ?? 0; 85 | value++; 86 | updateValue(value.toString()); 87 | }, 88 | child: FittedBox( 89 | child: Text( 90 | "+", 91 | style: TextStyle( 92 | fontWeight: FontWeight.bold, 93 | ), 94 | ), 95 | ), 96 | ), 97 | ), 98 | ], 99 | ), 100 | ); 101 | } 102 | } 103 | 104 | /// A quick example "keyboard" widget for Numeric. 105 | class NumericKeyboard extends StatelessWidget 106 | with KeyboardCustomPanelMixin 107 | implements PreferredSizeWidget { 108 | final ValueNotifier notifier; 109 | final FocusNode focusNode; 110 | 111 | NumericKeyboard({ 112 | Key? key, 113 | required this.notifier, 114 | required this.focusNode, 115 | }) : super(key: key); 116 | 117 | @override 118 | Size get preferredSize => Size.fromHeight(280); 119 | 120 | final format = NumberFormat("0000"); 121 | 122 | String _formatValue(String value) { 123 | final updatedValue = format.format(double.parse(value)); 124 | final finalValue = updatedValue.substring(0, updatedValue.length - 2) + 125 | "." + 126 | updatedValue.substring(updatedValue.length - 2, updatedValue.length); 127 | return finalValue; 128 | } 129 | 130 | void _onTapNumber(String value) { 131 | if (value == "Done") { 132 | focusNode.unfocus(); 133 | return; 134 | } 135 | final currentValue = notifier.value.replaceAll(".", ""); 136 | final temp = currentValue + value; 137 | updateValue(_formatValue(temp)); 138 | } 139 | 140 | void _onTapBackspace() { 141 | final currentValue = notifier.value.replaceAll(".", ""); 142 | final temp = currentValue.substring(0, currentValue.length - 1); 143 | updateValue(_formatValue(temp)); 144 | } 145 | 146 | @override 147 | Widget build(BuildContext context) { 148 | return Container( 149 | height: preferredSize.height, 150 | color: Color(0xFF313131), 151 | child: Padding( 152 | padding: const EdgeInsets.all(8.0), 153 | child: GridView( 154 | shrinkWrap: true, 155 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 156 | crossAxisCount: 3, 157 | childAspectRatio: 2.2, 158 | crossAxisSpacing: 10, 159 | mainAxisSpacing: 10, 160 | ), 161 | children: [ 162 | _buildButton(text: "7"), 163 | _buildButton(text: "8"), 164 | _buildButton(text: "9"), 165 | _buildButton(text: "4"), 166 | _buildButton(text: "5"), 167 | _buildButton(text: "6"), 168 | _buildButton(text: "1"), 169 | _buildButton(text: "2"), 170 | _buildButton(text: "3"), 171 | _buildButton(icon: Icons.backspace, color: Colors.black), 172 | _buildButton(text: "0"), 173 | _buildButton(text: "Done", color: Colors.black), 174 | ], 175 | ), 176 | ), 177 | ); 178 | } 179 | 180 | Widget _buildButton({ 181 | String? text, 182 | IconData? icon, 183 | Color? color, 184 | }) => 185 | NumericButton( 186 | text: text, 187 | icon: icon, 188 | color: color, 189 | onTap: () => icon != null ? _onTapBackspace() : _onTapNumber(text!), 190 | ); 191 | } 192 | 193 | class NumericButton extends StatelessWidget { 194 | final String? text; 195 | final VoidCallback onTap; 196 | final IconData? icon; 197 | final Color? color; 198 | 199 | const NumericButton({ 200 | Key? key, 201 | this.text, 202 | required this.onTap, 203 | this.icon, 204 | this.color, 205 | }) : assert((icon != null) != (text != null)), 206 | super(key: key); 207 | 208 | @override 209 | Widget build(BuildContext context) { 210 | return Material( 211 | borderRadius: BorderRadius.circular(5.0), 212 | color: color ?? Color(0xFF4A4A4A), 213 | elevation: 5, 214 | child: InkWell( 215 | onTap: onTap, 216 | child: FittedBox( 217 | child: Padding( 218 | padding: const EdgeInsets.all(3.0), 219 | child: icon != null 220 | ? Icon( 221 | icon, 222 | color: Colors.white, 223 | ) 224 | : Text( 225 | text!, 226 | style: TextStyle( 227 | color: Colors.white, 228 | fontWeight: FontWeight.w300, 229 | ), 230 | ), 231 | ), 232 | ), 233 | ), 234 | ); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/content.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'sample.dart'; 5 | import 'sample2.dart'; 6 | import 'sample3.dart'; 7 | import 'sample4.dart'; 8 | import 'sample5.dart'; 9 | 10 | // Application entry-point 11 | void main() => runApp(MyApp()); 12 | 13 | class MyApp extends StatelessWidget { 14 | const MyApp({Key? key}) : super(key: key); 15 | 16 | _openWidget(BuildContext context, Widget widget) => 17 | Navigator.of(context).push( 18 | MaterialPageRoute(builder: (_) => widget), 19 | ); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return MaterialApp( 24 | theme: ThemeData( 25 | primarySwatch: Colors.blue, 26 | ), 27 | home: Scaffold( 28 | backgroundColor: Colors.amber, 29 | body: Builder( 30 | builder: (myContext) => Center( 31 | child: Padding( 32 | padding: const EdgeInsets.all(18.0), 33 | child: Column( 34 | mainAxisAlignment: MainAxisAlignment.center, 35 | crossAxisAlignment: CrossAxisAlignment.stretch, 36 | children: [ 37 | ElevatedButton( 38 | child: Text("Full Screen form"), 39 | onPressed: () => _openWidget( 40 | myContext, 41 | ScaffoldTest(), 42 | ), 43 | ), 44 | const SizedBox( 45 | height: 25, 46 | ), 47 | ElevatedButton( 48 | child: Text("Dialog form"), 49 | onPressed: () => _openWidget( 50 | myContext, 51 | DialogTest(), 52 | ), 53 | ), 54 | const SizedBox( 55 | height: 25, 56 | ), 57 | ElevatedButton( 58 | child: Text("Custom Sample 1"), 59 | onPressed: () => _openWidget( 60 | myContext, 61 | Sample(), 62 | ), 63 | ), 64 | const SizedBox( 65 | height: 25, 66 | ), 67 | ElevatedButton( 68 | child: Text("Custom Sample 2"), 69 | onPressed: () => _openWidget( 70 | myContext, 71 | Sample2(), 72 | ), 73 | ), 74 | const SizedBox( 75 | height: 25, 76 | ), 77 | ElevatedButton( 78 | child: Text("Custom Sample 3"), 79 | onPressed: () => _openWidget( 80 | myContext, 81 | Sample3(), 82 | ), 83 | ), 84 | const SizedBox( 85 | height: 25, 86 | ), 87 | ElevatedButton( 88 | child: Text("Custom Sample 4"), 89 | onPressed: () => _openWidget( 90 | myContext, 91 | Sample4(), 92 | ), 93 | ), 94 | const SizedBox( 95 | height: 25, 96 | ), 97 | ElevatedButton( 98 | child: Text("Custom Sample 5"), 99 | onPressed: () => _openWidget( 100 | myContext, 101 | Sample5(), 102 | ), 103 | ), 104 | ], 105 | ), 106 | ), 107 | ), 108 | ), 109 | ), 110 | ); 111 | } 112 | } 113 | 114 | /// Displays our [TextField]s in a [Scaffold] with a [FormKeyboardActions]. 115 | class ScaffoldTest extends StatelessWidget { 116 | @override 117 | Widget build(BuildContext context) { 118 | return Scaffold( 119 | appBar: AppBar( 120 | title: Text("Keyboard Actions Sample"), 121 | ), 122 | body: Content(), 123 | ); 124 | } 125 | } 126 | 127 | /// Displays our [FormKeyboardActions] nested in a [AlertDialog]. 128 | class DialogTest extends StatelessWidget { 129 | @override 130 | Widget build(BuildContext context) { 131 | return Scaffold( 132 | appBar: AppBar( 133 | title: Text("Keyboard Actions Sample"), 134 | ), 135 | body: Center( 136 | child: TextButton( 137 | child: Text('Launch dialog'), 138 | onPressed: () => _launchInDialog(context), 139 | ), 140 | ), 141 | ); 142 | } 143 | 144 | void _launchInDialog(BuildContext context) async { 145 | final height = MediaQuery.of(context).size.height / 3; 146 | await showDialog( 147 | context: context, 148 | builder: (context) { 149 | return AlertDialog( 150 | title: Text('Dialog test'), 151 | content: SizedBox( 152 | height: height, 153 | child: Content( 154 | isDialog: true, 155 | ), 156 | ), 157 | actions: [ 158 | TextButton( 159 | child: Text('Ok'), 160 | onPressed: () { 161 | Navigator.of(context).pop(); 162 | }, 163 | ), 164 | ], 165 | ); 166 | }, 167 | ); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /example/lib/sample.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:keyboard_actions/keyboard_actions.dart'; 3 | 4 | class Sample extends StatelessWidget { 5 | final _focusNodeName = FocusNode(); 6 | final _focusNodeQuantity = FocusNode(); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | final size = MediaQuery.of(context).size; 11 | return Scaffold( 12 | floatingActionButton: FloatingActionButton( 13 | child: Icon(Icons.place), 14 | onPressed: () { 15 | _focusNodeName.requestFocus(); 16 | }, 17 | ), 18 | appBar: AppBar( 19 | title: Text("KeyboardActions"), 20 | ), 21 | body: Padding( 22 | padding: const EdgeInsets.only(top: 15.0, left: 15.0, right: 15.0), 23 | child: Center( 24 | child: Theme( 25 | data: Theme.of(context).copyWith( 26 | disabledColor: Colors.blue, 27 | iconTheme: IconTheme.of(context).copyWith( 28 | color: Colors.red, 29 | size: 35, 30 | ), 31 | ), 32 | child: KeyboardActions( 33 | tapOutsideBehavior: TapOutsideBehavior.opaqueDismiss, 34 | config: KeyboardActionsConfig( 35 | keyboardSeparatorColor: Colors.purple, 36 | actions: [ 37 | KeyboardActionsItem( 38 | focusNode: _focusNodeName, 39 | ), 40 | KeyboardActionsItem( 41 | focusNode: _focusNodeQuantity, 42 | ), 43 | ], 44 | ), 45 | child: ListView( 46 | children: [ 47 | SizedBox( 48 | height: size.height / 4, 49 | child: FlutterLogo(), 50 | ), 51 | TextField( 52 | focusNode: _focusNodeName, 53 | decoration: InputDecoration( 54 | labelText: "Product Name", 55 | ), 56 | ), 57 | TextField( 58 | focusNode: _focusNodeQuantity, 59 | keyboardType: TextInputType.phone, 60 | decoration: InputDecoration( 61 | labelText: "Quantity", 62 | ), 63 | ), 64 | ], 65 | ), 66 | ), 67 | ), 68 | ), 69 | ), 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /example/lib/sample2.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:keyboard_actions/keyboard_actions.dart'; 3 | 4 | class Sample2 extends StatelessWidget { 5 | final _focusSample = FocusNode(); 6 | final _textController = TextEditingController(); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Scaffold( 11 | appBar: AppBar( 12 | title: Text("Sample 2"), 13 | ), 14 | body: Padding( 15 | padding: const EdgeInsets.only(top: 15.0, left: 15.0, right: 15.0), 16 | child: Center( 17 | child: KeyboardActions( 18 | tapOutsideBehavior: TapOutsideBehavior.translucentDismiss, 19 | config: KeyboardActionsConfig( 20 | keyboardSeparatorColor: Colors.purple, 21 | actions: [ 22 | KeyboardActionsItem( 23 | focusNode: _focusSample, 24 | displayArrows: false, 25 | displayActionBar: false, 26 | footerBuilder: (context) { 27 | return MyCustomBarWidget( 28 | node: _focusSample, 29 | controller: _textController, 30 | ); 31 | }, 32 | ), 33 | ], 34 | ), 35 | child: ListView( 36 | children: [ 37 | TextField( 38 | controller: _textController, 39 | focusNode: _focusSample, 40 | keyboardType: TextInputType.phone, 41 | decoration: InputDecoration( 42 | labelText: "Sample Input", 43 | ), 44 | ), 45 | ], 46 | ), 47 | ), 48 | ), 49 | ), 50 | ); 51 | } 52 | } 53 | 54 | class MyCustomBarWidget extends StatelessWidget implements PreferredSizeWidget { 55 | final FocusNode node; 56 | final TextEditingController controller; 57 | 58 | const MyCustomBarWidget({ 59 | Key? key, 60 | required this.node, 61 | required this.controller, 62 | }) : super(key: key); 63 | 64 | @override 65 | Widget build(BuildContext context) { 66 | return Row( 67 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 68 | children: [ 69 | IconButton( 70 | icon: Icon(Icons.access_alarm), 71 | onPressed: () => print('hello world 1')), 72 | IconButton( 73 | icon: Icon(Icons.send), onPressed: () => print(controller.text)), 74 | Spacer(), 75 | IconButton(icon: Icon(Icons.close), onPressed: () => node.unfocus()), 76 | ], 77 | ); 78 | } 79 | 80 | @override 81 | Size get preferredSize => Size.fromHeight(60); 82 | } 83 | -------------------------------------------------------------------------------- /example/lib/sample3.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:keyboard_actions/keyboard_actions.dart'; 3 | 4 | /// Sample [Widget] demonstrating the usage of [KeyboardActionsConfig.defaultDoneWidget]. 5 | class Sample3 extends StatelessWidget { 6 | final _focusNodes = 7 | Iterable.generate(7).map((_) => FocusNode()).toList(); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Scaffold( 12 | appBar: AppBar( 13 | title: Text("Sample 3"), 14 | ), 15 | body: Padding( 16 | padding: const EdgeInsets.only(top: 15.0, left: 15.0, right: 15.0), 17 | child: Center( 18 | child: KeyboardActions( 19 | tapOutsideBehavior: TapOutsideBehavior.translucentDismiss, 20 | config: KeyboardActionsConfig( 21 | // Define ``defaultDoneWidget`` only once in the config 22 | defaultDoneWidget: _buildMyDoneWidget(), 23 | actions: _focusNodes 24 | .map((focusNode) => KeyboardActionsItem(focusNode: focusNode)) 25 | .toList(), 26 | ), 27 | child: ListView.separated( 28 | itemBuilder: (ctx, idx) => TextField( 29 | focusNode: _focusNodes[idx], 30 | keyboardType: TextInputType.text, 31 | decoration: InputDecoration( 32 | labelText: "Field ${idx + 1}", 33 | ), 34 | ), 35 | separatorBuilder: (ctx, idx) => const SizedBox(height: 10.0), 36 | itemCount: _focusNodes.length, 37 | ), 38 | ), 39 | ), 40 | ), 41 | ); 42 | } 43 | 44 | /// Returns the custom [Widget] to be rendered as the *"Done"* button. 45 | Widget _buildMyDoneWidget() { 46 | return Row( 47 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 48 | children: [ 49 | Text('My Done Widget'), 50 | const SizedBox(width: 10.0), 51 | Icon(Icons.arrow_drop_down, size: 20.0), 52 | ], 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /example/lib/sample4.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:keyboard_actions/keyboard_actions.dart'; 3 | 4 | /// Sample [Widget] demonstrating the usage of [KeyboardActionsItem.toolbarAlignment]. 5 | class Sample4 extends StatelessWidget { 6 | final _focusSample = FocusNode(); 7 | final _textController = TextEditingController(); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Scaffold( 12 | appBar: AppBar( 13 | title: Text("Sample 4"), 14 | ), 15 | body: Padding( 16 | padding: const EdgeInsets.only(top: 15.0, left: 15.0, right: 15.0), 17 | child: Center( 18 | child: KeyboardActions( 19 | tapOutsideBehavior: TapOutsideBehavior.translucentDismiss, 20 | config: KeyboardActionsConfig( 21 | actions: [ 22 | KeyboardActionsItem( 23 | toolbarAlignment: MainAxisAlignment.spaceAround, 24 | focusNode: _focusSample, 25 | displayArrows: false, 26 | toolbarButtons: [ 27 | (_) { 28 | return IconButton( 29 | icon: Icon(Icons.format_bold), 30 | onPressed: () {}, 31 | ); 32 | }, 33 | (_) { 34 | return IconButton( 35 | icon: Icon(Icons.format_italic), 36 | onPressed: () {}, 37 | ); 38 | }, 39 | (_) { 40 | return IconButton( 41 | icon: Icon(Icons.format_underline), 42 | onPressed: () {}, 43 | ); 44 | }, 45 | (_) { 46 | return IconButton( 47 | icon: Icon(Icons.format_strikethrough), 48 | onPressed: () {}, 49 | ); 50 | }, 51 | ], 52 | ), 53 | ], 54 | ), 55 | child: ListView( 56 | children: [ 57 | TextField( 58 | controller: _textController, 59 | focusNode: _focusSample, 60 | decoration: InputDecoration( 61 | labelText: "Sample Input", 62 | ), 63 | ), 64 | ], 65 | ), 66 | ), 67 | ), 68 | ), 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /example/lib/sample5.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:keyboard_actions/keyboard_actions.dart'; 3 | 4 | /// Sample [Widget] demonstrating the usage of [KeyboardActionsConfig.defaultDoneWidget]. 5 | class Sample5 extends StatelessWidget { 6 | final _focusNodes = 7 | Iterable.generate(7).map((_) => FocusNode()).toList(); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Scaffold( 12 | appBar: AppBar( 13 | title: Text("Sample 5"), 14 | ), 15 | body: Padding( 16 | padding: const EdgeInsets.only(top: 15.0, left: 15.0, right: 15.0), 17 | child: Column(mainAxisSize: MainAxisSize.min, children: [ 18 | Expanded( 19 | flex: 2, 20 | child: Center( 21 | child: KeyboardActions( 22 | tapOutsideBehavior: TapOutsideBehavior.translucentDismiss, 23 | config: KeyboardActionsConfig( 24 | // Define ``defaultDoneWidget`` only once in the config 25 | defaultDoneWidget: _buildMyDoneWidget(), 26 | actions: _focusNodes 27 | .map((focusNode) => 28 | KeyboardActionsItem(focusNode: focusNode)) 29 | .toList(), 30 | ), 31 | child: ListView.separated( 32 | itemBuilder: (ctx, idx) => TextField( 33 | focusNode: _focusNodes[idx], 34 | keyboardType: TextInputType.text, 35 | decoration: InputDecoration( 36 | fillColor: Colors.red, 37 | filled: true, 38 | labelText: "Field ${idx + 1}", 39 | ), 40 | ), 41 | separatorBuilder: (ctx, idx) => 42 | const SizedBox(height: 10.0), 43 | itemCount: _focusNodes.length, 44 | ), 45 | ), 46 | )), 47 | Expanded( 48 | flex: 1, 49 | child: Container( 50 | color: Colors.green, 51 | child: Center( 52 | child: TextButton( 53 | onPressed: () {}, 54 | child: Text( 55 | 'I take up space below the KeyboardActions', 56 | style: TextStyle(color: Colors.white), 57 | ), 58 | )))) 59 | ]), 60 | ), 61 | ); 62 | } 63 | 64 | /// Returns the custom [Widget] to be rendered as the *"Done"* button. 65 | Widget _buildMyDoneWidget() { 66 | return Row( 67 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 68 | children: [ 69 | Text('My Done Widget'), 70 | const SizedBox(width: 10.0), 71 | Icon(Icons.arrow_drop_down, size: 20.0), 72 | ], 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: Sample project using keyboard actions. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # Read more about versioning at semver.org. 14 | version: 1.0.0+1 15 | 16 | environment: 17 | sdk: ">=2.12.0 <3.0.0" 18 | 19 | dependencies: 20 | flutter: 21 | sdk: flutter 22 | 23 | intl: any 24 | keyboard_actions: 25 | path: ../ 26 | 27 | 28 | dev_dependencies: 29 | flutter_test: 30 | sdk: flutter 31 | 32 | 33 | # For information on the generic Dart part of this file, see the 34 | # following page: https://www.dartlang.org/tools/pub/pubspec 35 | 36 | # The following section is specific to Flutter. 37 | flutter: 38 | 39 | # The following line ensures that the Material Icons font is 40 | # included with your application, so that you can use the icons in 41 | # the material Icons class. 42 | uses-material-design: true 43 | 44 | # To add assets to your application, add an assets section, like this: 45 | # assets: 46 | # - images/a_dot_burr.jpeg 47 | # - images/a_dot_ham.jpeg 48 | 49 | # An image asset can refer to one or more resolution-specific "variants", see 50 | # https://flutter.io/assets-and-images/#resolution-aware. 51 | 52 | # For details regarding adding assets from package dependencies, see 53 | # https://flutter.io/assets-and-images/#from-packages 54 | 55 | # To add custom fonts to your application, add a fonts section here, 56 | # in this "flutter" section. Each entry in this list should have a 57 | # "family" key with the font family name, and a "fonts" key with a 58 | # list giving the asset and other descriptors for the font. For 59 | # example: 60 | # fonts: 61 | # - family: Schyler 62 | # fonts: 63 | # - asset: fonts/Schyler-Regular.ttf 64 | # - asset: fonts/Schyler-Italic.ttf 65 | # style: italic 66 | # - family: Trajan Pro 67 | # fonts: 68 | # - asset: fonts/TrajanPro.ttf 69 | # - asset: fonts/TrajanPro_Bold.ttf 70 | # weight: 700 71 | # 72 | # For details regarding fonts from package dependencies, 73 | # see https://flutter.io/custom-fonts/#from-packages 74 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:example/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /ios/Flutter/flutter_export_environment.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This is a generated file; do not edit or check into version control. 3 | export "FLUTTER_ROOT=/Users/diego/fvm/versions/3.7.3" 4 | export "FLUTTER_APPLICATION_PATH=/Users/diego/Development/workspaces/flutter/flutter_keyboard_actions" 5 | export "COCOAPODS_PARALLEL_CODE_SIGN=true" 6 | export "FLUTTER_TARGET=lib/main.dart" 7 | export "FLUTTER_BUILD_DIR=build" 8 | export "FLUTTER_BUILD_NAME=4.1.1" 9 | export "FLUTTER_BUILD_NUMBER=4.1.1" 10 | export "DART_OBFUSCATION=false" 11 | export "TRACK_WIDGET_CREATION=true" 12 | export "TREE_SHAKE_ICONS=false" 13 | export "PACKAGE_CONFIG=.dart_tool/package_config.json" 14 | -------------------------------------------------------------------------------- /lib/external/keyboard_avoider/bottom_area_avoider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/rendering.dart'; 5 | 6 | /// Helps [child] stay visible by resizing it to avoid the given [areaToAvoid]. 7 | /// 8 | /// Wraps the [child] in a [AnimatedContainer] that adjusts its bottom [padding] to accommodate the given area. 9 | /// 10 | /// If [autoScroll] is true and the [child] contains a focused widget such as a [TextField], 11 | /// automatically scrolls so that it is just visible above the keyboard, plus any additional [overscroll]. 12 | class BottomAreaAvoider extends StatefulWidget { 13 | static const Duration defaultDuration = Duration(milliseconds: 100); 14 | static const Curve defaultCurve = Curves.easeIn; 15 | static const double defaultOverscroll = 12.0; 16 | static const bool defaultAutoScroll = false; 17 | 18 | /// The child to embed. 19 | /// 20 | /// If the [child] is not a [ScrollView], it is automatically embedded in a [SingleChildScrollView]. 21 | /// If the [child] is a [ScrollView], it must have a [ScrollController]. 22 | final Widget? child; 23 | 24 | /// Amount of bottom area to avoid. For example, the height of the currently-showing system keyboard, or 25 | /// any custom bottom overlays. 26 | final double areaToAvoid; 27 | 28 | /// Whether to auto-scroll to the focused widget after the keyboard appears. Defaults to false. 29 | /// Could be expensive because it searches all the child objects in this widget's render tree. 30 | final bool autoScroll; 31 | 32 | /// Extra amount to scroll past the focused widget. Defaults to [defaultOverscroll]. 33 | /// Useful in case the focused widget is inside a parent widget that you also want to be visible. 34 | final double overscroll; 35 | 36 | /// Duration of the resize animation. Defaults to [defaultDuration]. To disable, set to [Duration.zero]. 37 | final Duration duration; 38 | 39 | /// Animation curve. Defaults to [defaultCurve] 40 | final Curve curve; 41 | 42 | /// The [ScrollPhysics] of the [SingleChildScrollView] which contains child 43 | final ScrollPhysics? physics; 44 | 45 | BottomAreaAvoider( 46 | {Key? key, 47 | required this.child, 48 | required this.areaToAvoid, 49 | this.autoScroll = false, 50 | this.duration = defaultDuration, 51 | this.curve = defaultCurve, 52 | this.overscroll = defaultOverscroll, 53 | this.physics}) 54 | : //assert(child is ScrollView ? child.controller != null : true), 55 | assert(areaToAvoid >= 0, 'Cannot avoid a negative area'), 56 | super(key: key); 57 | 58 | BottomAreaAvoiderState createState() => BottomAreaAvoiderState(); 59 | } 60 | 61 | class BottomAreaAvoiderState extends State { 62 | final _animationKey = new GlobalKey(); 63 | Function(AnimationStatus)? _animationListener; 64 | ScrollController? _scrollController; 65 | late double _previousAreaToAvoid; 66 | 67 | @override 68 | void didUpdateWidget(BottomAreaAvoider oldWidget) { 69 | _previousAreaToAvoid = oldWidget.areaToAvoid; 70 | super.didUpdateWidget(oldWidget); 71 | } 72 | 73 | @override 74 | void dispose() { 75 | _animationKey.currentState?.animation 76 | .removeStatusListener(_animationListener!); 77 | super.dispose(); 78 | } 79 | 80 | @override 81 | Widget build(BuildContext context) { 82 | // Add a status listener to the animation after the initial build. 83 | // Wait a frame so that _animationKey.currentState is not null. 84 | if (_animationListener == null) { 85 | WidgetsBinding.instance.addPostFrameCallback((_) { 86 | _animationListener = _paddingAnimationStatusChanged; 87 | _animationKey.currentState?.animation 88 | .addStatusListener(_animationListener!); 89 | }); 90 | } 91 | 92 | // If [child] is a [ScrollView], get its [ScrollController] 93 | // and embed the [child] directly in an [AnimatedContainer]. 94 | if (widget.child is ScrollView) { 95 | var scrollView = widget.child as ScrollView; 96 | _scrollController = 97 | scrollView.controller ?? PrimaryScrollController.of(context); 98 | return _buildAnimatedContainer(widget.child); 99 | } 100 | // If [child] is not a [ScrollView], and [autoScroll] is true, 101 | // embed the [child] in a [SingleChildScrollView] to make 102 | // it possible to scroll to the focused widget. 103 | if (widget.autoScroll) { 104 | _scrollController = new ScrollController(); 105 | return _buildAnimatedContainer( 106 | LayoutBuilder( 107 | builder: (context, constraints) { 108 | return SingleChildScrollView( 109 | physics: widget.physics, 110 | controller: _scrollController, 111 | child: ConstrainedBox( 112 | constraints: BoxConstraints( 113 | minHeight: constraints.maxHeight, 114 | ), 115 | child: widget.child, 116 | ), 117 | ); 118 | }, 119 | ), 120 | ); 121 | } 122 | // Just embed the [child] directly in an [AnimatedContainer]. 123 | return _buildAnimatedContainer(widget.child); 124 | } 125 | 126 | Widget _buildAnimatedContainer(Widget? child) { 127 | return AnimatedContainer( 128 | key: _animationKey, 129 | color: Colors.transparent, 130 | padding: EdgeInsets.only(bottom: widget.areaToAvoid), 131 | duration: widget.duration, 132 | curve: widget.curve, 133 | child: child, 134 | ); 135 | } 136 | 137 | /// Called whenever the status of our padding animation changes. 138 | /// 139 | /// If the animation has completed, we added overlap, and scroll is on, scroll to that. 140 | void _paddingAnimationStatusChanged(AnimationStatus status) { 141 | if (status != AnimationStatus.completed) { 142 | return; // Only check when the animation is finishing 143 | } 144 | if (!widget.autoScroll) { 145 | return; // auto scroll is not enabled, do nothing 146 | } 147 | if (widget.areaToAvoid <= _previousAreaToAvoid) { 148 | return; // decreased-- do nothing. We only scroll when area to avoid is added (keyboard shown). 149 | } 150 | // Need to wait a frame to get the new size (todo: is this still needed? we dont use mediaquery anymore) 151 | WidgetsBinding.instance.addPostFrameCallback((_) { 152 | if (!mounted) { 153 | return; // context is no longer valid 154 | } 155 | scrollToOverscroll(); 156 | }); 157 | } 158 | 159 | void scrollToOverscroll() { 160 | final focused = findFocusedObject(context.findRenderObject()); 161 | if (focused == null || _scrollController == null) return; 162 | scrollToObject(focused, _scrollController!, widget.duration, widget.curve, 163 | widget.overscroll); 164 | } 165 | } 166 | 167 | /// Utility helper methods 168 | 169 | /// Finds the first focused focused child of [root] using a breadth-first search. 170 | RenderObject? findFocusedObject(RenderObject? root) { 171 | final q = Queue(); 172 | q.add(root); 173 | while (q.isNotEmpty) { 174 | final node = q.removeFirst()!; 175 | final config = SemanticsConfiguration(); 176 | //ignore: invalid_use_of_protected_member 177 | node.describeSemanticsConfiguration(config); 178 | if (config.isFocused) { 179 | return node; 180 | } 181 | node.visitChildrenForSemantics((child) { 182 | q.add(child); 183 | }); 184 | } 185 | return null; 186 | } 187 | 188 | /// Scroll to the given [object], which must be inside [scrollController]s viewport. 189 | scrollToObject(RenderObject object, ScrollController scrollController, 190 | Duration duration, Curve curve, double overscroll) { 191 | // Calculate the offset needed to show the object in the [ScrollView] 192 | // so that its bottom touches the top of the keyboard. 193 | final viewport = RenderAbstractViewport.of(object); 194 | final offset = viewport.getOffsetToReveal(object, 1.0).offset + overscroll; 195 | 196 | // If the object is covered by the keyboard, scroll to reveal it, 197 | // and add [focusPadding] between it and top of the keyboard. 198 | if (offset > scrollController.position.pixels) { 199 | scrollController.position.moveTo( 200 | offset, 201 | duration: duration, 202 | curve: curve, 203 | ); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /lib/external/keyboard_avoider/keyboard_avoider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/widgets.dart'; 4 | 5 | import 'bottom_area_avoider.dart'; 6 | 7 | /// A widget that re-sizes its [child] to avoid the system keyboard. 8 | /// 9 | /// Unlike a [Scaffold], it only insets by the actual amount obscured by the keyboard. 10 | /// 11 | /// Watches for media query changes via [didChangeMetrics], and adjusts a [BottomAreaAvoider] accordingly. 12 | class KeyboardAvoider extends StatefulWidget { 13 | /// See [BottomAreaAvoider.child] 14 | final Widget child; 15 | 16 | /// See [BottomAreaAvoider.duration] 17 | final Duration duration; 18 | 19 | /// See [BottomAreaAvoider.curve] 20 | final Curve curve; 21 | 22 | /// See [BottomAreaAvoider.autoScroll] 23 | final bool autoScroll; 24 | 25 | /// See [BottomAreaAvoider.overscroll] 26 | final double overscroll; 27 | 28 | /// See [BottomAreaAvoider.physics] 29 | final ScrollPhysics? physics; 30 | 31 | KeyboardAvoider({ 32 | Key? key, 33 | required this.child, 34 | this.physics, 35 | this.duration = BottomAreaAvoider.defaultDuration, 36 | this.curve = BottomAreaAvoider.defaultCurve, 37 | this.autoScroll = BottomAreaAvoider.defaultAutoScroll, 38 | this.overscroll = BottomAreaAvoider.defaultOverscroll, 39 | }) : assert(child is ScrollView ? child.controller != null : true), 40 | super(key: key); 41 | 42 | _KeyboardAvoiderState createState() => _KeyboardAvoiderState(); 43 | } 44 | 45 | class _KeyboardAvoiderState extends State 46 | with WidgetsBindingObserver { 47 | /// The current amount of keyboard overlap. 48 | double _keyboardOverlap = 0.0; 49 | 50 | @override 51 | void initState() { 52 | super.initState(); 53 | WidgetsBinding.instance.addObserver(this); 54 | } 55 | 56 | @override 57 | void dispose() { 58 | WidgetsBinding.instance.removeObserver(this); 59 | super.dispose(); 60 | } 61 | 62 | @override 63 | Widget build(BuildContext context) { 64 | return BottomAreaAvoider( 65 | child: widget.child, 66 | areaToAvoid: _keyboardOverlap, 67 | autoScroll: widget.autoScroll, 68 | curve: widget.curve, 69 | duration: widget.duration, 70 | overscroll: widget.overscroll, 71 | physics: widget.physics, 72 | ); 73 | } 74 | 75 | /// WidgetsBindingObserver 76 | 77 | @override 78 | void didChangeMetrics() { 79 | // Need to wait a frame to get the new size 80 | WidgetsBinding.instance.addPostFrameCallback((_) { 81 | _resize(); 82 | }); 83 | } 84 | 85 | /// Re-calculates the amount of overlap, based on the current [MediaQueryData.viewInsets]. 86 | void _resize() { 87 | if (!mounted) { 88 | return; 89 | } 90 | 91 | // Calculate Rect of widget on screen 92 | final object = context.findRenderObject()!; 93 | final box = object as RenderBox; 94 | final offset = box.localToGlobal(Offset.zero); 95 | final widgetRect = Rect.fromLTWH( 96 | offset.dx, 97 | offset.dy, 98 | box.size.width, 99 | box.size.height, 100 | ); 101 | 102 | // Calculate top of keyboard 103 | final mediaQuery = MediaQuery.of(context); 104 | final screenSize = mediaQuery.size; 105 | final screenInsets = mediaQuery.viewInsets; 106 | final keyboardTop = screenSize.height - screenInsets.bottom; 107 | 108 | // If widget is entirely covered by keyboard, do nothing 109 | if (widgetRect.top > keyboardTop) { 110 | return; 111 | } 112 | 113 | // If widget is partially obscured by the keyboard, adjust bottom padding to fully expose it 114 | final overlap = max(0.0, widgetRect.bottom - keyboardTop); 115 | if (overlap != _keyboardOverlap) { 116 | setState(() { 117 | _keyboardOverlap = overlap; 118 | }); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /lib/external/platform_check/platform_check.dart: -------------------------------------------------------------------------------- 1 | import 'platform_web.dart' if (dart.library.io) 'platform_io.dart'; 2 | 3 | /// Class to check which is the current platform allow the compilation from web/mobile/desktop 4 | abstract class PlatformCheck { 5 | static bool get isWeb => currentPlatform == PlatformCheckType.Web; 6 | static bool get isMacOS => currentPlatform == PlatformCheckType.MacOS; 7 | static bool get isWindows => currentPlatform == PlatformCheckType.Windows; 8 | static bool get isLinux => currentPlatform == PlatformCheckType.Linux; 9 | static bool get isAndroid => currentPlatform == PlatformCheckType.Android; 10 | static bool get isIOS => currentPlatform == PlatformCheckType.IOS; 11 | } 12 | 13 | enum PlatformCheckType { Web, Windows, Linux, MacOS, Android, Fuchsia, IOS } 14 | -------------------------------------------------------------------------------- /lib/external/platform_check/platform_io.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'platform_check.dart'; 3 | 4 | PlatformCheckType get currentPlatform { 5 | if (Platform.isWindows) return PlatformCheckType.Windows; 6 | if (Platform.isFuchsia) return PlatformCheckType.Fuchsia; 7 | if (Platform.isMacOS) return PlatformCheckType.MacOS; 8 | if (Platform.isLinux) return PlatformCheckType.Linux; 9 | if (Platform.isIOS) return PlatformCheckType.IOS; 10 | return PlatformCheckType.Android; 11 | } 12 | -------------------------------------------------------------------------------- /lib/external/platform_check/platform_web.dart: -------------------------------------------------------------------------------- 1 | import 'platform_check.dart'; 2 | 3 | //Default to web, the platform_io class will override this if it gets imported. 4 | PlatformCheckType get currentPlatform => PlatformCheckType.Web; 5 | -------------------------------------------------------------------------------- /lib/keyboard_actions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:keyboard_actions/external/keyboard_avoider/bottom_area_avoider.dart'; 6 | import 'package:keyboard_actions/external/platform_check/platform_check.dart'; 7 | 8 | import 'keyboard_actions_config.dart'; 9 | import 'keyboard_actions_item.dart'; 10 | 11 | export 'keyboard_actions_config.dart'; 12 | export 'keyboard_actions_item.dart'; 13 | export 'keyboard_custom.dart'; 14 | 15 | const double _kBarSize = 45.0; 16 | const Duration _timeToDismiss = Duration(milliseconds: 110); 17 | 18 | enum KeyboardActionsPlatform { 19 | ANDROID, 20 | IOS, 21 | ALL, 22 | } 23 | 24 | /// The behavior when tapped outside the keyboard. 25 | /// 26 | /// none: no overlay is added; 27 | /// 28 | /// opaqueDismiss: an overlay is added which blocks the underneath widgets from 29 | /// gestures. Once tapped, the keyboard will be dismissed; 30 | /// 31 | /// translucentDismiss: an overlay is added which permits the underneath widgets 32 | /// to receive gestures. Once tapped, the keyboard will be dismissed; 33 | enum TapOutsideBehavior { 34 | none, 35 | opaqueDismiss, 36 | translucentDismiss, 37 | } 38 | 39 | /// A widget that shows a bar of actions above the keyboard, to help customize input. 40 | /// 41 | /// To use this class, add it somewhere higher up in your widget hierarchy. Then, from any child 42 | /// widgets, add [KeyboardActionsConfig] to configure it with the [KeyboardAction]s you'd 43 | /// like to use. These will be displayed whenever the wrapped focus nodes are selected. 44 | /// 45 | /// This widget wraps a [KeyboardAvoider], which takes over functionality from [Scaffold]: when the 46 | /// focus changes, this class re-sizes [child]'s focused object to still be visible, and scrolls to the 47 | /// focused node. **As such, set [Scaffold.resizeToAvoidBottomInset] to _false_ when using this Widget.** 48 | /// 49 | /// We manage resizing ourselves so that: 50 | /// 51 | /// 1. using scaffold is not required 52 | /// 2. content is only shrunk as needed (a problem with scaffold) 53 | /// 3. we shrink an additional [_kBarSize] so the keyboard action bar doesn't cover content either. 54 | class KeyboardActions extends StatefulWidget { 55 | /// Any content you want to resize/scroll when the keyboard comes up 56 | final Widget? child; 57 | 58 | /// Keyboard configuration 59 | final KeyboardActionsConfig config; 60 | 61 | /// If you want the content to auto-scroll when focused; see [KeyboardAvoider.autoScroll] 62 | final bool autoScroll; 63 | 64 | /// In case you don't want to enable keyboard_action bar (e.g. You are running your app on iPad) 65 | final bool enable; 66 | 67 | /// If you are using keyboard_actions inside a Dialog it must be true 68 | final bool isDialog; 69 | 70 | /// Tap outside the keyboard will dismiss this 71 | @Deprecated('Use tapOutsideBehavior instead.') 72 | final bool tapOutsideToDismiss; 73 | 74 | /// Tap outside behavior 75 | final TapOutsideBehavior tapOutsideBehavior; 76 | 77 | /// If you want to add overscroll. Eg: In some cases you have a [TextField] with an error text below that. 78 | final double overscroll; 79 | 80 | /// If you want to control the scroll physics of [BottomAreaAvoider] which uses a [SingleChildScrollView] to contain the child. 81 | final ScrollPhysics? bottomAvoiderScrollPhysics; 82 | 83 | /// If you are using [KeyboardActions] for just one textfield and don't need to scroll the content set this to `true` 84 | final bool disableScroll; 85 | 86 | /// Does not clear the focus if you tap on the node focused, useful for keeping the text cursor selection working. Usually used with tapOutsideBehavior as translucent 87 | final bool keepFocusOnTappingNode; 88 | 89 | /// Override default height of the bar. `null` is dynamic height 90 | final double? barSize; 91 | 92 | const KeyboardActions( 93 | {this.child, 94 | this.bottomAvoiderScrollPhysics, 95 | this.enable = true, 96 | this.autoScroll = true, 97 | this.isDialog = false, 98 | @Deprecated('Use tapOutsideBehavior instead.') 99 | this.tapOutsideToDismiss = false, 100 | this.tapOutsideBehavior = TapOutsideBehavior.none, 101 | required this.config, 102 | this.overscroll = 12.0, 103 | this.disableScroll = false, 104 | this.keepFocusOnTappingNode = false, 105 | this.barSize = _kBarSize}) 106 | : assert(child != null); 107 | 108 | @override 109 | KeyboardActionstate createState() => KeyboardActionstate(); 110 | } 111 | 112 | /// State class for [KeyboardActions]. 113 | class KeyboardActionstate extends State 114 | with WidgetsBindingObserver { 115 | /// The currently configured keyboard actions 116 | KeyboardActionsConfig? config; 117 | 118 | /// private state 119 | Map _map = Map(); 120 | KeyboardActionsItem? _currentAction; 121 | int? _currentIndex = 0; 122 | OverlayEntry? _overlayEntry; 123 | double _offset = 0; 124 | PreferredSizeWidget? _currentFooter; 125 | bool _dismissAnimationNeeded = true; 126 | final _keyParent = GlobalKey(); 127 | Completer? _dismissAnimation; 128 | 129 | /// If the keyboard bar is on for the current platform 130 | bool get _isAvailable { 131 | return config!.keyboardActionsPlatform == KeyboardActionsPlatform.ALL || 132 | (config!.keyboardActionsPlatform == KeyboardActionsPlatform.IOS && 133 | PlatformCheck.isIOS) || 134 | (config!.keyboardActionsPlatform == KeyboardActionsPlatform.ANDROID && 135 | PlatformCheck.isAndroid); 136 | } 137 | 138 | /// If we are currently showing the keyboard bar 139 | bool get _isShowing { 140 | return _overlayEntry != null; 141 | } 142 | 143 | /// The current previous index, or null. 144 | int? get _previousIndex { 145 | final nextIndex = _currentIndex! - 1; 146 | return nextIndex >= 0 ? nextIndex : null; 147 | } 148 | 149 | /// The current next index, or null. 150 | int? get _nextIndex { 151 | final nextIndex = _currentIndex! + 1; 152 | return nextIndex < _map.length ? nextIndex : null; 153 | } 154 | 155 | /// The distance from the bottom of the KeyboardActions widget to the 156 | /// bottom of the view port. 157 | /// 158 | /// Used to correctly calculate the offset to "avoid" with BottomAreaAvoider. 159 | double get _distanceBelowWidget { 160 | if (_keyParent.currentContext != null) { 161 | final widgetRenderBox = 162 | _keyParent.currentContext!.findRenderObject() as RenderBox; 163 | final fullHeight = MediaQuery.of(context).size.height; 164 | final widgetHeight = widgetRenderBox.size.height; 165 | final widgetTop = widgetRenderBox.localToGlobal(Offset.zero).dy; 166 | final widgetBottom = widgetTop + widgetHeight; 167 | final distanceBelowWidget = fullHeight - widgetBottom; 168 | return distanceBelowWidget; 169 | } 170 | return 0; 171 | } 172 | 173 | /// Set the config for the keyboard action bar. 174 | void setConfig(KeyboardActionsConfig newConfig) { 175 | clearConfig(); 176 | config = newConfig; 177 | for (int i = 0; i < config!.actions!.length; i++) { 178 | _addAction(i, config!.actions![i]); 179 | } 180 | _startListeningFocus(); 181 | } 182 | 183 | /// Clear any existing configuration. Unsubscribe from focus listeners. 184 | void clearConfig() { 185 | _dismissListeningFocus(); 186 | _clearAllFocusNode(); 187 | config = null; 188 | } 189 | 190 | void _addAction(int index, KeyboardActionsItem action) { 191 | _map[index] = action; 192 | } 193 | 194 | void _clearAllFocusNode() { 195 | _map = Map(); 196 | } 197 | 198 | void _clearFocus() { 199 | _currentAction?.focusNode.unfocus(); 200 | } 201 | 202 | Future _focusNodeListener() async { 203 | bool hasFocusFound = false; 204 | _map.keys.forEach((key) { 205 | final currentAction = _map[key]!; 206 | if (currentAction.focusNode.hasFocus) { 207 | hasFocusFound = true; 208 | _currentAction = currentAction; 209 | _currentIndex = key; 210 | return; 211 | } 212 | }); 213 | _focusChanged(hasFocusFound); 214 | } 215 | 216 | void _shouldGoToNextFocus(KeyboardActionsItem action, int? nextIndex) async { 217 | _dismissAnimationNeeded = true; 218 | _currentAction = action; 219 | _currentIndex = nextIndex; 220 | //remove focus for unselected fields 221 | _map.keys.forEach((key) { 222 | final currentAction = _map[key]!; 223 | if (currentAction == _currentAction && 224 | currentAction.footerBuilder != null) { 225 | _dismissAnimationNeeded = false; 226 | } 227 | if (currentAction != _currentAction) { 228 | currentAction.focusNode.unfocus(); 229 | } 230 | }); 231 | //if it is a custom keyboard then wait until the focus was dismissed from the others 232 | if (_currentAction!.footerBuilder != null) { 233 | await Future.delayed( 234 | Duration(milliseconds: _timeToDismiss.inMilliseconds), 235 | ); 236 | } 237 | 238 | FocusScope.of(context).requestFocus(_currentAction!.focusNode); 239 | await Future.delayed(const Duration(milliseconds: 100)); 240 | bottomAreaAvoiderKey.currentState?.scrollToOverscroll(); 241 | } 242 | 243 | void _onTapUp() { 244 | if (_previousIndex != null) { 245 | final currentAction = _map[_previousIndex!]!; 246 | if (currentAction.enabled) { 247 | _shouldGoToNextFocus(currentAction, _previousIndex); 248 | } else { 249 | _currentIndex = _previousIndex; 250 | _onTapUp(); 251 | } 252 | } 253 | } 254 | 255 | void _onTapDown() { 256 | if (_nextIndex != null) { 257 | final currentAction = _map[_nextIndex!]!; 258 | if (currentAction.enabled) { 259 | _shouldGoToNextFocus(currentAction, _nextIndex); 260 | } else { 261 | _currentIndex = _nextIndex; 262 | _onTapDown(); 263 | } 264 | } 265 | } 266 | 267 | /// Shows or hides the keyboard bar as needed, and re-calculates the overlay offset. 268 | /// 269 | /// Called every time the focus changes, and when the app is resumed on Android. 270 | void _focusChanged(bool showBar) async { 271 | if (_isAvailable) { 272 | if (_dismissAnimation != null) { 273 | // wait for the previous animation to complete 274 | await _dismissAnimation?.future; 275 | } 276 | if (showBar && !_isShowing) { 277 | _insertOverlay(); 278 | } else if (!showBar && _isShowing) { 279 | _removeOverlay(); 280 | } else if (showBar && _isShowing) { 281 | if (PlatformCheck.isAndroid) { 282 | _updateOffset(); 283 | } 284 | _overlayEntry!.markNeedsBuild(); 285 | } 286 | if (_currentAction != null && _currentAction!.footerBuilder != null) { 287 | WidgetsBinding.instance.addPostFrameCallback((_) { 288 | _updateOffset(); 289 | }); 290 | } 291 | } 292 | } 293 | 294 | @override 295 | void didChangeMetrics() { 296 | if (PlatformCheck.isAndroid) { 297 | final value = WidgetsBinding.instance.window.viewInsets.bottom; 298 | bool keyboardIsOpen = value > 0; 299 | _onKeyboardChanged(keyboardIsOpen); 300 | isKeyboardOpen = keyboardIsOpen; 301 | } 302 | // Need to wait a frame to get the new size 303 | WidgetsBinding.instance.addPostFrameCallback((_) { 304 | _updateOffset(); 305 | }); 306 | } 307 | 308 | void _startListeningFocus() { 309 | _map.values 310 | .forEach((action) => action.focusNode.addListener(_focusNodeListener)); 311 | } 312 | 313 | void _dismissListeningFocus() { 314 | _map.values.forEach( 315 | (action) => action.focusNode.removeListener(_focusNodeListener)); 316 | } 317 | 318 | bool _inserted = false; 319 | 320 | /// Insert the keyboard bar as an Overlay. 321 | /// 322 | /// This will be inserted above everything else in the MaterialApp, including dialog modals. 323 | /// 324 | /// Position the overlay based on the current [MediaQuery] to land above the keyboard. 325 | void _insertOverlay() { 326 | OverlayState os = Overlay.of(context); 327 | _inserted = true; 328 | _overlayEntry = OverlayEntry(builder: (context) { 329 | // Update and build footer, if any 330 | _currentFooter = (_currentAction!.footerBuilder != null) 331 | ? _currentAction!.footerBuilder!(context) 332 | : null; 333 | 334 | final queryData = MediaQuery.of(context); 335 | return Stack( 336 | children: [ 337 | if (widget.tapOutsideBehavior != TapOutsideBehavior.none || 338 | // ignore: deprecated_member_use_from_same_package 339 | widget.tapOutsideToDismiss) 340 | Positioned.fill( 341 | child: Listener( 342 | onPointerDown: (event) { 343 | if (!widget.keepFocusOnTappingNode || 344 | _currentAction?.focusNode.rect.contains(event.position) != 345 | true) { 346 | _clearFocus(); 347 | } 348 | }, 349 | behavior: widget.tapOutsideBehavior == 350 | TapOutsideBehavior.translucentDismiss 351 | ? HitTestBehavior.translucent 352 | : HitTestBehavior.opaque, 353 | ), 354 | ), 355 | Positioned( 356 | left: 0, 357 | right: 0, 358 | bottom: queryData.viewInsets.bottom, 359 | child: Material( 360 | color: config!.keyboardBarColor ?? Colors.grey[200], 361 | elevation: config!.keyboardBarElevation ?? 20, 362 | child: Column( 363 | mainAxisSize: MainAxisSize.min, 364 | children: [ 365 | if (_currentAction!.displayActionBar) 366 | _buildBar(_currentAction!.displayArrows), 367 | if (_currentFooter != null) 368 | AnimatedContainer( 369 | duration: _timeToDismiss, 370 | child: _currentFooter, 371 | height: 372 | _inserted ? _currentFooter!.preferredSize.height : 0, 373 | ), 374 | ], 375 | ), 376 | ), 377 | ), 378 | ], 379 | ); 380 | }); 381 | os.insert(_overlayEntry!); 382 | } 383 | 384 | /// Remove the overlay bar. Call when losing focus or being dismissed. 385 | void _removeOverlay({bool fromDispose = false}) async { 386 | _inserted = false; 387 | if (_currentFooter != null && _dismissAnimationNeeded) { 388 | if (mounted && !fromDispose) { 389 | _overlayEntry?.markNeedsBuild(); 390 | // add a completer to indicate the completion of dismiss animation. 391 | _dismissAnimation = Completer(); 392 | await Future.delayed(_timeToDismiss); 393 | _dismissAnimation?.complete(); 394 | _dismissAnimation = null; 395 | } 396 | } 397 | _overlayEntry?.remove(); 398 | _overlayEntry = null; 399 | _currentFooter = null; 400 | if (!fromDispose && _dismissAnimationNeeded) _updateOffset(); 401 | _dismissAnimationNeeded = true; 402 | } 403 | 404 | void _updateOffset() { 405 | if (!mounted) { 406 | return; 407 | } 408 | 409 | if (!_isShowing || !_isAvailable) { 410 | setState(() { 411 | _offset = 0.0; 412 | }); 413 | return; 414 | } 415 | 416 | double newOffset = _currentAction!.displayActionBar 417 | ? _kBarSize 418 | : 0; // offset for the actions bar 419 | 420 | final keyboardHeight = EdgeInsets.fromWindowPadding( 421 | WidgetsBinding.instance.window.viewInsets, 422 | WidgetsBinding.instance.window.devicePixelRatio) 423 | .bottom; 424 | 425 | newOffset += keyboardHeight; // + offset for the system keyboard 426 | 427 | if (_currentFooter != null) { 428 | newOffset += 429 | _currentFooter!.preferredSize.height; // + offset for the footer 430 | } 431 | 432 | newOffset -= _localMargin + _distanceBelowWidget; 433 | 434 | if (newOffset < 0) newOffset = 0; 435 | 436 | // Update state if changed 437 | if (_offset != newOffset) { 438 | setState(() { 439 | _offset = newOffset; 440 | }); 441 | } 442 | } 443 | 444 | double _localMargin = 0.0; 445 | 446 | void _onLayout() { 447 | if (widget.isDialog) { 448 | final render = 449 | _keyParent.currentContext?.findRenderObject() as RenderBox?; 450 | final fullHeight = MediaQuery.of(context).size.height; 451 | final localHeight = render?.size.height ?? 0; 452 | _localMargin = (fullHeight - localHeight) / 2; 453 | } 454 | } 455 | 456 | @override 457 | void didChangeAppLifecycleState(AppLifecycleState state) { 458 | if (defaultTargetPlatform == TargetPlatform.android) { 459 | if (state == AppLifecycleState.paused) { 460 | FocusScope.of(context).requestFocus(FocusNode()); 461 | _focusChanged(false); 462 | } 463 | } 464 | super.didChangeAppLifecycleState(state); 465 | } 466 | 467 | @override 468 | void didUpdateWidget(KeyboardActions oldWidget) { 469 | if (widget.enable) setConfig(widget.config); 470 | super.didUpdateWidget(oldWidget); 471 | } 472 | 473 | @override 474 | void dispose() { 475 | clearConfig(); 476 | _removeOverlay(fromDispose: true); 477 | WidgetsBinding.instance.removeObserver(this); 478 | super.dispose(); 479 | } 480 | 481 | @override 482 | void initState() { 483 | WidgetsBinding.instance.addObserver(this); 484 | if (widget.enable) { 485 | setConfig(widget.config); 486 | WidgetsBinding.instance.addPostFrameCallback((_) { 487 | _onLayout(); 488 | _updateOffset(); 489 | }); 490 | } 491 | super.initState(); 492 | } 493 | 494 | var isKeyboardOpen = false; 495 | 496 | void _onKeyboardChanged(bool isVisible) { 497 | bool footerHasSize = _checkIfFooterHasSize(); 498 | if (!isVisible && isKeyboardOpen && !footerHasSize) { 499 | _clearFocus(); 500 | } 501 | } 502 | 503 | bool _checkIfFooterHasSize() { 504 | return _currentFooter != null && 505 | (_currentFooter?.preferredSize.height ?? 0) > 0; 506 | } 507 | 508 | /// Build the keyboard action bar based on the current [config]. 509 | Widget _buildBar(bool displayArrows) { 510 | return AnimatedCrossFade( 511 | duration: _timeToDismiss, 512 | crossFadeState: 513 | _isShowing ? CrossFadeState.showFirst : CrossFadeState.showSecond, 514 | firstChild: Container( 515 | height: widget.barSize, 516 | width: MediaQuery.of(context).size.width, 517 | decoration: BoxDecoration( 518 | border: Border( 519 | top: BorderSide( 520 | color: widget.config.keyboardSeparatorColor, 521 | width: 1.0, 522 | ), 523 | ), 524 | ), 525 | child: SafeArea( 526 | top: false, 527 | bottom: false, 528 | child: Row( 529 | mainAxisAlignment: 530 | _currentAction?.toolbarAlignment ?? MainAxisAlignment.end, 531 | children: [ 532 | if (config!.nextFocus && displayArrows) ...[ 533 | IconButton( 534 | icon: Icon(Icons.keyboard_arrow_up), 535 | tooltip: 'Previous', 536 | iconSize: IconTheme.of(context).size!, 537 | color: IconTheme.of(context).color, 538 | disabledColor: Theme.of(context).disabledColor, 539 | onPressed: _previousIndex != null ? _onTapUp : null, 540 | ), 541 | IconButton( 542 | icon: Icon(Icons.keyboard_arrow_down), 543 | tooltip: 'Next', 544 | iconSize: IconTheme.of(context).size!, 545 | color: IconTheme.of(context).color, 546 | disabledColor: Theme.of(context).disabledColor, 547 | onPressed: _nextIndex != null ? _onTapDown : null, 548 | ), 549 | const Spacer(), 550 | ], 551 | if (_currentAction?.displayDoneButton != null && 552 | _currentAction!.displayDoneButton && 553 | (_currentAction!.toolbarButtons == null || 554 | _currentAction!.toolbarButtons!.isEmpty)) 555 | Padding( 556 | padding: const EdgeInsets.all(5.0), 557 | child: InkWell( 558 | onTap: () { 559 | if (_currentAction?.onTapAction != null) { 560 | _currentAction!.onTapAction!(); 561 | } 562 | _clearFocus(); 563 | }, 564 | child: Container( 565 | padding: 566 | EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0), 567 | child: config?.defaultDoneWidget ?? 568 | Text( 569 | "Done", 570 | style: TextStyle( 571 | fontSize: 16.0, 572 | fontWeight: FontWeight.w500, 573 | ), 574 | ), 575 | ), 576 | ), 577 | ), 578 | if (_currentAction?.toolbarButtons != null) 579 | ..._currentAction!.toolbarButtons! 580 | .map((item) => item(_currentAction!.focusNode)) 581 | .toList() 582 | ], 583 | ), 584 | ), 585 | ), 586 | secondChild: const SizedBox.shrink(), 587 | ); 588 | } 589 | 590 | final GlobalKey bottomAreaAvoiderKey = 591 | GlobalKey(); 592 | 593 | @override 594 | Widget build(BuildContext context) { 595 | // Return the given child wrapped in a [KeyboardAvoider]. 596 | // We will call [_buildBar] and insert it via overlay on demand. 597 | // Add [_kBarSize] padding to ensure we scroll past the action bar. 598 | 599 | // We need to add this sized box to support embedding in IntrinsicWidth 600 | // areas, like AlertDialog. This is because of the LayoutBuilder KeyboardAvoider uses 601 | // if it has no child ScrollView. 602 | // If we don't, we get "LayoutBuilder does not support returning intrinsic dimensions". 603 | // See https://github.com/flutter/flutter/issues/18108. 604 | // The SizedBox can be removed when thats fixed. 605 | return widget.enable && !widget.disableScroll 606 | ? Material( 607 | color: Colors.transparent, 608 | child: SizedBox( 609 | width: double.maxFinite, 610 | key: _keyParent, 611 | child: BottomAreaAvoider( 612 | key: bottomAreaAvoiderKey, 613 | areaToAvoid: _offset, 614 | overscroll: widget.overscroll, 615 | duration: Duration( 616 | milliseconds: 617 | (_timeToDismiss.inMilliseconds * 1.8).toInt()), 618 | autoScroll: widget.autoScroll, 619 | physics: widget.bottomAvoiderScrollPhysics, 620 | child: widget.child, 621 | ), 622 | ), 623 | ) 624 | : widget.child!; 625 | } 626 | } 627 | -------------------------------------------------------------------------------- /lib/keyboard_actions_config.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'keyboard_actions.dart'; 3 | 4 | /// Wrapper for a single configuration of the keyboard actions bar. 5 | class KeyboardActionsConfig { 6 | /// Keyboard Action for specific platform 7 | /// KeyboardActionsPlatform : ANDROID , IOS , ALL 8 | final KeyboardActionsPlatform keyboardActionsPlatform; 9 | 10 | /// true to display arrows prev/next to move focus between inputs 11 | final bool nextFocus; 12 | 13 | /// [KeyboardActionsItem] for each input 14 | final List? actions; 15 | 16 | /// Color of the background to the Custom keyboard buttons 17 | final Color? keyboardBarColor; 18 | 19 | /// Elevation of the Custom keyboard buttons 20 | final double? keyboardBarElevation; 21 | 22 | /// Color of the line separator between keyboard and content 23 | final Color keyboardSeparatorColor; 24 | 25 | /// A [Widget] to be optionally used instead of the "Done" button 26 | /// which dismisses the keyboard. 27 | final Widget? defaultDoneWidget; 28 | 29 | const KeyboardActionsConfig({ 30 | this.keyboardActionsPlatform = KeyboardActionsPlatform.ALL, 31 | this.nextFocus = true, 32 | this.actions, 33 | this.keyboardBarColor, 34 | this.keyboardBarElevation, 35 | this.keyboardSeparatorColor = Colors.transparent, 36 | this.defaultDoneWidget, 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /lib/keyboard_actions_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | typedef ButtonBuilder = Widget Function(FocusNode focusNode); 4 | 5 | ///Class to define the `focusNode` that you pass to your `TextField` too and other params to customize 6 | ///the bar that will appear over your keyboard 7 | class KeyboardActionsItem { 8 | /// The Focus object coupled to TextField, listening for got/lost focus events 9 | final FocusNode focusNode; 10 | 11 | /// Optional widgets to display to the right of the bar/ 12 | /// NOTE: `toolbarButtons` override the Done button by default 13 | final List? toolbarButtons; 14 | 15 | /// true [default] to display the Done button 16 | final bool displayDoneButton; 17 | 18 | /// Optional callback if the Done button for TextField was tapped 19 | /// It will only work if `displayDoneButton` is [true] and `toolbarButtons` is null or empty 20 | final VoidCallback? onTapAction; 21 | 22 | /// true [default] to display the arrows to move between the fields 23 | final bool displayArrows; 24 | 25 | /// true [default] if the TextField is enabled 26 | final bool enabled; 27 | 28 | /// true [default] to display the action bar 29 | final bool displayActionBar; 30 | 31 | /// Builder for an optional widget to show below the action bar. 32 | /// 33 | /// Consider using for field validation or as a replacement for a system keyboard. 34 | /// 35 | /// This widget must be a PreferredSizeWidget to report its exact height; use [Size.fromHeight] 36 | final PreferredSizeWidget Function(BuildContext context)? footerBuilder; 37 | 38 | /// Alignment of the row that displays [toolbarButtons]. If you want to show your 39 | /// buttons from the left side of the toolbar, you can set [toolbarAlignment] and 40 | /// set the value of [displayArrows] to `false` 41 | final MainAxisAlignment toolbarAlignment; 42 | 43 | const KeyboardActionsItem({ 44 | required this.focusNode, 45 | this.onTapAction, 46 | this.toolbarButtons, 47 | this.enabled = true, 48 | this.displayActionBar = true, 49 | this.displayArrows = true, 50 | this.displayDoneButton = true, 51 | this.footerBuilder, 52 | this.toolbarAlignment = MainAxisAlignment.end, 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /lib/keyboard_custom.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// Signature for a function that creates a widget for a given value 4 | typedef WidgetKeyboardBuilder = Widget Function( 5 | BuildContext context, T value, bool? hasFocus); 6 | 7 | /// A widget that allow us to create a custom keyboard instead of the platform keyboard. 8 | class KeyboardCustomInput extends StatefulWidget { 9 | ///Create your own widget and receive the [T] value 10 | final WidgetKeyboardBuilder builder; 11 | 12 | ///Set the same `focusNode` you add to the [KeyboardAction] 13 | final FocusNode focusNode; 14 | 15 | ///The height of your widget 16 | final double? height; 17 | 18 | ///Set the same `notifier` you add to the [KeyboardAction] 19 | final ValueNotifier notifier; 20 | 21 | const KeyboardCustomInput({ 22 | Key? key, 23 | required this.focusNode, 24 | required this.builder, 25 | required this.notifier, 26 | this.height, 27 | }) : super(key: key); 28 | 29 | @override 30 | _KeyboardCustomInputState createState() => _KeyboardCustomInputState(); 31 | } 32 | 33 | class _KeyboardCustomInputState extends State> 34 | with AutomaticKeepAliveClientMixin { 35 | bool? _hasFocus; 36 | 37 | @override 38 | void initState() { 39 | super.initState(); 40 | _hasFocus = widget.focusNode.hasFocus; 41 | } 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | super.build(context); 46 | return Focus( 47 | focusNode: widget.focusNode, 48 | child: GestureDetector( 49 | onTap: () { 50 | if (!widget.focusNode.hasFocus) { 51 | widget.focusNode.requestFocus(); 52 | } 53 | }, 54 | child: Container( 55 | height: widget.height, 56 | width: double.maxFinite, 57 | child: InputDecorator( 58 | decoration: InputDecoration( 59 | border: InputBorder.none, 60 | filled: false, 61 | disabledBorder: InputBorder.none, 62 | focusedBorder: InputBorder.none, 63 | errorBorder: InputBorder.none, 64 | enabled: false, 65 | ), 66 | isFocused: _hasFocus!, 67 | child: MergeSemantics( 68 | child: Semantics( 69 | focused: _hasFocus, 70 | child: Container( 71 | child: AnimatedBuilder( 72 | animation: widget.notifier, 73 | builder: (context, child) => widget.builder( 74 | context, widget.notifier.value, _hasFocus), 75 | ), 76 | ), 77 | ), 78 | ), 79 | ), 80 | ), 81 | ), 82 | onFocusChange: (newValue) => setState(() { 83 | _hasFocus = newValue; 84 | }), 85 | ); 86 | } 87 | 88 | @override 89 | bool get wantKeepAlive => true; 90 | } 91 | 92 | /// A mixin which help to update the notifier, you must mix this class in case you want to create your own keyboard 93 | mixin KeyboardCustomPanelMixin { 94 | ///We'll use this notifier to send the data and refresh the widget inside [KeyboardCustomInput] 95 | ValueNotifier get notifier; 96 | 97 | ///This method will update the notifier 98 | void updateValue(T value) { 99 | notifier.value = value; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: keyboard_actions 2 | description: Now you can add features to the Android / iOS keyboard in a very simple way. 3 | version: 4.2.0 4 | homepage: https://github.com/diegoveloper/ 5 | repository: https://github.com/diegoveloper/flutter_keyboard_actions/ 6 | 7 | environment: 8 | sdk: '>=2.17.0 <3.0.0' 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | flutter: --------------------------------------------------------------------------------