From 4c11ca99264e6122b3b32b4ff80ebd2af3131857 Mon Sep 17 00:00:00 2001 From: ManelRosPuig Date: Fri, 22 Sep 2023 21:47:24 +0200 Subject: [PATCH 1/5] Update main.dart example --- example/lib/main.dart | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index e4654bf..80e3308 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:standard_searchbar/standard_searchbar.dart'; +import 'package:standard_searchbar/new/standard_search_anchor.dart'; +import 'package:standard_searchbar/new/standard_search_bar.dart'; + +// import 'package:standard_searchbar/standard_searchbar.dart'; void main() => runApp(const MyApp()); @@ -16,24 +19,10 @@ class MyApp extends StatelessWidget { ), body: const SizedBox( width: double.infinity, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox(height: 50), - StandardSearchBar( - width: 360, - suggestions: [ - 'apple', - 'banana', - 'melon', - 'orange', - 'pineapple', - 'strawberry', - 'watermelon' - ], - ), - SizedBox(height: 200), - ], + child: Center( + child: StandardSearchAnchor( + searchBar: StandardSearchBar(), + ), ), ), // backgroundColor: Colors.black12, From 007ba2be229f6f0ddb9b8ac0289bbcbe9d9cc40d Mon Sep 17 00:00:00 2001 From: ManelRosPuig Date: Fri, 22 Sep 2023 21:47:56 +0200 Subject: [PATCH 2/5] Add Standard Search Anchor --- lib/new/standard_search_anchor.dart | 88 +++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 lib/new/standard_search_anchor.dart diff --git a/lib/new/standard_search_anchor.dart b/lib/new/standard_search_anchor.dart new file mode 100644 index 0000000..abdd999 --- /dev/null +++ b/lib/new/standard_search_anchor.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; + +import 'package:standard_searchbar/new/standard_search_bar.dart'; +import 'package:standard_searchbar/new/standard_search_controller.dart'; + +/// If there is no StandardSearchController passed in the constructor of +/// StandardSearchBar, then a default one will be created. +class StandardSearchAnchor extends StatefulWidget { + const StandardSearchAnchor({ + super.key, + this.controller, + required this.searchBar, + }); + + final StandardSearchController? controller; + final StandardSearchBar searchBar; + + @override + State createState() => StandardSearchAnchorState(); +} + +class StandardSearchAnchorState extends State { + late final StandardSearchController controller; + final unfocus = [false, false]; + final layerLink = LayerLink(); + OverlayEntry? entry; + + @override + void initState() { + super.initState(); + if (widget.controller != null) { + controller = widget.controller!; + } else { + controller = StandardSearchController(); + } + controller.anchor = this; + } + + @override + Widget build(BuildContext context) { + return TapRegion( + onTapInside: (e) { + controller.open(); + }, + onTapOutside: (e) { + controller.close(); + }, + child: CompositedTransformTarget( + link: layerLink, + child: widget.searchBar, + ), + ); + } + + void clear() { + controller.clear(); + } + + void open() { + if (entry != null) return; + final renderBox = context.findRenderObject() as RenderBox; + final offset = renderBox.localToGlobal(Offset.zero); + entry = OverlayEntry(builder: (_) { + return Positioned( + left: offset.dx, + top: offset.dy + renderBox.size.height + 16, + width: renderBox.size.width, + child: CompositedTransformFollower( + link: layerLink, + showWhenUnlinked: false, + offset: Offset(0, renderBox.size.height), + child: Container( + height: 200, + color: Colors.red, + ), + ), + ); + }); + Overlay.of(context).insert(entry!); + } + + void close() { + if (entry != null) { + entry!.remove(); + entry = null; + } + } +} From 9d5ba06cd945ddefc59e7f497dd5440c56ac7a23 Mon Sep 17 00:00:00 2001 From: ManelRosPuig Date: Fri, 22 Sep 2023 21:48:18 +0200 Subject: [PATCH 3/5] Add Standard Search Bar --- lib/new/standard_search_bar.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 lib/new/standard_search_bar.dart diff --git a/lib/new/standard_search_bar.dart b/lib/new/standard_search_bar.dart new file mode 100644 index 0000000..32ee693 --- /dev/null +++ b/lib/new/standard_search_bar.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class StandardSearchBar extends StatelessWidget { + const StandardSearchBar({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + width: 360, + height: 56, + color: Colors.yellow, + padding: const EdgeInsets.symmetric(horizontal: 16), + ); + } +} From af6a7a86af8f7be7aaa46607b48535324c8dcf6e Mon Sep 17 00:00:00 2001 From: ManelRosPuig Date: Fri, 22 Sep 2023 21:48:44 +0200 Subject: [PATCH 4/5] Add Standard Search Controller --- lib/new/standard_search_controller.dart | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 lib/new/standard_search_controller.dart diff --git a/lib/new/standard_search_controller.dart b/lib/new/standard_search_controller.dart new file mode 100644 index 0000000..143faf0 --- /dev/null +++ b/lib/new/standard_search_controller.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +import 'package:standard_searchbar/new/standard_search_anchor.dart'; + +class StandardSearchController extends TextEditingController { + // Anchor attached to this controller + StandardSearchAnchorState? _anchor; + set anchor(StandardSearchAnchorState anchor) => _anchor = anchor; + + void open() => _anchor?.open(); + void close() => _anchor?.close(); + + @override + void clear() { + super.clear(); + _anchor?.clear(); + } + + @override + void dispose() { + super.dispose(); + _anchor?.close(); + } +} From 07ae11490832f61cb6916923bc366b941b37cf94 Mon Sep 17 00:00:00 2001 From: ManelRosPuig Date: Fri, 22 Sep 2023 21:49:12 +0200 Subject: [PATCH 5/5] Remove old files --- analysis_options.yaml | 2 +- lib/standard_icon.dart | 60 ----- lib/standard_searchbar.dart | 394 ------------------------------ lib/standard_suggestions_box.dart | 98 -------- lib/standard_text_field.dart | 58 ----- 5 files changed, 1 insertion(+), 611 deletions(-) delete mode 100644 lib/standard_icon.dart delete mode 100644 lib/standard_searchbar.dart delete mode 100644 lib/standard_suggestions_box.dart delete mode 100644 lib/standard_text_field.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index f9b3034..a3be6b8 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1 +1 @@ -include: package:flutter_lints/flutter.yaml +include: package:flutter_lints/flutter.yaml \ No newline at end of file diff --git a/lib/standard_icon.dart b/lib/standard_icon.dart deleted file mode 100644 index 3caa5f3..0000000 --- a/lib/standard_icon.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; - -class StandardIcon extends StatelessWidget { - const StandardIcon({ - super.key, - required this.icon, - required this.iconColor, - required this.iconSize, - this.iconSplashColor, - this.iconOnTap, - this.iconPaddingLeft, - this.iconPaddingRight, - }); - - /// The Icon to display. - final IconData icon; - - /// The color of the Icon. - final Color iconColor; - - /// The size of the Icon. - final double iconSize; - - /// The splash color of the Icon. - final Color? iconSplashColor; - - /// The callback function when the Icon is tapped. - final Function()? iconOnTap; - - /// The left padding of the Icon. - final double? iconPaddingLeft; - - /// The right padding of the Icon. - final double? iconPaddingRight; - - @override - Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.only( - left: iconPaddingLeft ?? 0, right: iconPaddingRight ?? 0), - child: ClipOval( - child: Material( - color: Colors.transparent, - child: InkWell( - splashColor: iconSplashColor, - onTap: iconOnTap, - child: Padding( - padding: const EdgeInsets.all(4.0), - child: Icon( - icon, - color: iconColor, - size: iconSize, - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/standard_searchbar.dart b/lib/standard_searchbar.dart deleted file mode 100644 index 4681038..0000000 --- a/lib/standard_searchbar.dart +++ /dev/null @@ -1,394 +0,0 @@ -library standard_searchbar; - -import 'package:flutter/material.dart'; - -import 'package:standard_searchbar/standard_icon.dart'; -import 'package:standard_searchbar/standard_suggestions_box.dart'; -import 'package:standard_searchbar/standard_text_field.dart'; - -class StandardSearchBar extends StatefulWidget { - const StandardSearchBar({ - super.key, - this.width, - this.height = 50, - this.borderRadius = 25, - this.backgroundColor = Colors.white, - this.hintText = 'Search', - this.hintStyle = const TextStyle(color: Colors.grey), - this.startIcon = Icons.search, - this.startIconColor = Colors.grey, - this.endIcon = Icons.mic, - this.endIconColor = Colors.grey, - this.showStartIcon = true, - this.showEndIcon = false, - this.cursorColor = Colors.grey, - this.startIconSplashColor, - this.startIconOnTap, - this.endIconOnTap, - this.endIconSplashColor, - this.startIconSize = 20, - this.endIconSize = 20, - this.horizontalPadding = 10, - this.startIconPaddingRight = 8, - this.endIconPaddingLeft = 8, - this.onSubmitted, - this.onChanged, - this.shadow = const [ - BoxShadow( - color: Colors.black12, - spreadRadius: 0, - blurRadius: 5, - offset: Offset(0, 3), - ), - ], - this.textStyle = const TextStyle(color: Colors.black), - this.suggestions, - this.suggestionsBoxHeight = 175, - this.suggestionsBoxPadding = - const EdgeInsets.symmetric(horizontal: 16, vertical: 16), - this.suggestionTextStyle = const TextStyle(fontSize: 16), - this.maxSuggestions = 10, - }); - - /// The width of the search bar. By default is the width of the parent (expanded). - final double? width; - - /// The height of the search bar. By default is 50. - final double height; - - /// The border radius of the search bar. By default is 25 (rounded), but it can be - /// any value. - final double borderRadius; - - /// The background color of the search bar. By default is white. - final Color backgroundColor; - - /// The hint text of the SearchBar. By default is 'Search'. - final String hintText; - - /// The hint text style of the SearchBar. By default is grey. - final TextStyle hintStyle; - - /// The start icon of the SearchBar. By default is Icons.search. - final IconData startIcon; - - /// The color of the start icon. By default is grey. - final Color startIconColor; - - /// The end icon of the SearchBar. By default is Icons.mic. - final IconData endIcon; - - /// The color of the end icon. By default is grey. - final Color endIconColor; - - /// Whether to show the start icon or not. By default is true. If false, the - /// start icon will not be shown and the icon padding will be removed. - final bool showStartIcon; - - /// Whether to show the end icon or not. By default is false. If true, the - /// end icon will be shown and the icon padding will be removed. - final bool showEndIcon; - - /// The color of the cursor. By default is grey. - final Color cursorColor; - - /// The splash color of the start icon. The splash color is the color that - /// appears when the icon is tapped. By default is null, because it is calculated - /// automatically by the Material widget. - final Color? startIconSplashColor; - - /// The function callback of the startIcon. If it is not null, the end icon will - /// be clickable and the splash color will be shown. By default is null. - final Function()? startIconOnTap; - - /// The function callback of the endIcon. If it is not null, the end icon will - /// be clickable and the splash color will be shown. By default is null. - final Function()? endIconOnTap; - - /// The splash color of the end icon. The splash color is the color that - /// appears when the icon is tapped. By default is null, because it is calculated - /// automatically by the Material widget. - final Color? endIconSplashColor; - - /// The size of the start icon. By default is 20. - final double startIconSize; - - /// The size of the end icon. By default is 20. - final double endIconSize; - - /// The horizontal padding of the search bar. By default is 10. - final double horizontalPadding; - - /// The right padding of the start icon. By default is 8. - final double startIconPaddingRight; - - /// The left padding of the end icon. By default is 8. - final double endIconPaddingLeft; - - /// The function callback of the TextField onSubmitted. By default is null. - /// This function can be used to search the text with the given value. This - /// function is called when the user presses the enter key. - final Function(String)? onSubmitted; - - /// The function callback of the TextField onChanged. By default is null. - /// This function is executed every time the text changes. So it can be - /// execute a search every time the user types a letter or it can be used - /// to update the search suggestions. - final Function(String)? onChanged; - - /// The shadow of the search bar. By default is a little black shadow. It - /// can be any value. A list of BoxShadow. - final List shadow; - - /// The text style of the TextField. By default the text color is black. - final TextStyle textStyle; - - /// The suggestions of the search bar. By default is null. It can be any - /// value. A list of String. - final List? suggestions; - - /// The height of the suggestions box. By default is 175. - final double? suggestionsBoxHeight; - - /// The padding of the suggestions box. By default is symetric horizontal 16 - /// and vertical 12. - final EdgeInsetsGeometry? suggestionsBoxPadding; - - /// The text style of the suggestions. By default the text color is fontsize is 16. - final TextStyle? suggestionTextStyle; - - /// The max number of suggestions to show. If null, all the suggestions will - /// be shown. By default is 10. - final int? maxSuggestions; - - @override - State createState() => _StandardSearchBarState(); -} - -class _StandardSearchBarState extends State { - bool isSearchBarFocused = false; - final TextEditingController controller = TextEditingController(); - List? suggestions; - - OverlayEntry? entry; - final layerLink = LayerLink(); - final unfocus = [false, false]; - - @override - void initState() { - super.initState(); - suggestions = widget.suggestions; - } - - void updateSuggestions(String value) { - // Null safety - if (widget.suggestions == null) return; - - if (value.isEmpty) { - setState(() => suggestions = widget.suggestions ?? []); - return; - } - - setState(() { - suggestions = widget.suggestions! - .where((element) => isSimilar(element, value)) - .toList(); - suggestions = orderContains(suggestions!, value); - suggestions = orderStartsWith(suggestions!, value); - suggestions = removeDuplicates(suggestions!); - }); - - updateOverlay(); - } - - bool isSimilar(String original, String searchValue) { - String originalLower = original.toLowerCase(); - String searchValueLower = searchValue.toLowerCase(); - - if (originalLower.contains(searchValueLower)) return true; - - int similarCharactersCount = 0; - for (var char in searchValueLower.runes) { - if (originalLower.runes.contains(char)) { - similarCharactersCount++; - } - } - - return similarCharactersCount >= searchValueLower.length / 1.25; // 2 - } - - List orderContains(List suggestions, String value) { - suggestions.sort((a, b) { - if (a.contains(value) && !b.contains(value)) { - return -1; - } else if (!a.contains(value) && b.contains(value)) { - return 1; - } else { - return 0; - } - }); - return suggestions; - } - - List orderStartsWith(List suggestions, String value) { - suggestions.sort((a, b) { - if (a.startsWith(value) && !b.startsWith(value)) { - return -1; - } else if (!a.startsWith(value) && b.startsWith(value)) { - return 1; - } else { - return 0; - } - }); - return suggestions; - } - - List removeDuplicates(List suggestions) { - return suggestions.toSet().toList(); - } - - @override - Widget build(BuildContext context) { - return TapRegion( - onTapInside: (e) { - if (suggestions == null) return; - if (isSearchBarFocused) return; - unfocus[1] = false; - focus(); - showOverlay(); - }, - onTapOutside: (e) { - unfocus[1] = true; - requestUnFocus(1); - }, - child: CompositedTransformTarget( - link: layerLink, - child: Container( - width: widget.width, - decoration: BoxDecoration( - color: widget.backgroundColor, - borderRadius: isSearchBarFocused - ? BorderRadius.only( - topLeft: Radius.circular(widget.borderRadius), - topRight: Radius.circular(widget.borderRadius), - ) - : BorderRadius.circular(widget.borderRadius), - boxShadow: widget.shadow, - ), - child: Padding( - padding: EdgeInsets.symmetric(horizontal: widget.horizontalPadding), - child: Row( - children: [ - if (widget.showStartIcon != false) - StandardIcon( - icon: widget.startIcon, - iconColor: widget.startIconColor, - iconSize: widget.startIconSize, - iconSplashColor: widget.startIconSplashColor, - iconPaddingRight: widget.startIconPaddingRight, - iconOnTap: widget.startIconOnTap, - ), - Expanded( - child: StandardTextField( - showEndIcon: widget.showEndIcon, - endIconPaddingLeft: widget.endIconPaddingLeft, - hintText: widget.hintText, - hintStyle: widget.hintStyle, - cursorColor: widget.cursorColor, - textStyle: widget.textStyle, - controller: controller, - onSubmitted: widget.onSubmitted, - onChanged: widget.onChanged, - horizontalPadding: widget.horizontalPadding, - updateSuggestions: updateSuggestions, - ), - ), - if (widget.showEndIcon != false) - StandardIcon( - icon: widget.endIcon, - iconColor: widget.endIconColor, - iconSize: widget.endIconSize, - iconSplashColor: widget.endIconSplashColor, - iconPaddingLeft: widget.endIconPaddingLeft, - iconOnTap: widget.endIconOnTap, - ) - ], - ), - ), - ), - ), - ); - } - - void showOverlay() { - if (widget.suggestions == null) return; - - final overlay = Overlay.of(context); - final renderBox = context.findRenderObject() as RenderBox; - final size = renderBox.size; - final offset = renderBox.localToGlobal(Offset.zero); - entry = OverlayEntry( - builder: (context) => Positioned( - left: offset.dx, - top: offset.dy + size.height + 16, - width: size.width, - child: CompositedTransformFollower( - link: layerLink, - showWhenUnlinked: false, - offset: Offset(0, size.height), - child: StandardSuggestionsBox( - suggestions: suggestions! - .take(widget.maxSuggestions ?? suggestions!.length) - .toList(), - borderRadius: widget.borderRadius, - backgroundColor: widget.backgroundColor, - boxHeight: widget.suggestionsBoxHeight!, - boxPadding: widget.suggestionsBoxPadding!, - suggestionTextStyle: widget.suggestionTextStyle!, - onSuggestionSelected: (s) { - controller.text = s; - widget.onSubmitted?.call(s); - unFocus(); - }, - onTapInside: (e) { - unfocus[0] = false; - }, - onTapOutside: (e) { - unfocus[0] = true; - Future.delayed( - const Duration(milliseconds: 100), () => requestUnFocus(0)); - }, - ), - ), - ), - ); - - overlay.insert(entry!); - } - - void updateOverlay() { - if (widget.suggestions == null) return; - if (!isSearchBarFocused) return; - entry?.markNeedsBuild(); - } - - void requestUnFocus(int n) { - if (unfocus[0] && unfocus[1]) { - unFocus(); - unfocus[0] = false; - unfocus[1] = false; - } - } - - void focus() { - if (widget.suggestions == null) return; - setState(() => isSearchBarFocused = true); - } - - void unFocus() { - if (widget.suggestions == null) return; - if (!isSearchBarFocused) return; - setState(() => isSearchBarFocused = false); - entry?.remove(); - } -} diff --git a/lib/standard_suggestions_box.dart b/lib/standard_suggestions_box.dart deleted file mode 100644 index 7ae8169..0000000 --- a/lib/standard_suggestions_box.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:flutter/material.dart'; - -class StandardSuggestionsBox extends StatelessWidget { - const StandardSuggestionsBox({ - super.key, - required this.suggestions, - required this.borderRadius, - required this.backgroundColor, - required this.onSuggestionSelected, - required this.onTapInside, - required this.onTapOutside, - required this.boxHeight, - required this.boxPadding, - required this.suggestionTextStyle, - }); - - /// List of suggestions to display. The `StandardSearchBar` class will pass - /// the filtered and ordered suggestions to this class. - final List suggestions; - - /// The radius of the bottom corners of the suggestions box. This param is - /// the same as the one in the `StandardSearchBar` class. - final double borderRadius; - - /// The background color of the suggestions box. This param is the same as - /// the one in the `StandardSearchBar` class. - final Color backgroundColor; - - /// Callback function when a suggestion is selected. Basically, it takes the - /// selected suggestion and put it in the text field. Then, it calls the - /// onSubmitted callback function and unfocus the text field. - final Function(String) onSuggestionSelected; - - /// Callback function when the suggestions box is tapped inside. Used to detect - /// when the suggestions box is tapped inside so that the suggestions box can - /// be closed. Same as the onTapOutside callback function. - final Function(PointerEvent) onTapInside; - - /// Callback function when the suggestions box is tapped outside. Used to detect - /// when the suggestions box is tapped outside so that the suggestions box can - /// be closed. Same as the onTapInside callback function. - final Function(PointerEvent) onTapOutside; - - /// The height of the suggestions box. This param is the same as the one in - /// the `StandardSearchBar` class. - final double boxHeight; - - /// The padding of the suggestions box list tiles. This param is the same as - /// the one in the `StandardSearchBar` class. - final EdgeInsetsGeometry boxPadding; - - /// The text style of the suggestions. This param is the same as the one in - /// the `StandardSearchBar` class. - final TextStyle suggestionTextStyle; - - @override - Widget build(BuildContext context) { - return TapRegion( - onTapOutside: onTapOutside, - onTapInside: onTapInside, - child: Container( - height: boxHeight, - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(borderRadius), - bottomRight: Radius.circular(borderRadius), - ), - ), - child: Padding( - padding: const EdgeInsets.only(bottom: 22), - child: Material( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(borderRadius), - bottomRight: Radius.circular(borderRadius), - ), - color: Colors.transparent, - child: ListView.builder( - itemCount: suggestions.length, - itemBuilder: (context, index) { - return InkWell( - onTap: () => onSuggestionSelected(suggestions[index]), - child: Container( - padding: boxPadding, - child: Text( - suggestions[index], - style: suggestionTextStyle, - ), - ), - ); - }, - ), - ), - ), - ), - ); - } -} diff --git a/lib/standard_text_field.dart b/lib/standard_text_field.dart deleted file mode 100644 index ae6445a..0000000 --- a/lib/standard_text_field.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:flutter/material.dart'; - -class StandardTextField extends StatelessWidget { - const StandardTextField({ - super.key, - required this.showEndIcon, - required this.endIconPaddingLeft, - required this.hintText, - required this.hintStyle, - required this.cursorColor, - required this.textStyle, - this.controller, - this.onSubmitted, - this.onChanged, - required this.horizontalPadding, - required this.updateSuggestions, - }); - - final bool showEndIcon; - final double endIconPaddingLeft; - final String hintText; - final TextStyle hintStyle; - final Color cursorColor; - final TextStyle textStyle; - final TextEditingController? controller; - final void Function(String)? onSubmitted; - final void Function(String)? onChanged; - final double horizontalPadding; - final Function(String) updateSuggestions; - - @override - Widget build(BuildContext context) { - return Padding( - padding: showEndIcon - ? EdgeInsets.zero - : EdgeInsets.only( - right: endIconPaddingLeft, - ), - child: TextField( - controller: controller, - decoration: InputDecoration( - border: InputBorder.none, - hintText: hintText, - hintStyle: hintStyle, - ), - cursorColor: cursorColor, - style: textStyle, - onSubmitted: onSubmitted, - onChanged: (value) { - updateSuggestions(value); - if (onChanged != null) { - onChanged!(value); - } - }, - ), - ); - } -}