diff --git a/example/lib/main.dart b/example/lib/main.dart index 80b868cec..81c99b865 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -8,12 +8,34 @@ void main() { runApp(Home()); } -class Home extends StatelessWidget { +class Home extends StatefulWidget { const Home({Key? key}) : super(key: key); + @override + State createState() => _HomeState(); +} + +class _HomeState extends State { + final _filteredItems = []; + final _searchController = TextEditingController(); + + void _onEscape() => setState(() { + _filteredItems.clear(); + _searchController.clear(); + }); + + void _onSearchChanged(String value) { + setState(() { + _filteredItems.clear(); + _filteredItems.addAll(examplePageItems.where((element) => + element.title.toLowerCase().contains(value.toLowerCase()))); + }); + } + @override Widget build(BuildContext context) { return MaterialApp( + debugShowCheckedModeBanner: false, theme: yaruLight, darkTheme: yaruDark, home: YaruMasterDetailPage( @@ -21,7 +43,19 @@ class Home extends StatelessWidget { previousIconData: YaruIcons.go_previous, searchHint: 'Search...', searchIconData: YaruIcons.search, - pageItems: examplePageItems, + clearSearchIconData: YaruIcons.window_close, + pageItems: + _filteredItems.isNotEmpty ? _filteredItems : examplePageItems, + appBar: YaruSearchAppBar( + searchHint: 'Search...', + clearSearchIconData: YaruIcons.window_close, + searchController: _searchController, + onChanged: _onSearchChanged, + onEscape: _onEscape, + appBarHeight: 48, + searchIconData: YaruIcons.search, + automaticallyImplyLeading: false, + ), ), ); } diff --git a/lib/src/yaru_landscape_layout.dart b/lib/src/yaru_landscape_layout.dart index c46d310c8..4e6ad9430 100644 --- a/lib/src/yaru_landscape_layout.dart +++ b/lib/src/yaru_landscape_layout.dart @@ -1,38 +1,35 @@ import 'package:flutter/material.dart'; import 'package:yaru_widgets/src/yaru_page_item_list_view.dart'; -import 'package:yaru_widgets/src/yaru_search_app_bar.dart'; + import 'yaru_page_item.dart'; class YaruLandscapeLayout extends StatefulWidget { - // Creates a landscape layout + /// Creates a landscape layout const YaruLandscapeLayout({ Key? key, required this.selectedIndex, - required this.pages, + required this.pageItems, required this.onSelected, required this.leftPaneWidth, - this.searchIconData, - this.searchHint, + this.appBar, }) : super(key: key); - // Current index of the selected page. + /// Current index of the selected page. final int selectedIndex; - // Creates horizontal array of pages. - // All the `children` will be of type [YaruPageItem] - final List pages; + /// Creates horizontal array of pages. + /// All the `children` will be of type [YaruPageItem] + final List pageItems; - // Callback that returns an index when the page changes. + /// Callback that returns an index when the page changes. final ValueChanged onSelected; - // Specifies the width of left pane. + /// Specifies the width of left pane. final double leftPaneWidth; - // The icon that is given to the search widget. - final IconData? searchIconData; - - // The hint text given to the search widget. - final String? searchHint; + /// An optional [PreferredSizeWidget] used as the left [AppBar] + /// If provided, a second [AppBar] will be created right to it. + final PreferredSizeWidget? appBar; @override State createState() => _YaruLandscapeLayoutState(); @@ -40,22 +37,14 @@ class YaruLandscapeLayout extends StatefulWidget { class _YaruLandscapeLayoutState extends State { late int _selectedIndex; - late TextEditingController _searchController; - final _filteredItems = []; @override void initState() { _selectedIndex = widget.selectedIndex; - _searchController = TextEditingController(); super.initState(); } - void onTap(int index) { - if (_filteredItems.isNotEmpty) { - index = widget.pages.indexOf(widget.pages.firstWhere( - (pageItem) => pageItem.title == _filteredItems[index].title)); - } - + void _onTap(int index) { widget.onSelected(index); setState(() => _selectedIndex = index); } @@ -66,19 +55,23 @@ class _YaruLandscapeLayoutState extends State { body: Column( children: [ SizedBox( - height: - Theme.of(context).appBarTheme.toolbarHeight ?? kToolbarHeight, + height: widget.appBar != null + ? Theme.of(context).appBarTheme.toolbarHeight ?? kToolbarHeight + : 0, child: Row( children: [ SizedBox( width: widget.leftPaneWidth, - child: addSearchBar(), + child: widget.appBar, ), - Expanded( - child: AppBar( - title: Text(widget.pages[_selectedIndex].title), - ), - ) + if (widget.appBar != null) + Expanded( + child: AppBar( + title: widget.pageItems.length > _selectedIndex + ? Text(widget.pageItems[_selectedIndex].title) + : Text(widget.pageItems[0].title), + ), + ) ], ), ), @@ -99,15 +92,15 @@ class _YaruLandscapeLayoutState extends State { ), ), child: YaruPageItemListView( - selectedIndex: _selectedIndex, - onTap: onTap, - pages: _filteredItems.isEmpty - ? widget.pages - : _filteredItems, - ), + selectedIndex: _selectedIndex, + onTap: _onTap, + pages: widget.pageItems), ), ), - Expanded(child: widget.pages[_selectedIndex].builder(context)), + Expanded( + child: widget.pageItems.length > _selectedIndex + ? widget.pageItems[_selectedIndex].builder(context) + : widget.pageItems[0].builder(context)), ], ), ), @@ -115,31 +108,4 @@ class _YaruLandscapeLayoutState extends State { ), ); } - - YaruSearchAppBar addSearchBar() { - return YaruSearchAppBar( - automaticallyImplyLeading: false, - searchHint: widget.searchHint, - searchController: _searchController, - onChanged: (value) { - setState(() { - _filteredItems.clear(); - for (YaruPageItem pageItem in widget.pages) { - if (pageItem.title - .toLowerCase() - .contains(_searchController.value.text.toLowerCase())) { - _filteredItems.add(pageItem); - } - } - }); - }, - onEscape: () => setState(() { - _searchController.clear(); - _filteredItems.clear(); - }), - appBarHeight: - Theme.of(context).appBarTheme.toolbarHeight ?? kToolbarHeight, - searchIconData: widget.searchIconData, - ); - } } diff --git a/lib/src/yaru_master_detail_page.dart b/lib/src/yaru_master_detail_page.dart index 7159ff802..732aad2c6 100644 --- a/lib/src/yaru_master_detail_page.dart +++ b/lib/src/yaru_master_detail_page.dart @@ -16,8 +16,6 @@ class YaruMasterDetailPage extends StatefulWidget { /// appBarHeight: 48, /// leftPaneWidth: 280, /// previousIconData: YaruIcons.go_previous, - /// searchHint: 'Search...', - /// searchIconData: YaruIcons.search, /// pageItems: pageItems, /// ); /// ``` @@ -28,6 +26,8 @@ class YaruMasterDetailPage extends StatefulWidget { this.searchIconData, required this.leftPaneWidth, this.searchHint, + this.clearSearchIconData, + this.appBar, }) : super(key: key); /// Creates horizontal array of pages. @@ -45,9 +45,15 @@ class YaruMasterDetailPage extends StatefulWidget { /// The icon that is given to the search widget. final IconData? searchIconData; + /// Search icon for search bar. + final IconData? clearSearchIconData; + /// The hint text given to the search widget. final String? searchHint; + /// An optional custom AppBar for the left pane. + final PreferredSizeWidget? appBar; + @override _YaruMasterDetailPageState createState() => _YaruMasterDetailPageState(); } @@ -68,20 +74,18 @@ class _YaruMasterDetailPageState extends State { if (constraints.maxWidth < 620) { return YaruPortraitLayout( selectedIndex: _index, - pages: widget.pageItems, + pageItems: widget.pageItems, onSelected: _setIndex, previousIconData: widget.previousIconData, - searchIconData: widget.searchIconData, - searchHint: widget.searchHint, + appBar: widget.appBar, ); } else { return YaruLandscapeLayout( selectedIndex: _index == -1 ? _previousIndex : _index, - pages: widget.pageItems, + pageItems: widget.pageItems, onSelected: _setIndex, leftPaneWidth: widget.leftPaneWidth, - searchIconData: widget.searchIconData, - searchHint: widget.searchHint, + appBar: widget.appBar, ); } }, diff --git a/lib/src/yaru_portrait_layout.dart b/lib/src/yaru_portrait_layout.dart index bcdc030a0..12b659b55 100644 --- a/lib/src/yaru_portrait_layout.dart +++ b/lib/src/yaru_portrait_layout.dart @@ -1,26 +1,24 @@ import 'package:flutter/material.dart'; import 'package:yaru_widgets/src/yaru_page_item_list_view.dart'; -import 'package:yaru_widgets/src/yaru_search_app_bar.dart'; import 'yaru_page_item.dart'; class YaruPortraitLayout extends StatefulWidget { - const YaruPortraitLayout( - {Key? key, - required this.selectedIndex, - required this.pages, - required this.onSelected, - this.previousIconData, - this.searchIconData, - this.searchHint}) - : super(key: key); + const YaruPortraitLayout({ + Key? key, + required this.selectedIndex, + required this.pageItems, + required this.onSelected, + this.previousIconData, + this.appBar, + }) : super(key: key); final int selectedIndex; - final List pages; + final List pageItems; final ValueChanged onSelected; final IconData? previousIconData; - final IconData? searchIconData; - final String? searchHint; + + final PreferredSizeWidget? appBar; @override _YaruPortraitLayoutState createState() => _YaruPortraitLayoutState(); @@ -28,50 +26,54 @@ class YaruPortraitLayout extends StatefulWidget { class _YaruPortraitLayoutState extends State { late int _selectedIndex; - late TextEditingController _searchController; - final _filteredItems = []; final _navigatorKey = GlobalKey(); NavigatorState get _navigator => _navigatorKey.currentState!; @override void initState() { - _searchController = TextEditingController(); _selectedIndex = widget.selectedIndex; super.initState(); } - void onTap(int index) { - if (_filteredItems.isNotEmpty) { - index = widget.pages.indexOf(widget.pages.firstWhere( - (pageItem) => pageItem.title == _filteredItems[index].title)); - } - - _navigator.push(pageRoute(index)); + void _onTap(int index) { widget.onSelected(index); + _navigator.push(pageRoute(index)); setState(() => _selectedIndex = index); - _searchController.clear(); - _filteredItems.clear(); + } + + void _goBack() { + widget.onSelected(-1); + _navigator.pop(context); } MaterialPageRoute pageRoute(int index) { final width = MediaQuery.of(context).size.width; return MaterialPageRoute( builder: (context) { - final page = widget.pages[_selectedIndex]; + final page = widget.pageItems[_selectedIndex]; return Scaffold( - appBar: AppBar( - toolbarHeight: Theme.of(context).appBarTheme.toolbarHeight, - title: Text(page.title), - leading: InkWell( - child: Icon(widget.previousIconData ?? Icons.navigate_before), - onTap: () { - widget.onSelected(-1); - _navigator.pop(context); - }, - ), - ), + appBar: widget.appBar != null + ? AppBar( + toolbarHeight: Theme.of(context).appBarTheme.toolbarHeight, + title: Text(page.title), + leading: InkWell( + child: + Icon(widget.previousIconData ?? Icons.navigate_before), + onTap: _goBack, + ), + ) + : null, body: SizedBox(width: width, child: page.builder(context)), + floatingActionButton: widget.appBar == null + ? FloatingActionButton( + child: Icon(widget.previousIconData), + onPressed: _goBack, + ) + : null, + floatingActionButtonLocation: widget.appBar == null + ? FloatingActionButtonLocation.miniStartFloat + : null, ); }, ); @@ -88,13 +90,11 @@ class _YaruPortraitLayoutState extends State { MaterialPageRoute( builder: (context) { return Scaffold( - appBar: addSearchBar(), + appBar: widget.appBar, body: YaruPageItemListView( - selectedIndex: _selectedIndex, - onTap: onTap, - pages: - _filteredItems.isEmpty ? widget.pages : _filteredItems, - ), + selectedIndex: _selectedIndex, + onTap: _onTap, + pages: widget.pageItems), ); }, ), @@ -104,31 +104,4 @@ class _YaruPortraitLayoutState extends State { ), ); } - - YaruSearchAppBar addSearchBar() { - return YaruSearchAppBar( - searchHint: widget.searchHint, - searchController: _searchController, - onChanged: (value) { - setState(() { - _filteredItems.clear(); - for (YaruPageItem pageItem in widget.pages) { - if (pageItem.title - .toLowerCase() - .contains(_searchController.value.text.toLowerCase())) { - _filteredItems.add(pageItem); - } - } - }); - }, - onEscape: () => setState(() { - _searchController.clear(); - _filteredItems.clear(); - }), - appBarHeight: - Theme.of(context).appBarTheme.toolbarHeight ?? kToolbarHeight, - searchIconData: widget.searchIconData, - automaticallyImplyLeading: false, - ); - } } diff --git a/lib/src/yaru_search_app_bar.dart b/lib/src/yaru_search_app_bar.dart index 67f3ac3b5..b263f2269 100644 --- a/lib/src/yaru_search_app_bar.dart +++ b/lib/src/yaru_search_app_bar.dart @@ -12,7 +12,7 @@ class YaruSearchAppBar extends StatelessWidget implements PreferredSizeWidget { /// Vertical alignment of the [TextField] will be center. const YaruSearchAppBar({ Key? key, - required this.searchController, + this.searchController, required this.onChanged, required this.onEscape, required this.automaticallyImplyLeading, @@ -20,10 +20,11 @@ class YaruSearchAppBar extends StatelessWidget implements PreferredSizeWidget { required this.appBarHeight, this.textStyle, this.searchHint, + this.clearSearchIconData, }) : super(key: key); /// Pass a new [TextEditingController] instance. - final TextEditingController searchController; + final TextEditingController? searchController; /// The callback that gets invoked when the value changes in the text field. final Function(String value) onChanged; @@ -43,9 +44,11 @@ class YaruSearchAppBar extends StatelessWidget implements PreferredSizeWidget { /// Specifies the [TextStyle] final TextStyle? textStyle; - /// If false, hides the search icon in the [AppBar] + /// If false, hides the back icon in the [AppBar] final bool automaticallyImplyLeading; + final IconData? clearSearchIconData; + @override Widget build(BuildContext context) { final textColor = Theme.of(context).appBarTheme.foregroundColor; @@ -62,38 +65,41 @@ class YaruSearchAppBar extends StatelessWidget implements PreferredSizeWidget { focusNode: FocusNode(), child: SizedBox( height: appBarHeight, - child: Padding( - padding: const EdgeInsets.only(top: 4), - child: TextField( - expands: true, - maxLines: null, - minLines: null, - cursorColor: textColor, - textAlignVertical: TextAlignVertical.center, - style: textStyle ?? - Theme.of(context) - .appBarTheme - .titleTextStyle - ?.copyWith(fontWeight: FontWeight.w200, fontSize: 18), - decoration: InputDecoration( - prefixIcon: Padding( - padding: - const EdgeInsets.only(left: 28, right: 25, bottom: 7), - child: Icon( - searchIconData ?? Icons.search, - color: textColor, - ), - ), - hintText: searchHint, - enabledBorder: UnderlineInputBorder( - borderSide: - BorderSide(color: Colors.black.withOpacity(0.01))), - border: const UnderlineInputBorder(), + child: TextField( + expands: true, + maxLines: null, + minLines: null, + cursorColor: textColor, + textAlignVertical: TextAlignVertical.center, + style: textStyle ?? + Theme.of(context) + .appBarTheme + .titleTextStyle + ?.copyWith(fontWeight: FontWeight.w200, fontSize: 18), + decoration: InputDecoration( + contentPadding: const EdgeInsets.only(top: 6), + prefixIcon: Icon( + searchIconData ?? Icons.search, + color: textColor, + ), + prefixIconConstraints: BoxConstraints.expand( + width: appBarHeight, height: appBarHeight), + suffixIcon: InkWell( + child: + Icon(clearSearchIconData ?? Icons.close, color: textColor), + onTap: onEscape, ), - controller: searchController, - autofocus: true, - onChanged: (value) => onChanged(value), + suffixIconConstraints: BoxConstraints.expand( + width: appBarHeight, height: appBarHeight), + hintText: searchHint, + enabledBorder: UnderlineInputBorder( + borderSide: + BorderSide(color: Colors.black.withOpacity(0.01))), + border: const UnderlineInputBorder(), ), + controller: searchController, + autofocus: true, + onChanged: onChanged, ), ), ),