├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README_CN.md ├── lib └── fsearch.dart ├── publish.sh ├── pubspec.yaml └── test └── fsearch_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | 7 | build/ 8 | 9 | .idea/ 10 | 11 | .metadata 12 | 13 | *.iml 14 | 15 | pubspec.lock 16 | 17 | 18 | android/ 19 | ios/ 20 | macos/ 21 | 22 | readme.py 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.0 2 | - Support null-safety 3 | 4 | ## 1.0.1 5 | 6 | - Adaptation the text in the input box is not centered in versions below 1.19 7 | 8 | ## 1.0.0 9 | 10 | - the first version 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020-present Fliggy Android Team . 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at following link. 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 |

fsearch

8 | 9 | 10 |
11 | 12 |

Help developers build the most beautiful search bar🍹.

13 | 14 |

[FSearch] provides developers with a one-stop search bar construction service. Supports borders, corners, gradient background colors and shadows, as well as any number of prefix and suffix action buttons. Provides beautiful Hint animation.

15 | 16 |

Author:Newton(coorchice.cb@alibaba-inc.com)

17 | 18 |

19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |

47 |

48 | 49 |

50 | 51 | |||| 52 | |:--:|:--:|:--:| 53 | |![](https://gw.alicdn.com/tfs/TB1QSOkJYr1gK0jSZR0XXbP8XXa-320-469.gif)|![](https://gw.alicdn.com/tfs/TB1fWytJYj1gK0jSZFuXXcrHpXa-320-469.gif)|![](https://gw.alicdn.com/tfs/TB1dh1sJ7T2gK0jSZFkXXcIQFXa-320-469.gif)| 54 | |![](https://gw.alicdn.com/tfs/TB1XrOdJ1L2gK0jSZPhXXahvXXa-320-464.gif)|![](https://gw.alicdn.com/tfs/TB1x4CmJVP7gK0jSZFjXXc5aXXa-320-464.gif)|![](https://gw.alicdn.com/tfs/TB1iFAMXI4z2K4jSZKPXXXAYpXa-360-466.gif)| 55 | |![](https://gw.alicdn.com/tfs/TB1Pt1oJYY1gK0jSZTEXXXDQVXa-360-298.gif)|![](https://gw.alicdn.com/tfs/TB19oLDGKT2gK0jSZFvXXXnFXXa-360-212.gif)|![](https://gw.alicdn.com/tfs/TB19oLDGKT2gK0jSZFvXXXnFXXa-360-212.gif)| 56 | 57 | **English | [简体中文](https://github.com/Fliggy-Mobile/fsearch/blob/master/README_CN.md)** 58 | 59 | > Like it? Please cast your **Star** 🥰 ! 60 | 61 | # ✨ Features 62 | 63 | - Support beautiful border effect 64 | 65 | - Provide rich corner configuration 66 | 67 | - Support stunning gradient effects 68 | 69 | - Provides easy to use shadow capabilities 70 | 71 | - Support any number of prefix and suffix action buttons 72 | 73 | - Provide colorful, flexible and powerful Hint effects 74 | 75 | - Controllers that are easier to use for developers 76 | 77 | 78 | # 🛠 Guide 79 | 80 | 81 | ## ⚙️ Parameter & Interface 82 | 83 | ### 🔩 FSearch Param 84 | 85 | |Param|Type|Necessary|Default|desc| 86 | |---|---|:---:|---|---| 87 | |controller|FSearchController|false|null|Controller. See [FSearchController] for details| 88 | |width|double|false|null|width| 89 | |height|double|false|null|height| 90 | |enable|bool|false|true|enable| 91 | |onTap|VoidCallback|false|null|Callback when the input box is clicked| 92 | |text|String|false|null|Input content| 93 | |onSearch|ValueChanged|false|null|Callback when the keyboard search button is clicked| 94 | |corner|FSearchCorner|false|null|Corner effect. See [FSearchCorner] for details| 95 | |cornerStyle|FSearchCornerStyle|false|null|Corner style. The default [FSearchCornerStyle.round]. See [FSearchCornerStyle] for details| 96 | |strokeColor|Color|false|null|Border color| 97 | |strokeWidth|double|false|null|border width| 98 | |backgroundColor|Color|false|null|background color| 99 | |gradient|Gradient|false|null|Background gradient. Will overwrite [backgroundColor]| 100 | |shadowColor|Color|false|null|Set widget shadow color| 101 | |shadowOffset|Offset|false|null|Set widget shadow color| 102 | |shadowBlur|double|false|null|Set the standard deviation of the widget Gaussian and shadow shape convolution| 103 | |cursorColor|Color|false|null|Cursor color| 104 | |cursorWidth|double|false|null|Cursor width| 105 | |cursorRadius|double|false|null|Cursor corner size| 106 | |prefixes|List|false|null|Prefix action widget| 107 | |suffixes|List|false|null|Suffix action widget| 108 | |padding|EdgeInsets|false|null|The distance between the actual input area and the edge of [FSearch]| 109 | |margin|EdgeInsets|false|null|[FSearch] outer spacing| 110 | |style|TextStyle|false|null|Input text style| 111 | |hintStyle|TextStyle|false|null|Hint text style| 112 | |hints|List|false|null|Hint. If there is only one Hint, Hint swap animation cannot be enabled.| 113 | |hintSwitchDuration|Duration|false|null|Hint exchange interval| 114 | |hintSwitchAnimDuration|Duration|false|null|Hint swap animation time| 115 | |hintSwitchEnable|bool|false|null|Whether to enable Hint swap animation| 116 | |hintSwitchType|FSearchAnimationType|false|null|Hint exchanges animation types. The default [FSearchAnimationType.Scroll]. See [FSearchAnimationType] for details.| 117 | |stopHintSwitchOnFocus|bool|true|null|When the focus is obtained, whether to automatically stop the Hint exchange animation.| 118 | |hintPrefix|Widget|false|null|Hint prefix widget| 119 | |center|bool|false|null|Centered。| 120 | 121 | ### 💻 FSearchController 122 | 123 | **FSearchController** is the controller of **FSearch**, which can return to the input text, `Hint`, focus status and other information. At the same time provide a variety of monitoring and text update capabilities. 124 | 125 | 126 | #### 🔩 Param 127 | 128 | |Param|Type|Desc| 129 | |---|---|---| 130 | |text|String|Input text| 131 | |hint|String|Current Hint text| 132 | |focus|bool|Focus state| 133 | 134 | #### 📲 Interface 135 | 136 | - `setListener(VoidCallback listener)` 137 | 138 | Set input monitor 139 | 140 | 141 | - `setOnFocusChangedListener(ValueChanged listener)` 142 | 143 | Set focus change monitoring 144 | 145 | 146 | - `requestFocus()` 147 | 148 | Request focus 149 | 150 | - `clearFocus()` 151 | 152 | Remove focus 153 | 154 | ### 🎥 FSearchAnimationType 155 | 156 | **FSearchAnimationType** Used to specify the FSearch Hint exchange animation type. 157 | 158 | ```dart 159 | enum FSearchAnimationType { 160 | /// 渐变动画 161 | /// 162 | /// Alpha animation 163 | Fade, 164 | 165 | /// 缩放动画 166 | /// 167 | /// Scale animation 168 | Scale, 169 | 170 | /// 上下滚动动画 171 | /// 172 | /// Scroll up and down animation 173 | Scroll, 174 | } 175 | ``` 176 | 177 | ## 📺 Demo 178 | 179 | ### 🔩 Base Demo 180 | 181 | ![](https://gw.alicdn.com/tfs/TB1QSOkJYr1gK0jSZR0XXbP8XXa-320-469.gif) 182 | 183 | ```dart 184 | FSearch( 185 | 186 | /// 设置高 187 | /// 188 | /// Set height 189 | height: 30.0, 190 | 191 | /// 设置背景颜色 192 | /// 193 | /// Set background color 194 | backgroundColor: color, 195 | 196 | /// 设置输入内容样式 197 | /// 198 | /// Set input text style 199 | style: style, 200 | 201 | /// 点击键盘搜索时触发 202 | /// 203 | /// Fired when you click on the keyboard to search 204 | onSearch: (value) { 205 | /// do something 206 | }, 207 | 208 | prefixes: [buildAction()], 209 | ) 210 | ``` 211 | 212 | Using **FSearch** to build a search bar is very relaxing. 213 | 214 | Through some simple parameters, developers can easily change the size, color, and font of the search bar. 215 | 216 | When the user clicks on the keyboard to search for **Action**, onSearch will be triggered, allowing the developer to perform some search operations here. 217 | 218 | 219 | ### 🌖 Prefixes & Suffixes 220 | 221 | ![](https://gw.alicdn.com/tfs/TB1fWytJYj1gK0jSZFuXXcrHpXa-320-469.gif) 222 | 223 | ```dart 224 | FSearch( 225 | height: 30.0, 226 | backgroundColor: mainBackgroundColor, 227 | style: style, 228 | 229 | /// 前缀 Widget 230 | /// 231 | /// prefix widget 232 | prefixes: [ buildAction() ], 233 | 234 | /// 后缀 Widget 235 | /// 236 | /// suffix widget 237 | suffixes: [ 238 | buildAction_1(), 239 | buildAction_2(), 240 | buildAction_3(), 241 | ], 242 | onSearch: _onSearch, 243 | ) 244 | ``` 245 | 246 | In FSearch, developers can configure any number of prefix or suffix action buttons for the search bar through the `prefixes` and `suffixes` parameters. 247 | 248 | 249 | ### 🌈 Gradient 250 | 251 | ![](https://gw.alicdn.com/tfs/TB1dh1sJ7T2gK0jSZFkXXcIQFXa-320-469.gif) 252 | 253 | ```dart 254 | FSearch( 255 | height: 30.0, 256 | backgroundColor: mainBackgroundColor, 257 | style: style, 258 | 259 | /// 配置渐变色 260 | /// 261 | /// Set gradient 262 | gradient: _gradient, 263 | prefixes: [ buildAction() ], 264 | ) 265 | ``` 266 | 267 | **FSearch** can support developers to create a beautiful gradient search bar. 268 | 269 | Only need to configure through the `gradient` parameter. 270 | 271 | ### 🍄 Corner & Stroke & Shadow 272 | 273 | ![](https://gw.alicdn.com/tfs/TB1XrOdJ1L2gK0jSZPhXXahvXXa-320-464.gif) 274 | 275 | ```dart 276 | 277 | /// #1 278 | FSearch( 279 | height: 30.0, 280 | backgroundColor: color, 281 | style: style, 282 | 283 | /// 边角 284 | /// 285 | /// Corner 286 | corner: FSearchCorner( 287 | leftTopCorner: 15.0, 288 | leftBottomCorner: 15.0, 289 | rightBottomCorner: 15.0), 290 | 291 | /// 边框宽 292 | /// 293 | /// border width 294 | strokeWidth: 1.0, 295 | 296 | /// 边框颜色 297 | /// 298 | /// border color 299 | strokeColor: mainTextTitleColor, 300 | 301 | /// 阴影 302 | /// 303 | /// shadow 304 | shadowColor: Colors.black38, 305 | shadowBlur: 5.0, 306 | shadowOffset: Offset(2.0, 2.0), 307 | prefixes: [buildAction()], 308 | ) 309 | ``` 310 | 311 | The **Border** and **Shadow** effects of **FSearch** are the same as those of other **FWidget** members, and are simple and easy to use. 312 | 313 | Through the `corner` parameter, developers can use **FSearchCorner** to freely control the table corner size of **FSearch**. 314 | 315 | ```dart 316 | /// #2 317 | FSearch( 318 | height: 30.0, 319 | backgroundColor: color, 320 | style: style, 321 | 322 | /// 边角 323 | /// 324 | /// Corner 325 | corner: FSearchCorner.all(6.0), 326 | 327 | /// 边角风格 328 | /// 329 | /// Corner style 330 | cornerStyle: FSearchCornerStyle.bevel, 331 | prefixes: [buildAction()], 332 | ) 333 | ``` 334 | 335 | If combined with `cornerStyle`, more complex and exquisite effects can be achieved. 336 | 337 | 338 | ### 📍 Cursor 339 | 340 | ![](https://gw.alicdn.com/tfs/TB1x4CmJVP7gK0jSZFjXXc5aXXa-320-464.gif) 341 | 342 | ```dart 343 | FSearch( 344 | /// 光标配置 345 | /// 346 | /// Cursor 347 | cursorColor: Colors.red[200], 348 | cursorRadius: 5.0, 349 | cursorWidth: 5.0, 350 | 351 | height: 36.0, 352 | style: style, 353 | gradient: _gradient, 354 | corner: _corner, 355 | prefixes: [ buildAction() ], 356 | suffixes: [ buildAction() ], 357 | ) 358 | ``` 359 | 360 | **FSearch** supports modifying the cursor in the input box of the search bar. You can change it to whatever you want. 361 | 362 | 363 | 364 | ### 🗂 Hint 365 | 366 | ![](https://gw.alicdn.com/tfs/TB1iFAMXI4z2K4jSZKPXXXAYpXa-360-466.gif) 367 | 368 | 369 | ```dart 370 | /// #1 371 | FSearch( 372 | height: 36.0, 373 | style: style, 374 | color: _color, 375 | corner: _corner, 376 | prefixes: [ buildAction() ], 377 | suffixes: [ buildAction() ], 378 | 379 | /// Hints 380 | hints: [ 381 | "FSuper is awesome 👍", 382 | "Come to use FButton", 383 | "You will love FSearch", 384 | ], 385 | 386 | /// 开启 hint 交换动画 387 | /// 388 | /// Turn on hint exchange animation 389 | hintSwitchEnable: true, 390 | 391 | /// 配置 hint 交换动画类型 392 | /// 393 | /// Configure hint exchange animation type 394 | hintSwitchType: FSearchAnimationType.Fade, 395 | ) 396 | ``` 397 | 398 | **FSearch** provides developers with a very powerful **Hint** effect. 399 | 400 | Developers can easily set multiple **Hint** for **FSearch**, and can configure multiple **Hint** swap animations by configuring `hintSwitchEnable: true`. 401 | 402 | When the user starts typing, Hint will be automatically hidden, and the swap animation will also be stopped. 403 | 404 | When the content of the search input box becomes empty again, Hint will appear again, and the exchange animation will start playing. 405 | 406 | ```dart 407 | /// #2 408 | FSearch( 409 | height: 36.0, 410 | style: style, 411 | color: _color, 412 | corner: _corner, 413 | prefixes: [ buildAction() ], 414 | suffixes: [ buildAction1(), buildAction2()], 415 | hints: [ 416 | "Do you want to try FFloat?😃", 417 | "FRadio can do more 😱 !", 418 | "I heard that you have been waiting for FDottedLine for a long time...", 419 | ], 420 | hintSwitchEnable: true, 421 | ) 422 | ``` 423 | 424 | 425 | **FSearch** has prepared rich exchange animations for developers. By default, **FSearch** will use the most common scroll swap animation, which is `FSearchAnimationType.Scroll`. 426 | 427 | Of course, developers can configure their favorite animation types through the `hintSwitchType` parameter. 428 | 429 | > 💡 Note that when `hints.length == 1`, the Hint swap animation will not be played. Only an ordinary Hint will be displayed at this time. 430 | 431 | 432 | ```dart 433 | /// #3 434 | FSearch( 435 | height: 36.0, 436 | style: style, 437 | color: _color, 438 | corner: _corner, 439 | prefixes: [ buildAction() ], 440 | suffixes: [ buildAction() ], 441 | 442 | /// Hints 443 | hints: [ 444 | "Embrace FWidget 👬", 445 | "We care about your app 🥰", 446 | "Want to build beautiful apps 🤨 ?", 447 | ], 448 | hintSwitchEnable: true, 449 | 450 | /// 配置 hint 交换动画类型 451 | /// 452 | /// Configure hint exchange animation type 453 | hintSwitchType: FSearchAnimationType.Scale, 454 | 455 | /// 获得焦点时是否停止交换动画 456 | /// 457 | /// Whether to stop exchanging animation when focus is obtained 458 | stopHintSwitchOnFocus: false, 459 | ) 460 | ``` 461 | 462 | By default, when **FSearch** gains input focus, **FSearch** will automatically pause **Hint** to swap animations; when it gets focus again, it will resume automatically. 463 | 464 | By configuring `stopHintSwitchOnFocus: false`, you can continue to play **Hint** swap animation until the user starts typing when **FSearch** has the focus. 465 | 466 | 467 | 468 | ### 💻 Controller 469 | 470 | ![](https://gw.alicdn.com/tfs/TB1Pt1oJYY1gK0jSZTEXXXDQVXa-360-298.gif) 471 | 472 | ```dart 473 | FSearch( 474 | controller: _controller, 475 | height: 36.0, 476 | style: style, 477 | gradient: _gradient, 478 | corner: _corner, 479 | prefixes: [ buildAction() ], 480 | suffixes: [ buildAction() ], 481 | hints: [ 482 | "Want more beautiful widgets 🤨 ?", 483 | "We will launch the official website of FWidget", 484 | "Will you expect it?", 485 | ], 486 | hintStyle: hintStyle, 487 | hintSwitchEnable: true, 488 | ) 489 | 490 | /// 获取输入框内容 491 | /// 492 | /// Get the input box content 493 | String input = controller.text; 494 | 495 | /// 清空输入框内容 496 | /// 497 | /// Clear the contents of the input box 498 | controller.text = null; 499 | 500 | /// 获取当前 hint,如果有的话 501 | /// 502 | /// Get the current hint, if any 503 | String hint = controller.hint; 504 | 505 | /// 移除焦点 506 | /// 507 | /// Remove focus 508 | controller.clearFocus(); 509 | 510 | 511 | /// 获取焦点 512 | /// 513 | /// Request focus 514 | controller.requestFocus(); 515 | ``` 516 | 517 | **FSearch** provides developers with simple, easy-to-use, certain controllers, through which developers can modify or obtain the contents of the search bar at any location. 518 | 519 | # 😃 How to use? 520 | 521 | Add dependencies in the project `pubspec.yaml` file: 522 | 523 | ## 🌐 pub dependency 524 | 525 | ``` 526 | dependencies: 527 | fsearch: ^ 528 | ``` 529 | 530 | > ⚠️ Attention,please go to [**pub**] (https://pub.dev/packages/fsearch) to get the latest version number of **FSearch** 531 | 532 | ## 🖥 Git dependency 533 | 534 | ``` 535 | dependencies: 536 | fsearch: 537 | git: 538 | url: 'git@github.com:Fliggy-Mobile/fsearch.git' 539 | ref: '' 540 | ``` 541 | 542 | > ⚠️ Attention,please refer to [**FSearch**] (https://github.com/Fliggy-Mobile/fsearch) official project for branch number or tag. 543 | 544 | 545 | # 💡 License 546 | 547 | ``` 548 | Copyright 2020-present Fliggy Android Team . 549 | 550 | Licensed under the Apache License, Version 2.0 (the "License"); 551 | you may not use this file except in compliance with the License. 552 | You may obtain a copy of the License at following link. 553 | 554 | http://www.apache.org/licenses/LICENSE-2.0 555 | 556 | Unless required by applicable law or agreed to in writing, software 557 | distributed under the License is distributed on an "AS IS" BASIS, 558 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 559 | See the License for the specific language governing permissions and 560 | limitations under the License. 561 | 562 | ``` 563 | 564 | 565 | ### Like it? Please cast your [**Star**](https://github.com/Fliggy-Mobile/fsearch) 🥰 ! 566 | 567 | 568 | --- 569 | 570 | # How to run Demo project? 571 | 572 | 1. **clone** project to local 573 | 574 | 2. Enter the project `example` directory and run the following command 575 | 576 | ``` 577 | flutter create . 578 | ``` 579 | 580 | 3. Run the demo in `example` 581 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 |

fsearch

8 | 9 | 10 |
11 | 12 |

帮助开发者构建最美的搜索栏🍹。

13 | 14 |

[FSearch] 为开发者提供了一站式的搜索栏构建服务。支持边框、边角、渐变背景色以及阴影,同时支持任意数量的前缀、后缀动作按钮。提供了精美的 Hint 动画。

15 | 16 |

主理人:纽特(coorchice.cb@alibaba-inc.com)

17 | 18 |

19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |

47 |

48 | 49 |

50 | 51 | |||| 52 | |:--:|:--:|:--:| 53 | |![](https://gw.alicdn.com/tfs/TB1QSOkJYr1gK0jSZR0XXbP8XXa-320-469.gif)|![](https://gw.alicdn.com/tfs/TB1fWytJYj1gK0jSZFuXXcrHpXa-320-469.gif)|![](https://gw.alicdn.com/tfs/TB1dh1sJ7T2gK0jSZFkXXcIQFXa-320-469.gif)| 54 | |![](https://gw.alicdn.com/tfs/TB1XrOdJ1L2gK0jSZPhXXahvXXa-320-464.gif)|![](https://gw.alicdn.com/tfs/TB1x4CmJVP7gK0jSZFjXXc5aXXa-320-464.gif)|![](https://gw.alicdn.com/tfs/TB1iFAMXI4z2K4jSZKPXXXAYpXa-360-466.gif)| 55 | |![](https://gw.alicdn.com/tfs/TB1Pt1oJYY1gK0jSZTEXXXDQVXa-360-298.gif)|![](https://gw.alicdn.com/tfs/TB19oLDGKT2gK0jSZFvXXXnFXXa-360-212.gif)|![](https://gw.alicdn.com/tfs/TB19oLDGKT2gK0jSZFvXXXnFXXa-360-212.gif)| 56 | 57 | **[English](https://github.com/Fliggy-Mobile/fsearch) | 简体中文** 58 | 59 | > 感觉还不错?请投出您的 **Star** 吧 🥰 ! 60 | 61 | # ✨ 特性 62 | 63 | - 支持精美的边框效果 64 | 65 | - 提供丰富的边角配置 66 | 67 | - 支持惊艳的渐变效果 68 | 69 | - 提供简单易用的阴影能力 70 | 71 | - 支持任意数量的前缀、后缀动作按钮 72 | 73 | - 提供丰富多彩的、灵活强大 Hint 效果 74 | 75 | - 给开发者更易用的控制器 76 | 77 | # 🛠 使用指南 78 | 79 | 80 | ## ⚙️ 参数 & 接口 81 | 82 | ### 🔩 FSearch 参数 83 | 84 | |参数|类型|必要|默认值|说明| 85 | |---|---|:---:|---|---| 86 | |controller|FSearchController|false|null|控制器。详见 [FSearchController]| 87 | |width|double|false|null|宽| 88 | |height|double|false|null|高| 89 | |enable|bool|false|true|是否可用| 90 | |onTap|VoidCallback|false|null|当输入框被点击时会回调| 91 | |text|String|false|null|输入内容| 92 | |onSearch|ValueChanged|false|null|当点击键盘搜索按钮时会回调| 93 | |corner|FSearchCorner|false|null|边角效果。详见 [FSearchCorner]| 94 | |cornerStyle|FSearchCornerStyle|false|null|边角风格。默认 [FSearchCornerStyle.round]。详见 [FSearchCornerStyle]| 95 | |strokeColor|Color|false|null|边框颜色| 96 | |strokeWidth|double|false|null|边框宽| 97 | |backgroundColor|Color|false|null|背景颜色| 98 | |gradient|Gradient|false|null|背景渐变色。会覆盖 [backgroundColor]| 99 | |shadowColor|Color|false|null|设置组件阴影颜色| 100 | |shadowOffset|Offset|false|null|设置组件阴影偏移| 101 | |shadowBlur|double|false|null|设置组件高斯与阴影形状卷积的标准偏差| 102 | |cursorColor|Color|false|null|光标颜色| 103 | |cursorWidth|double|false|null|光标宽| 104 | |cursorRadius|double|false|null|光标边角大小| 105 | |prefixes|List|false|null|前缀动作按钮| 106 | |suffixes|List|false|null|后缀动作按钮| 107 | |padding|EdgeInsets|false|null|实际输入区域与 [FSearch] 边缘的间距| 108 | |margin|EdgeInsets|false|null|[FSearch] 的外间距| 109 | |style|TextStyle|false|null|输入文本风格| 110 | |hintStyle|TextStyle|false|null|Hint 文本风格| 111 | |hints|List|false|null|Hint。如果只有一条 Hint,将无法启用 Hint 交换动画。| 112 | |hintSwitchDuration|Duration|false|null|Hint 交换时间间隔| 113 | |hintSwitchAnimDuration|Duration|false|null|Hint 交换动画时间| 114 | |hintSwitchEnable|bool|false|null|是否启用 Hint 交换动画| 115 | |hintSwitchType|FSearchAnimationType|false|null|Hint 交换动画类型。默认 [FSearchAnimationType.Scroll]。详见 [FSearchAnimationType]。| 116 | |stopHintSwitchOnFocus|bool|true|null|当获得焦点时,是否自动停止 Hint 交换动画。| 117 | |hintPrefix|Widget|false|null|Hint 前缀小部件| 118 | |center|bool|false|null|是否居中。| 119 | 120 | ### 💻 FSearchController 121 | 122 | **FSearchController** 是 **FSearch** 的控制器,能够回去到输入的文本、 `Hint`、焦点状态等信息。同时提供各种监听和文本更新能力。 123 | 124 | 125 | #### 🔩 参数 126 | 127 | |参数|类型|说明| 128 | |---|---|---| 129 | |text|String|输入的文本内容| 130 | |hint|String|当前 Hint 内容| 131 | |focus|bool|焦点状态| 132 | 133 | #### 📲 接口 134 | 135 | - `setListener(VoidCallback listener)` 136 | 137 | 设置输入监听 138 | 139 | 140 | - `setOnFocusChangedListener(ValueChanged listener)` 141 | 142 | 设置焦点变化监听 143 | 144 | 145 | - `requestFocus()` 146 | 147 | 请求获得焦点 148 | 149 | - `clearFocus()` 150 | 151 | 移除焦点 152 | 153 | ### 🎥 FSearchAnimationType 154 | 155 | **FSearchAnimationType** 用于指定 FSearch 的 Hint 交换动画类型。 156 | 157 | ```dart 158 | enum FSearchAnimationType { 159 | /// 渐变动画 160 | /// 161 | /// Alpha animation 162 | Fade, 163 | 164 | /// 缩放动画 165 | /// 166 | /// Scale animation 167 | Scale, 168 | 169 | /// 上下滚动动画 170 | /// 171 | /// Scroll up and down animation 172 | Scroll, 173 | } 174 | ``` 175 | 176 | ## 📺 使用示例 177 | 178 | ### 🔩 Base Demo 179 | 180 | ![](https://gw.alicdn.com/tfs/TB1QSOkJYr1gK0jSZR0XXbP8XXa-320-469.gif) 181 | 182 | ```dart 183 | FSearch( 184 | 185 | /// 设置高 186 | /// 187 | /// Set height 188 | height: 30.0, 189 | 190 | /// 设置背景颜色 191 | /// 192 | /// Set background color 193 | backgroundColor: color, 194 | 195 | /// 设置输入内容样式 196 | /// 197 | /// Set input text style 198 | style: style, 199 | 200 | /// 点击键盘搜索时触发 201 | /// 202 | /// Fired when you click on the keyboard to search 203 | onSearch: (value) { 204 | /// do something 205 | }, 206 | 207 | prefixes: [buildAction()], 208 | ) 209 | ``` 210 | 211 | 使用 **FSearch** 来构建一个搜索栏是一件十分悠然自得的事。 212 | 213 | 通过一些简单的参数,开发者能够很容易去改变搜索栏的大小、颜色、字体。 214 | 215 | 当用户点击键盘的搜索 **Action** 时,会触发 `onSearch`,使得开发者可以在这里进行一些搜索操作。 216 | 217 | 218 | ### 🌖 Prefixes & Suffixes 219 | 220 | ![](https://gw.alicdn.com/tfs/TB1fWytJYj1gK0jSZFuXXcrHpXa-320-469.gif) 221 | 222 | ```dart 223 | FSearch( 224 | height: 30.0, 225 | backgroundColor: mainBackgroundColor, 226 | style: style, 227 | 228 | /// 前缀 Widget 229 | /// 230 | /// prefix widget 231 | prefixes: [ buildAction() ], 232 | 233 | /// 后缀 Widget 234 | /// 235 | /// suffix widget 236 | suffixes: [ 237 | buildAction_1(), 238 | buildAction_2(), 239 | buildAction_3(), 240 | ], 241 | onSearch: _onSearch, 242 | ) 243 | ``` 244 | 245 | 在 FSearch 中,开发者可以通过 `prefixes` 和 `suffixes` 参数,为搜索栏分别配置任意个数的前缀或后缀动作按钮。 246 | 247 | 248 | ### 🌈 Gradient 249 | 250 | ![](https://gw.alicdn.com/tfs/TB1dh1sJ7T2gK0jSZFkXXcIQFXa-320-469.gif) 251 | 252 | ```dart 253 | FSearch( 254 | height: 30.0, 255 | backgroundColor: mainBackgroundColor, 256 | style: style, 257 | 258 | /// 配置渐变色 259 | /// 260 | /// Set gradient 261 | gradient: _gradient, 262 | prefixes: [ buildAction() ], 263 | ) 264 | ``` 265 | 266 | **FSearch** 能够支持开发者创建一个漂亮的渐变色搜索栏。 267 | 268 | 只需要通过 `gradient` 参数进行配置就行。 269 | 270 | ### 🍄 Corner & Stroke & Shadow 271 | 272 | ![](https://gw.alicdn.com/tfs/TB1XrOdJ1L2gK0jSZPhXXahvXXa-320-464.gif) 273 | 274 | ```dart 275 | 276 | /// #1 277 | FSearch( 278 | height: 30.0, 279 | backgroundColor: color, 280 | style: style, 281 | 282 | /// 边角 283 | /// 284 | /// Corner 285 | corner: FSearchCorner( 286 | leftTopCorner: 15.0, 287 | leftBottomCorner: 15.0, 288 | rightBottomCorner: 15.0), 289 | 290 | /// 边框宽 291 | /// 292 | /// border width 293 | strokeWidth: 1.0, 294 | 295 | /// 边框颜色 296 | /// 297 | /// border color 298 | strokeColor: mainTextTitleColor, 299 | 300 | /// 阴影 301 | /// 302 | /// shadow 303 | shadowColor: Colors.black38, 304 | shadowBlur: 5.0, 305 | shadowOffset: Offset(2.0, 2.0), 306 | prefixes: [buildAction()], 307 | ) 308 | ``` 309 | 310 | **FSearch** 的 **边框** 和 **阴影** 效果和其它的 **FWidget** 成员一样,简单易用。 311 | 312 | 通过 `corner` 参数,开发者可以使用 **FSearchCorner** 随意的控制 **FSearch** 的表边角大小。 313 | 314 | ```dart 315 | /// #2 316 | FSearch( 317 | height: 30.0, 318 | backgroundColor: color, 319 | style: style, 320 | 321 | /// 边角 322 | /// 323 | /// Corner 324 | corner: FSearchCorner.all(6.0), 325 | 326 | /// 边角风格 327 | /// 328 | /// Corner style 329 | cornerStyle: FSearchCornerStyle.bevel, 330 | prefixes: [buildAction()], 331 | ) 332 | ``` 333 | 334 | 如果配合 `cornerStyle`,可以实现更加复杂精美的效果。 335 | 336 | 337 | ### 📍 Cursor 338 | 339 | ![](https://gw.alicdn.com/tfs/TB1x4CmJVP7gK0jSZFjXXc5aXXa-320-464.gif) 340 | 341 | ```dart 342 | FSearch( 343 | /// 光标配置 344 | /// 345 | /// Cursor 346 | cursorColor: Colors.red[200], 347 | cursorRadius: 5.0, 348 | cursorWidth: 5.0, 349 | 350 | height: 36.0, 351 | style: style, 352 | gradient: _gradient, 353 | corner: _corner, 354 | prefixes: [ buildAction() ], 355 | suffixes: [ buildAction() ], 356 | ) 357 | ``` 358 | 359 | **FSearch** 支持通过对搜索栏输入框的光标进行修改。你想改成什么样,就改成什么样。 360 | 361 | 362 | 363 | ### 🗂 Hint 364 | 365 | ![](https://gw.alicdn.com/tfs/TB1iFAMXI4z2K4jSZKPXXXAYpXa-360-466.gif) 366 | 367 | 368 | ```dart 369 | /// #1 370 | FSearch( 371 | height: 36.0, 372 | style: style, 373 | color: _color, 374 | corner: _corner, 375 | prefixes: [ buildAction() ], 376 | suffixes: [ buildAction() ], 377 | 378 | /// Hints 379 | hints: [ 380 | "FSuper is awesome 👍", 381 | "Come to use FButton", 382 | "You will love FRefresh", 383 | ], 384 | 385 | /// 开启 hint 交换动画 386 | /// 387 | /// Turn on hint exchange animation 388 | hintSwitchEnable: true, 389 | 390 | /// 配置 hint 交换动画类型 391 | /// 392 | /// Configure hint exchange animation type 393 | hintSwitchType: FSearchAnimationType.Fade, 394 | ) 395 | ``` 396 | 397 | **FSearch** 为开发者提供了非常强大的 **Hint** 效果。 398 | 399 | 开发者可以很容易的为 **FSearch** 设置多条 **Hint** ,而且可以通过配置 `hintSwitchEnable: true` 来开启多 **Hint** 交换动画。 400 | 401 | 当用户开始输入时,Hint 将会被自动隐藏,同时交换动画也会被停止。 402 | 403 | 当搜索输入框内容再次变为空时,Hint 将会再次出现,且开始播放交换动画。 404 | 405 | ```dart 406 | /// #2 407 | FSearch( 408 | height: 36.0, 409 | style: style, 410 | color: _color, 411 | corner: _corner, 412 | prefixes: [ buildAction() ], 413 | suffixes: [ buildAction1(), buildAction2()], 414 | hints: [ 415 | "Do you want to try FFloat?😃", 416 | "FRadio can do more 😱 !", 417 | "I heard that you have been waiting for FDottedLine for a long time...", 418 | ], 419 | hintSwitchEnable: true, 420 | ) 421 | ``` 422 | 423 | 424 | **FSearch** 为开发者准备了丰富的交换动画。默认情况下, **FSearch** 会使用最常见的翻滚交换动画,即 `FSearchAnimationType.Scroll`。 425 | 426 | 当然,开发者可以通过 `hintSwitchType` 参数来配置自己喜欢的动画类型。 427 | 428 | > 💡 注意,当 `hints.length == 1` 时,将不会播放 Hint 交换动画。此时仅仅会展示一个普通的 Hint。 429 | 430 | 431 | ```dart 432 | /// #3 433 | FSearch( 434 | height: 36.0, 435 | style: style, 436 | color: _color, 437 | corner: _corner, 438 | prefixes: [ buildAction() ], 439 | suffixes: [ buildAction() ], 440 | 441 | /// Hints 442 | hints: [ 443 | "Embrace FWidget 👬", 444 | "We care about your app 🥰", 445 | "Want to build beautiful apps 🤨 ?", 446 | ], 447 | hintSwitchEnable: true, 448 | 449 | /// 配置 hint 交换动画类型 450 | /// 451 | /// Configure hint exchange animation type 452 | hintSwitchType: FSearchAnimationType.Scale, 453 | 454 | /// 获得焦点时是否停止交换动画 455 | /// 456 | /// Whether to stop exchanging animation when focus is obtained 457 | stopHintSwitchOnFocus: false, 458 | ) 459 | ``` 460 | 461 | 默认情况下,当 **FSearch** 获得输入焦点, **FSearch** 会自动暂停 **Hint** 交换动画;再次获得焦点时,又会自动恢复。 462 | 463 | 通过配置 `stopHintSwitchOnFocus: false`,可以让 **FSearch** 获得焦点的情况下,依旧继续播放 **Hint** 交换动画,直到用户开始输入. 464 | 465 | 466 | 467 | ### 💻 Controller 468 | 469 | ![](https://gw.alicdn.com/tfs/TB1Pt1oJYY1gK0jSZTEXXXDQVXa-360-298.gif) 470 | 471 | ```dart 472 | FSearch( 473 | controller: _controller, 474 | height: 36.0, 475 | style: style, 476 | gradient: _gradient, 477 | corner: _corner, 478 | prefixes: [ buildAction() ], 479 | suffixes: [ buildAction() ], 480 | hints: [ 481 | "Want more beautiful widgets 🤨 ?", 482 | "We will launch the official website of FWidget", 483 | "Will you expect it?", 484 | ], 485 | hintStyle: hintStyle, 486 | hintSwitchEnable: true, 487 | ) 488 | 489 | /// 获取输入框内容 490 | /// 491 | /// Get the input box content 492 | String input = controller.text; 493 | 494 | /// 清空输入框内容 495 | /// 496 | /// Clear the contents of the input box 497 | controller.text = null; 498 | 499 | /// 获取当前 hint,如果有的话 500 | /// 501 | /// Get the current hint, if any 502 | String hint = controller.hint; 503 | 504 | /// 移除焦点 505 | /// 506 | /// Remove focus 507 | controller.clearFocus(); 508 | 509 | 510 | /// 获取焦点 511 | /// 512 | /// Request focus 513 | controller.requestFocus(); 514 | ``` 515 | 516 | **FSearch** 为开发者们提供了简单好用的、确定的控制器,通过控制器开发者可以在任意的位置对搜索栏的内容进行修改,或者获取。 517 | 518 | # 😃 如何使用? 519 | 520 | 在项目 `pubspec.yaml` 文件中添加依赖: 521 | 522 | ## 🌐 pub 依赖方式 523 | 524 | ``` 525 | dependencies: 526 | fsearch: ^<版本号> 527 | ``` 528 | 529 | > ⚠️ 注意,请到 [**pub**](https://pub.dev/packages/fsearch) 获取 **FSearch** 最新版本号 530 | 531 | ## 🖥 git 依赖方式 532 | 533 | ``` 534 | dependencies: 535 | fsearch: 536 | git: 537 | url: 'git@github.com:Fliggy-Mobile/fsearch.git' 538 | ref: '<分支号 或 tag>' 539 | ``` 540 | 541 | 542 | > ⚠️ 注意,分支号 或 tag 请以 [**FSearch**](https://github.com/Fliggy-Mobile/fsearch) 官方项目为准。 543 | 544 | 545 | # 💡 License 546 | 547 | ``` 548 | Copyright 2020-present Fliggy Android Team . 549 | 550 | Licensed under the Apache License, Version 2.0 (the "License"); 551 | you may not use this file except in compliance with the License. 552 | You may obtain a copy of the License at following link. 553 | 554 | http://www.apache.org/licenses/LICENSE-2.0 555 | 556 | Unless required by applicable law or agreed to in writing, software 557 | distributed under the License is distributed on an "AS IS" BASIS, 558 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 559 | See the License for the specific language governing permissions and 560 | limitations under the License. 561 | 562 | ``` 563 | 564 | 565 | ### 感觉还不错?请投出您的 [**Star**](https://github.com/Fliggy-Mobile/fsearch) 吧 🥰 ! 566 | 567 | 568 | --- 569 | 570 | # 如何运行 Demo 工程? 571 | 572 | 1.**clone** 工程到本地 573 | 574 | 2.进入工程 `example` 目录,运行以下命令 575 | 576 | ``` 577 | flutter create . 578 | ``` 579 | 580 | 3.运行 `example` 中的 Demo 581 | -------------------------------------------------------------------------------- /lib/fsearch.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | /// [hint] 交换动画类型 6 | /// 7 | /// [hint] Swap animation types 8 | enum FSearchAnimationType { 9 | /// 渐变动画 10 | /// 11 | /// Alpha animation 12 | Fade, 13 | 14 | /// 缩放动画 15 | /// 16 | /// Scale animation 17 | Scale, 18 | 19 | /// 上下滚动动画 20 | /// 21 | /// Scroll up and down animation 22 | Scroll, 23 | } 24 | 25 | /// [FSearch] 用于处理搜索模块。支持边框、边角、背景等诸多配置效果。支持不同风格的多 [hint] 切换动画。 26 | /// 27 | /// [FSearch] is used to process the search module. Support many configuration effects such as border, corner, background and so on. 28 | /// Support multiple [hint] switching animations of different styles. 29 | class FSearch extends StatefulWidget { 30 | /// 控制器。详见 [FSearchController] 31 | /// 32 | /// Controller.See [FSearchController] for details 33 | final FSearchController? controller; 34 | 35 | /// 宽。 36 | /// 37 | /// Width 38 | final double? width; 39 | 40 | /// 高 41 | /// 42 | /// Height 43 | final double? height; 44 | 45 | /// 是否可用 46 | /// 47 | /// enable 48 | final bool enable; 49 | 50 | /// 当输入框被点击时会回调 51 | /// 52 | /// Callback when the input box is clicked 53 | final VoidCallback? onTap; 54 | 55 | /// 输入内容 56 | /// 57 | /// input content 58 | final String? text; 59 | 60 | /// 当点击键盘搜索按钮时会回调 61 | /// 62 | /// Callback when the keyboard search button is clicked 63 | final ValueChanged? onSearch; 64 | 65 | /// 边角效果。详见 [FSearchCorner] 66 | /// 67 | /// Corner effect. See [FSearchCorner] for details 68 | final FSearchCorner? corner; 69 | 70 | /// 边角风格。默认 [FSearchCornerStyle.round]。详见 [FSearchCornerStyle] 71 | /// 72 | /// Corner style. The default [FSearchCornerStyle.round]. See [FSearchCornerStyle] for details 73 | final FSearchCornerStyle cornerStyle; 74 | 75 | /// 边框颜色 76 | /// 77 | /// stroke color 78 | final Color? strokeColor; 79 | 80 | /// 边框宽 81 | /// 82 | /// stroke width 83 | final double? strokeWidth; 84 | 85 | /// 背景颜色 86 | /// 87 | /// background color 88 | final Color? backgroundColor; 89 | 90 | /// 背景渐变色。会覆盖 [backgroundColor] 91 | /// 92 | /// Background gradient. Will overwrite [backgroundColor] 93 | final Gradient? gradient; 94 | 95 | /// 设置组件阴影颜色 96 | /// 97 | /// Set component shadow color 98 | final Color? shadowColor; 99 | 100 | /// 设置组件阴影偏移 101 | /// 102 | /// Set component shadow offset 103 | final Offset? shadowOffset; 104 | 105 | /// 设置组件高斯与阴影形状卷积的标准偏差。 106 | /// 107 | /// Sets the standard deviation of the component's Gaussian convolution with the shadow shape. 108 | final double? shadowBlur; 109 | 110 | /// 光标颜色 111 | /// 112 | /// Cursor color 113 | final Color? cursorColor; 114 | 115 | /// 光标宽 116 | /// 117 | /// cursor width 118 | final double? cursorWidth; 119 | 120 | /// 光标边角大小 121 | /// 122 | /// Cursor corner size 123 | final double? cursorRadius; 124 | 125 | /// 前缀动作按钮 126 | /// 127 | /// Prefix action button 128 | final List? prefixes; 129 | 130 | /// 后缀动作按钮 131 | /// 132 | /// Suffix action button 133 | final List? suffixes; 134 | 135 | /// 实际输入区域与 [FSearch] 边缘的间距 136 | /// 137 | /// The distance between the actual input area and the edge of [FSearch] 138 | final EdgeInsets? padding; 139 | 140 | /// [FSearch] 的外间距 141 | /// 142 | /// [FSearch] outer spacing 143 | final EdgeInsets? margin; 144 | 145 | /// 输入文本风格 146 | /// 147 | /// Input text style 148 | final TextStyle? style; 149 | 150 | /// Hint 文本风格 151 | /// 152 | /// Hint text style 153 | final TextStyle? hintStyle; 154 | 155 | /// Hint。如果只有一条 Hint,将无法启用 Hint 交换动画。 156 | /// 157 | /// Hint. If there is only one Hint, Hint swap animation cannot be enabled. 158 | final List? hints; 159 | 160 | /// Hint 交换时间间隔 161 | /// 162 | /// Hint exchange interval 163 | final Duration hintSwitchDuration; 164 | 165 | /// Hint 交换动画时间 166 | /// 167 | /// Hint swap animation time 168 | final Duration hintSwitchAnimDuration; 169 | 170 | /// 是否启用 Hint 交换动画 171 | /// 172 | /// Whether to enable Hint swap animation 173 | final bool hintSwitchEnable; 174 | 175 | /// Hint 交换动画类型。默认 [FSearchAnimationType.Scroll]。详见 [FSearchAnimationType]。 176 | /// 177 | /// Hint exchanges animation types. The default [FSearchAnimationType.Scroll]. See [FSearchAnimationType] for details. 178 | final FSearchAnimationType? hintSwitchType; 179 | 180 | /// 当获得焦点时,是否自动停止 Hint 交换动画。默认 true。 181 | /// 182 | /// When the focus is obtained, whether to automatically stop the Hint exchange animation. The default is true. 183 | final bool? stopHintSwitchOnFocus; 184 | 185 | /// Hint 前缀小部件 186 | /// 187 | /// Hint prefix widget 188 | final Widget? hintPrefix; 189 | 190 | /// 是否居中。 191 | /// 192 | /// Is it centered 193 | final bool center; 194 | 195 | FSearch({ 196 | Key? key, 197 | this.text, 198 | this.width, 199 | this.height, 200 | this.corner, 201 | this.strokeColor, 202 | this.strokeWidth, 203 | this.cornerStyle = FSearchCornerStyle.round, 204 | this.backgroundColor, 205 | this.gradient, 206 | this.shadowBlur, 207 | this.shadowColor, 208 | this.shadowOffset, 209 | this.cursorColor, 210 | this.cursorWidth = 2.0, 211 | this.cursorRadius = 0.0, 212 | this.style, 213 | this.hintStyle, 214 | this.prefixes, 215 | this.suffixes, 216 | this.padding = EdgeInsets.zero, 217 | this.margin, 218 | this.hints, 219 | this.hintSwitchDuration = const Duration(milliseconds: 3000), 220 | this.hintSwitchAnimDuration = const Duration(milliseconds: 800), 221 | this.hintSwitchEnable = false, 222 | this.center = false, 223 | this.stopHintSwitchOnFocus = true, 224 | this.hintPrefix, 225 | this.controller, 226 | this.hintSwitchType = FSearchAnimationType.Scroll, 227 | this.onSearch, 228 | this.enable = true, 229 | this.onTap, 230 | }) : super(key: key); 231 | 232 | @override 233 | _FSearchState createState() => _FSearchState(); 234 | } 235 | 236 | class _FSearchState extends State { 237 | String? hint_0; 238 | String? hint_1; 239 | int nextHintIndex = -1; 240 | double? hintSwitchTop_0; 241 | double? hintSwitchTop_1; 242 | int scrollHintCurrentIndex = 0; 243 | double? inputHeight; 244 | double? inputWidth; 245 | late Duration hintSwitchDuration_0; 246 | late Duration hintSwitchDuration_1; 247 | 248 | bool showHint = true; 249 | 250 | GlobalKey inputKey = GlobalKey(); 251 | late TextEditingController controller; 252 | bool scrollAnimPlaying = false; 253 | Timer? switchTimer; 254 | FocusNode focusNode = FocusNode(); 255 | 256 | String? get hint { 257 | String? r; 258 | if (widget.hints != null && widget.hints!.length > 0) { 259 | int index = nextHintIndex + 1; 260 | if (index > -1 && index < widget.hints!.length) { 261 | r = widget.hints![index]; 262 | } else { 263 | r = widget.hints![0]; 264 | } 265 | } 266 | return r; 267 | } 268 | 269 | @override 270 | void initState() { 271 | widget.controller?._state = this; 272 | 273 | showHint = widget.hints != null && widget.hints!.length > 0; 274 | hintSwitchDuration_0 = widget.hintSwitchAnimDuration; 275 | hintSwitchDuration_1 = widget.hintSwitchAnimDuration; 276 | 277 | controller = TextEditingController(); 278 | 279 | /// 输入监听 280 | /// 281 | /// Input monitor 282 | controller.addListener(() { 283 | bool hasText = false; 284 | if (controller.value.text != null && controller.value.text.length > 0) { 285 | hasText = true; 286 | } 287 | if (showHint != !hasText) { 288 | setState(() { 289 | showHint = !hasText; 290 | if (showHint && 291 | !(focusNode.hasFocus && (widget.stopHintSwitchOnFocus ?? false))) { 292 | playHintSwitchAnim(); 293 | } else { 294 | switchTimer?.cancel(); 295 | tryToFixScrollAnim(); 296 | } 297 | }); 298 | } 299 | widget.controller?._listener?.call(); 300 | }); 301 | 302 | /// 首次初始化文字 303 | /// 304 | /// Initialize text for the first time 305 | if (widget.controller != null && widget.text != null) { 306 | widget.controller!.text = widget.text!; 307 | } else if (widget.text != null) { 308 | controller.text = widget.text!; 309 | } 310 | 311 | /// 焦点监听 312 | /// 313 | /// Focus monitoring 314 | focusNode.addListener(() { 315 | if (focusNode.hasFocus && (widget.stopHintSwitchOnFocus ?? false)) { 316 | switchTimer?.cancel(); 317 | } else { 318 | playHintSwitchAnim(); 319 | } 320 | widget.controller?._focusListener?.call(focusNode.hasFocus); 321 | }); 322 | super.initState(); 323 | } 324 | 325 | void tryToFixScrollAnim() { 326 | if (widget.hintSwitchType == FSearchAnimationType.Scroll && 327 | scrollAnimPlaying) { 328 | scrollAnimPlaying = false; 329 | if (scrollHintCurrentIndex == 0) { 330 | setState(() { 331 | hintSwitchTop_0 = 0; 332 | hintSwitchTop_1 = inputHeight; 333 | }); 334 | } else { 335 | setState(() { 336 | hintSwitchTop_0 = inputHeight; 337 | hintSwitchTop_1 = 0; 338 | }); 339 | } 340 | } 341 | } 342 | 343 | @override 344 | void dispose() { 345 | controller.dispose(); 346 | focusNode.dispose(); 347 | super.dispose(); 348 | } 349 | 350 | @override 351 | Widget build(BuildContext context) { 352 | initInputSize(); 353 | Decoration decoration = buildDecoration(); 354 | List children = []; 355 | 356 | /// 添加前缀 357 | /// 358 | /// Add prefix 359 | if (widget.prefixes != null && widget.prefixes?.length != 0) { 360 | children.addAll(widget.prefixes!); 361 | } 362 | 363 | /// 构建输入区域 364 | /// 365 | /// Build input area 366 | Widget inputArea = buildInputArea(); 367 | children.add(Expanded(child: inputArea)); 368 | 369 | /// 添加后缀 370 | /// 371 | /// Add suffix 372 | if (widget.suffixes != null && widget.suffixes?.length != 0) { 373 | children.addAll(widget.suffixes!); 374 | } 375 | return Container( 376 | width: widget.width, 377 | height: widget.height, 378 | decoration: decoration, 379 | alignment: Alignment.center, 380 | // padding: widget.padding, 381 | margin: widget.margin, 382 | child: Row( 383 | crossAxisAlignment: CrossAxisAlignment.center, 384 | mainAxisSize: MainAxisSize.min, 385 | children: children, 386 | ), 387 | ); 388 | } 389 | 390 | void initInputSize() { 391 | // if (inputHeight != null) return; 392 | WidgetsBinding.instance?.addPostFrameCallback((timeStamp) { 393 | if (!mounted) return; 394 | RenderBox box = inputKey.currentContext?.findRenderObject() as RenderBox; 395 | if (widget.hints != null && 396 | widget.hints!.length > 1 && 397 | inputHeight != box.size.height && 398 | inputWidth != box.size.width) { 399 | setState(() { 400 | inputHeight = box.size.height; 401 | inputWidth = box.size.width; 402 | hintSwitchTop_0 = 0; 403 | hintSwitchTop_1 = inputHeight; 404 | hint_0 = widget.hints![0]; 405 | hint_1 = widget.hints![1]; 406 | playHintSwitchAnim(); 407 | }); 408 | } 409 | }); 410 | } 411 | 412 | Widget buildInputArea() { 413 | List children = []; 414 | 415 | TextStyle style = widget.style ?? buildDefaultTextStyle(); 416 | Widget textField = TextField( 417 | key: inputKey, 418 | focusNode: focusNode, 419 | controller: controller, 420 | textAlign: widget.center ? TextAlign.center : TextAlign.start, 421 | textInputAction: TextInputAction.search, 422 | textAlignVertical: TextAlignVertical.center, 423 | decoration: InputDecoration( 424 | border: OutlineInputBorder( 425 | borderSide: BorderSide(style: BorderStyle.none), 426 | gapPadding: 0.0, 427 | ), 428 | focusedBorder: OutlineInputBorder( 429 | borderSide: BorderSide(style: BorderStyle.none), 430 | gapPadding: 0.0, 431 | ), 432 | focusedErrorBorder: OutlineInputBorder( 433 | borderSide: BorderSide(style: BorderStyle.none), 434 | gapPadding: 0.0, 435 | ), 436 | enabledBorder: OutlineInputBorder( 437 | borderSide: BorderSide(style: BorderStyle.none), 438 | gapPadding: 0.0, 439 | ), 440 | disabledBorder: OutlineInputBorder( 441 | borderSide: BorderSide(style: BorderStyle.none), 442 | gapPadding: 0.0, 443 | ), 444 | errorBorder: OutlineInputBorder( 445 | borderSide: BorderSide(style: BorderStyle.none), 446 | gapPadding: 0.0, 447 | ), 448 | contentPadding: EdgeInsets.all(0), 449 | isDense: true, 450 | ), 451 | style: style, 452 | cursorColor: widget.cursorColor, 453 | cursorWidth: widget.cursorWidth ?? 1, 454 | cursorRadius: Radius.circular(widget.cursorRadius ?? 0), 455 | onSubmitted: widget.onSearch, 456 | onTap: () { 457 | widget.onTap?.call(); 458 | if (!widget.enable) { 459 | focusNode.unfocus(); 460 | } 461 | }, 462 | ); 463 | children.add(Column( 464 | mainAxisAlignment: MainAxisAlignment.center, 465 | children: [textField], 466 | )); 467 | 468 | if (showHint) { 469 | if (widget.hintSwitchEnable && inputHeight != null) { 470 | if (widget.hintSwitchType == FSearchAnimationType.Fade) { 471 | Widget hintSwitcher = buildFadeSwitcher(); 472 | children.add(hintSwitcher); 473 | } else if (widget.hintSwitchType == FSearchAnimationType.Scale) { 474 | Widget hintSwitcher = buildScaleSwitcher(); 475 | children.add(hintSwitcher); 476 | } else { 477 | /// 滚动动画 478 | /// 479 | /// transition anim 480 | Widget hintSwitch_0 = buildScrollSwitch_1(); 481 | Widget hintSwitch_1 = buildScrollSwitch_2(); 482 | children.add(hintSwitch_0); 483 | children.add(hintSwitch_1); 484 | } 485 | } else if (widget.hints != null && widget.hints!.length > 0) { 486 | children.add(buildNormalHint()); 487 | } 488 | } 489 | 490 | return Container( 491 | padding: widget.padding, 492 | child: Stack( 493 | alignment: widget.center ? Alignment.center : Alignment.centerLeft, 494 | children: children, 495 | ), 496 | ); 497 | } 498 | 499 | Widget buildNormalHint() { 500 | TextStyle style = widget.style ?? buildDefaultTextStyle(); 501 | List children = []; 502 | if (widget.hintPrefix != null) { 503 | children.add(widget.hintPrefix!); 504 | } 505 | children.add(LimitedBox( 506 | maxWidth: inputWidth ?? 0.0, 507 | child: Text( 508 | widget.hints![0], 509 | style: widget.hintStyle ?? 510 | style.copyWith( 511 | color: Colors.grey, 512 | ), 513 | overflow: TextOverflow.ellipsis, 514 | ), 515 | )); 516 | return IgnorePointer( 517 | child: Container( 518 | width: inputWidth, 519 | child: Row( 520 | mainAxisSize: MainAxisSize.min, 521 | children: children, 522 | ), 523 | ), 524 | ); 525 | } 526 | 527 | AnimatedPositioned buildScrollSwitch_2() { 528 | TextStyle style = widget.style ?? buildDefaultTextStyle(); 529 | Widget child = Text( 530 | hint_1 ?? "", 531 | style: widget.hintStyle ?? style.copyWith(color: Colors.grey), 532 | overflow: TextOverflow.ellipsis, 533 | ); 534 | return AnimatedPositioned( 535 | top: hintSwitchTop_1, 536 | child: IgnorePointer( 537 | child: LimitedBox( 538 | maxWidth: inputWidth ?? 0.0, 539 | child: Container( 540 | height: inputHeight, 541 | width: inputWidth, 542 | alignment: widget.center ? Alignment.center : Alignment.centerLeft, 543 | child: child, 544 | ), 545 | ), 546 | ), 547 | duration: hintSwitchDuration_1, 548 | onEnd: () { 549 | if (hintSwitchTop_1 == -(inputHeight ?? 0)) { 550 | setState(() { 551 | hintSwitchTop_1 = inputHeight; 552 | hintSwitchDuration_1 = Duration(milliseconds: 0); 553 | }); 554 | } else { 555 | hintSwitchDuration_1 = widget.hintSwitchAnimDuration; 556 | } 557 | }, 558 | ); 559 | } 560 | 561 | Widget buildScrollSwitch_1() { 562 | TextStyle style = widget.style ?? buildDefaultTextStyle(); 563 | Widget child = Text( 564 | (hint_0 ?? ""), 565 | style: widget.hintStyle ?? style.copyWith(color: Colors.grey), 566 | overflow: TextOverflow.ellipsis, 567 | ); 568 | return AnimatedPositioned( 569 | top: hintSwitchTop_0, 570 | child: IgnorePointer( 571 | child: LimitedBox( 572 | maxWidth: inputWidth ?? 0.0, 573 | child: Container( 574 | height: inputHeight, 575 | alignment: widget.center ? Alignment.center : Alignment.centerLeft, 576 | child: child, 577 | ), 578 | ), 579 | ), 580 | duration: hintSwitchDuration_0, 581 | onEnd: () { 582 | scrollAnimPlaying = false; 583 | if (hintSwitchTop_0 == -(inputHeight ?? 0)) { 584 | setState(() { 585 | hintSwitchTop_0 = inputHeight; 586 | hintSwitchDuration_0 = Duration(milliseconds: 0); 587 | }); 588 | } else { 589 | hintSwitchDuration_0 = widget.hintSwitchAnimDuration; 590 | } 591 | }, 592 | ); 593 | } 594 | 595 | Widget buildScaleSwitcher() { 596 | int index = nextHintIndex == -1 ? 0 : nextHintIndex; 597 | TextStyle style = widget.style ?? buildDefaultTextStyle(); 598 | Widget child = Text( 599 | widget.hints![index], 600 | style: widget.hintStyle ?? style.copyWith(color: Colors.grey), 601 | overflow: TextOverflow.ellipsis, 602 | ); 603 | return AnimatedSwitcher( 604 | child: IgnorePointer( 605 | key: ValueKey(index), 606 | child: Container( 607 | alignment: widget.center ? Alignment.center : Alignment.centerLeft, 608 | height: inputHeight, 609 | child: child, 610 | ), 611 | ), 612 | duration: widget.hintSwitchAnimDuration, 613 | transitionBuilder: (child, animation) => ScaleTransition( 614 | scale: animation, 615 | child: child, 616 | alignment: widget.center ? Alignment.center : Alignment.centerLeft, 617 | ), 618 | ); 619 | } 620 | 621 | Widget buildFadeSwitcher() { 622 | int index = nextHintIndex == -1 ? 0 : nextHintIndex; 623 | TextStyle style = widget.style ?? buildDefaultTextStyle(); 624 | Widget child = Text( 625 | widget.hints![index], 626 | style: widget.hintStyle ?? style.copyWith(color: Colors.grey), 627 | overflow: TextOverflow.ellipsis, 628 | ); 629 | Widget hintSwitcher = AnimatedSwitcher( 630 | child: IgnorePointer( 631 | key: ValueKey(index), 632 | child: Container( 633 | height: inputHeight, 634 | alignment: widget.center ? Alignment.center : Alignment.centerLeft, 635 | child: child, 636 | ), 637 | ), 638 | duration: widget.hintSwitchAnimDuration, 639 | ); 640 | return hintSwitcher; 641 | } 642 | 643 | void playHintSwitchAnim() { 644 | if (!widget.hintSwitchEnable || 645 | !showHint || 646 | inputHeight == null || 647 | widget.hints == null || 648 | widget.hints!.length < 2) return; 649 | switchTimer?.cancel(); 650 | switchTimer = Timer(widget.hintSwitchDuration, () { 651 | if (!mounted && 652 | showHint && 653 | widget.hintSwitchEnable && 654 | inputHeight != null) return; 655 | List hints = widget.hints!; 656 | if (widget.hintSwitchType != FSearchAnimationType.Scroll) { 657 | setState(() { 658 | nextHintIndex = 659 | (nextHintIndex + 1 == hints.length ? 0 : nextHintIndex + 1); 660 | }); 661 | } else { 662 | double switchHintTop(double hintTop) { 663 | if (hintTop == 0) { 664 | hintTop = -inputHeight!; 665 | } else if (hintTop == inputHeight) { 666 | hintTop = 0; 667 | } else { 668 | hintTop = inputHeight!; 669 | } 670 | return hintTop; 671 | } 672 | 673 | setState(() { 674 | scrollAnimPlaying = true; 675 | if (hintSwitchTop_0 == hintSwitchTop_1) tryToFixScrollAnim(); 676 | nextHintIndex = 677 | (nextHintIndex + 1 == hints.length ? 0 : nextHintIndex + 1); 678 | int nextIndex = 679 | (nextHintIndex + 1 == hints.length ? 0 : nextHintIndex + 1); 680 | if (hintSwitchTop_0 == inputHeight) { 681 | hint_0 = hints[nextIndex]; 682 | } else { 683 | hint_0 = hints[nextHintIndex]; 684 | } 685 | if (hintSwitchTop_1 == inputHeight) { 686 | hint_1 = hints[nextIndex]; 687 | } else { 688 | hint_1 = hints[nextHintIndex]; 689 | } 690 | hintSwitchTop_0 = switchHintTop(hintSwitchTop_0!); 691 | if (hintSwitchTop_0 == 0) { 692 | scrollHintCurrentIndex = 0; 693 | } 694 | hintSwitchTop_1 = switchHintTop(hintSwitchTop_1!); 695 | if (hintSwitchTop_1 == 0) { 696 | scrollHintCurrentIndex = 1; 697 | } 698 | }); 699 | } 700 | playHintSwitchAnim(); 701 | }); 702 | } 703 | 704 | TextStyle buildDefaultTextStyle() => TextStyle( 705 | color: Color(0xff333333), 706 | fontSize: 16.0, 707 | ); 708 | 709 | Decoration buildDecoration() { 710 | double opacity = 0.38; 711 | BorderRadius borderRadius = widget.corner == null 712 | ? BorderRadius.all(Radius.circular(0)) 713 | : BorderRadius.only( 714 | topRight: Radius.circular(widget.corner!.rightTopCorner), 715 | bottomRight: Radius.circular(widget.corner!.rightBottomCorner), 716 | bottomLeft: Radius.circular(widget.corner!.leftBottomCorner), 717 | ); 718 | Color sideColor = widget.strokeColor ?? Colors.transparent; 719 | BorderSide borderSide = BorderSide( 720 | width: widget.strokeWidth ?? 0, 721 | color: sideColor, 722 | style: BorderStyle.solid, 723 | ); 724 | var shape = widget.cornerStyle == FSearchCornerStyle.round 725 | ? RoundedRectangleBorder( 726 | borderRadius: borderRadius, 727 | side: borderSide, 728 | ) 729 | : BeveledRectangleBorder( 730 | borderRadius: borderRadius, 731 | side: borderSide, 732 | ); 733 | Decoration decoration = ShapeDecoration( 734 | color: widget.gradient == null ? widget.backgroundColor : null, 735 | gradient: widget.gradient, 736 | shadows: widget.shadowColor != null && widget.shadowBlur != 0 737 | ? [ 738 | BoxShadow( 739 | color: widget.shadowColor ?? 740 | (widget.backgroundColor ?? Colors.white).withOpacity(opacity), 741 | offset: widget.shadowOffset ?? Offset(0, 0), 742 | blurRadius: widget.shadowBlur ?? 0.0, 743 | ) 744 | ] 745 | : null, 746 | shape: shape); 747 | return decoration; 748 | } 749 | } 750 | 751 | /// [FSearch] 的控制器,能够回去到输入的文本、Hint、焦点状态等信息。同时提供各种监听和文本更新能力。 752 | /// 753 | /// The controller of [FSearch] can go back to the input text, Hint, focus status and other information. 754 | /// At the same time provide a variety of monitoring and text update capabilities. 755 | class FSearchController { 756 | _FSearchState? _state; 757 | 758 | /// 输入的文本内容 759 | /// 760 | /// input text 761 | String get text => _state?.controller.value.text ?? ""; 762 | 763 | /// 主动更新输入文本 764 | /// 765 | /// Actively update input text 766 | set text(String value) { 767 | if (_state?.controller.text != value) { 768 | _state?.controller.clear(); 769 | _state?.controller.text = value; 770 | _state?.controller.selection = 771 | TextSelection.collapsed(offset: value.length); 772 | _listener?.call(); 773 | } 774 | } 775 | 776 | /// 当前 Hint 内容 777 | /// 778 | /// Current Hint content 779 | String? get hint => (_state?.hint) ?? null; 780 | 781 | /// 焦点状态 782 | /// 783 | /// Focus state 784 | bool get focus => (_state?.focusNode.hasFocus) ?? false; 785 | 786 | ValueChanged? _focusListener; 787 | 788 | VoidCallback? _listener; 789 | 790 | /// 设置输入监听 791 | /// 792 | /// Set input listener 793 | setListener(VoidCallback listener) { 794 | _listener = listener; 795 | } 796 | 797 | /// 设置焦点变化监听 798 | /// 799 | /// set focus changed listener 800 | setOnFocusChangedListener(ValueChanged listener) { 801 | _focusListener = listener; 802 | } 803 | 804 | /// 请求获得焦点 805 | /// 806 | /// request focus 807 | requestFocus() { 808 | _state?.focusNode.requestFocus(); 809 | } 810 | 811 | /// 移除焦点 812 | /// 813 | /// clear focus 814 | clearFocus() { 815 | _state?.focusNode.unfocus(); 816 | } 817 | 818 | /// 销毁 819 | /// 820 | /// destroy 821 | dispose() { 822 | _state = null; 823 | } 824 | } 825 | 826 | /// 边角。 827 | /// 828 | /// Corner 829 | class FSearchCorner { 830 | final double leftTopCorner; 831 | final double rightTopCorner; 832 | final double rightBottomCorner; 833 | final double leftBottomCorner; 834 | 835 | /// 指定每一个圆角的大小 836 | /// 837 | /// Specify the size of each rounded corner 838 | const FSearchCorner({ 839 | this.leftTopCorner = 0, 840 | this.rightTopCorner = 0, 841 | this.rightBottomCorner = 0, 842 | this.leftBottomCorner = 0, 843 | }); 844 | 845 | /// 设置所有圆角为一个大小 846 | /// 847 | /// Set all rounded corners to one size 848 | FSearchCorner.all(double radius) 849 | : leftTopCorner = radius, 850 | rightTopCorner = radius, 851 | rightBottomCorner = radius, 852 | leftBottomCorner = radius; 853 | } 854 | 855 | /// 边角风格。 856 | /// [round] - 圆角 857 | /// [bevel] - 斜角 858 | /// 859 | /// Rounded corner style. 860 | /// [round]-rounded corners 861 | /// [bevel]-beveled corners 862 | enum FSearchCornerStyle { 863 | round, 864 | bevel, 865 | } 866 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | dart pub publish --dry-run 4 | 5 | echo "" 6 | echo "\033[33m This package will publish after 10s. Please make sure everything is ok. \033[0m" 7 | 8 | count=1 9 | while(($count<10)) 10 | do 11 | echo "$count.." 12 | let "count++" 13 | sleep 1 14 | done 15 | 16 | dart pub publish --server=https://pub.dartlang.org 17 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: fsearch 2 | description: To help developers build the most beautiful search bar. 3 | version: 2.0.0 4 | author: CoorChice 5 | homepage: https://github.com/Fliggy-Mobile/fsearch 6 | 7 | environment: 8 | sdk: ">=2.12.0 <3.0.0" 9 | flutter: ">=1.10.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | 15 | dev_dependencies: 16 | flutter_test: 17 | sdk: flutter 18 | 19 | # For information on the generic Dart part of this file, see the 20 | # following page: https://dart.dev/tools/pub/pubspec 21 | 22 | # The following section is specific to Flutter. 23 | flutter: 24 | # This section identifies this Flutter project as a plugin project. 25 | # The 'pluginClass' and Android 'package' identifiers should not ordinarily 26 | # be modified. They are used by the tooling to maintain consistency when 27 | # adding or updating assets for this project. 28 | #plugin: 29 | ## platforms: 30 | ## android: 31 | ## package: com.taobao.fapi.fsearch 32 | ## pluginClass: FsearchPlugin 33 | ## ios: 34 | ## pluginClass: FsearchPlugin 35 | ## macos: 36 | ## pluginClass: FsearchPlugin 37 | 38 | # To add assets to your plugin package, add an assets section, like this: 39 | # assets: 40 | # - images/a_dot_burr.jpeg 41 | # - images/a_dot_ham.jpeg 42 | # 43 | # For details regarding assets in packages, see 44 | # https://flutter.dev/assets-and-images/#from-packages 45 | # 46 | # An image asset can refer to one or more resolution-specific "variants", see 47 | # https://flutter.dev/assets-and-images/#resolution-aware. 48 | 49 | # To add custom fonts to your plugin package, add a fonts section here, 50 | # in this "flutter" section. Each entry in this list should have a 51 | # "family" key with the font family name, and a "fonts" key with a 52 | # list giving the asset and other descriptors for the font. For 53 | # example: 54 | # fonts: 55 | # - family: Schyler 56 | # fonts: 57 | # - asset: fonts/Schyler-Regular.ttf 58 | # - asset: fonts/Schyler-Italic.ttf 59 | # style: italic 60 | # - family: Trajan Pro 61 | # fonts: 62 | # - asset: fonts/TrajanPro.ttf 63 | # - asset: fonts/TrajanPro_Bold.ttf 64 | # weight: 700 65 | # 66 | # For details regarding fonts in packages, see 67 | # https://flutter.dev/custom-fonts/#from-packages 68 | -------------------------------------------------------------------------------- /test/fsearch_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:fsearch/fsearch.dart'; 4 | 5 | void main() { 6 | 7 | } 8 | --------------------------------------------------------------------------------