🔎

SearchBar class

2024/12/14に公開

マテリアルデザインの検索バー

https://api.flutter.dev/flutter/material/SearchBar-class.html
https://www.youtube.com/watch?v=vM2dC8OCZoY

SearchBar は TextField のように見えます。検索バーをタップすると、通常「検索ビュー」ルートが表示されます。SearchBarは通常、SearchAnchor.builderによって作成されます。builderはSearchControllerを提供し、検索バーのSearchBar.onTapまたはSearchBar.onChangedコールバックで使用され、検索ビューを表示したり、ユーザが候補を選択したときに非表示にしたりします。

TextDirection.ltrの場合、先頭のウィジェットはバーの左側にあります。これは、ナビゲーショナル・アクション(メニューや上向き矢印など)か、機能しない検索アイコンを含む必要があります。

末尾は検索バーの反対側に表示されるオプションのリストです。通常、1つか2つのアクションアイコンだけが含まれる。これらのアクションは、追加の検索モード(音声検索のような)、別の高レベルのアクション(現在地など)、またはオーバーフローメニューを表すことができます。

作成するときは、オプションで先頭のアイコンまたはヒントテキストを指定できます。その後検索バーがアプリのデザインに一致するように、多数のプロパティを使用してさらにカスタマイズできます。たとべばbackgroundColor、shadowColor、標高、形状、パディングなどです。

SearchBar(
              backgroundColor:  WidgetStateProperty.all(Colors.blue),
              shadowColor: WidgetStateProperty.all(Colors.black),
              elevation: WidgetStateProperty.all(5.0),
              shape: WidgetStateProperty.all(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20.0))),
              padding: WidgetStateProperty.all(const EdgeInsets.symmetric(horizontal: 8.0)),
              leading: const Icon(Icons.search)
            )

SearchBarはユーザーが操作できるコールバックも提供します。onChangeコールバックは、検索バーのテキストが変更されるたびに呼び出されます。

SearchBar(
              onChanged: (value) {
                // update UI the query
              },
            )

onSubmittedは、ユーザーがEnterキーを押すなど、検索クエリを送信したときに呼び出されます。

SearchBar(
              onSubmitted: (value) {
                // update UI the query
              },
            )

onTapは、ユーザーが検索バーをタップするとアクティブになります。

SearchBar(
              onTap: () {
                // update UI the query
              },
            )

onTapOutsideは、検索バーにフォーカスがあり、ユーザーがその外側をタップしたときにアクティブになります。

SearchBar(
              onTapOutside: (event) {
                
              },
            )

検索候補を表示するにはどうすればいいのか?
SearchAnchorを使用します。検索候補を表示するには、suggestionsBuilderを表示します。

SearchAnchor(
            builder: (BuildContext context, SearchController controller) {
          return SearchBar(
            controller: controller,
            padding: const WidgetStatePropertyAll<EdgeInsets>(
                EdgeInsets.symmetric(horizontal: 16.0)),
            onTap: () {
              controller.openView();
            },
            onChanged: (_) {
              controller.openView();
            },
            leading: const Icon(Icons.search),
            trailing: <Widget>[
              Tooltip(
                message: 'Change brightness mode',
              )
            ],
          );
        }, suggestionsBuilder:
                (BuildContext context, SearchController controller) {
          return List.generate(carts.length, (index) {
            final String item = carts.values.toList()[index];
            return ListTile(
              title: Row(
                spacing: 10,
                children: [
                  Icon(Icons.shopping_cart, color: Colors.blue),
                  Text(item),
                ],
              ),
              onTap: () {
                setState(() {
                  controller.closeView(item);
                });
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => DetailPage(item: item),
                  ),
                );
              },
            );
          });
        }),

example

ダミーのスニーカーのショッピングカートをイメージして作ってみた。良いコードではないかもしれないが。参考までに。公式のサンプルだと物足りなくて😅

https://www.youtube.com/shorts/rkp_-uuDfHE

sample
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        appBarTheme: const AppBarTheme(
          backgroundColor: Colors.orange,
        ),
        useMaterial3: true,
      ),
      home: const SearchBarApp(),
    );
  }
}

class SearchBarApp extends StatefulWidget {
  const SearchBarApp({super.key});

  
  State<SearchBarApp> createState() => _SearchBarAppState();
}

class _SearchBarAppState extends State<SearchBarApp> {
  bool isDark = false;

  Map<String, dynamic> carts = {
    'shoes1': 'ADIDAS',
    'shoes2': 'PUMA',
    'shoes3': 'NIKE',
    'shoes4': 'CONVERSE',
    'shoes5': 'VANS',
    'shoes6': 'NIKE',
    'shoes7': 'CONVERSE',
    'shoes8': 'VANS',
    'shoes9': 'NIKE',
    'shoes10': 'CONVERSE',
    'shoes11': 'VANS',
    'shoes12': 'NIKE',
    'shoes13': 'CONVERSE',
    'shoes14': 'VANS',
    'shoes15': 'NIKE',
    'shoes16': 'CONVERSE',
    'shoes17': 'VANS',
    'shoes18': 'NIKE',
    'shoes19': 'CONVERSE',
    'shoes20': 'VANS',
  };

  final SearchController controller = SearchController();

  
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Search Bar Demo')),
      body: ListView(
        children: [
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: SearchAnchor(
                builder: (BuildContext context, SearchController controller) {
              return SearchBar(
                controller: controller,
                padding: const WidgetStatePropertyAll<EdgeInsets>(
                    EdgeInsets.symmetric(horizontal: 16.0)),
                onTap: () {
                  controller.openView();
                },
                onChanged: (_) {
                  controller.openView();
                },
                leading: const Icon(Icons.search),
                trailing: <Widget>[
                  Tooltip(
                    message: 'Change brightness mode',
                  )
                ],
              );
            }, suggestionsBuilder:
                    (BuildContext context, SearchController controller) {
              final String searchQuery = controller.text.toLowerCase();
              final filteredItems = carts.values
                  .where((item) => item.toLowerCase().contains(searchQuery))
                  .toList();
              return List<Widget>.generate(filteredItems.length, (index) {
                final String item = filteredItems[index];
                return ListTile(
                  title: Row(
                    children: [
                      Icon(Icons.shopping_cart, color: Colors.blue),
                      const SizedBox(width: 10),
                      Text(item),
                    ],
                  ),
                  onTap: () {
                    setState(() {
                      controller.closeView(item);
                    });
                    Navigator.push(
                      context,
                      MaterialPageRoute(
                        builder: (context) => DetailPage(item: item),
                      ),
                    );
                  },
                );
              });
            }),
          ),
          ...carts.entries.map((e) => ListTile(
                title: Row(
                  children: [
                    Icon(Icons.shopping_cart, color: Colors.blue),
                    Text(e.value),
                  ],
                ),
                onTap: () {
                  Navigator.push(
                    context,
                    MaterialPageRoute(
                      builder: (context) => DetailPage(item: e.value),
                    ),
                  );
                },
              )),
        ],
      ),
    );
  }
}

class DetailPage extends StatelessWidget {
  final String item;

  const DetailPage({super.key, required this.item});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('$item Details'),
      ),
      body: Center(
        child: ListView(
          children: [
            ListTile(title: Text(item)),
            ListTile(title: Text("2,500 JPY")),
          ],
        ),
      ),
    );
  }
}

感想

SwiftUIで以前マップアプリを作っていたときに同じようなことをしたことありました。SearchBarは昔はなかったのか自作していたようだ?パッケージもあるみたいですけどね。
https://pub.dev/packages/standard_searchbar

Discussion