Flutter浮动搜索栏插件material_floating_search_bar的使用

发布于 1周前 作者 vueper 来自 Flutter

Flutter浮动搜索栏插件material_floating_search_bar的使用

Material Floating Search Bar GitHub Stars

Material Floating Search Bar 是一个 Flutter 实现的可扩展的浮动搜索栏,类似于广泛应用于 Google 应用程序中的持久搜索。

点击 这里 查看完整的示例。

安装

pubspec.yaml 文件中添加依赖:

dependencies:
  material_floating_search_bar: ^0.3.7

从命令行安装包:

flutter packages get

如果您喜欢这个包,请考虑在 GitHubpub.dev 上给它点赞 ❤️。

使用

FloatingSearchBar 应该放置在您的主内容上方,并允许其填充所有可用空间。

[@override](/user/override)
Widget build(BuildContext context) {
  return Scaffold(
    // 这由搜索栏本身处理。
    resizeToAvoidBottomInset: false,
    body: Stack(
      fit: StackFit.expand,
      children: [
        buildMap(),
        buildBottomNavigationBar(),
        buildFloatingSearchBar(),
      ],
    ),
  );
}

Widget buildFloatingSearchBar() {
  final isPortrait = MediaQuery.of(context).orientation == Orientation.portrait;

  return FloatingSearchBar(
    hint: '搜索...',
    scrollPadding: const EdgeInsets.only(top: 16, bottom: 56),
    transitionDuration: const Duration(milliseconds: 800),
    transitionCurve: Curves.easeInOut,
    physics: const BouncingScrollPhysics(),
    axisAlignment: isPortrait ? 0.0 : -1.0,
    openAxisAlignment: 0.0,
    width: isPortrait ? 600 : 500,
    debounceDelay: const Duration(milliseconds: 500),
    onQueryChanged: (query) {
      // 调用您的模型、小部件或控制器。
    },
    // 指定一个自定义过渡来在打开和关闭状态之间进行动画。
    transition: CircularFloatingSearchBarTransition(),
    actions: [
      FloatingSearchBarAction(
        showIfOpened: false,
        child: CircularButton(
          icon: const Icon(Icons.place),
          onPressed: () {},
        ),
      ),
      FloatingSearchBarAction.searchToClear(
        showIfClosed: false,
      ),
    ],
    builder: (context, transition) {
      return ClipRRect(
        borderRadius: BorderRadius.circular(8),
        child: Material(
          color: Colors.white,
          elevation: 4.0,
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: Colors.accents.map((color) {
              return Container(height: 112, color: color);
            }).toList(),
          ),
        ),
      );
    },
  );
}

带滚动视图的使用

默认情况下,builder 返回的小部件不允许具有无限高度。这是为了使搜索栏能够在用户点击子项区域下方时关闭(例如,当列表项不足以填满整个屏幕时)。

因此,应在所有滚动视图上将 shrinkWrap 设置为 true 并将 physics 设置为 NeverScrollableScrollPhysics。对于列,应将 mainAxisSize 设置为 MainAxisSize.min

如果您不希望这种行为,可以将 isScrollControlled 标志设置为 true。然后您可以使用展开小部件,如 Scrollables,但要注意搜索栏可能无法检测到背景区域的点击。

自定义

有许多定制选项:

字段 描述
body 显示在 FloatingSearchBar 下的小部件。
accentColor 用于进度指示器等元素的颜色。
backgroundColor 卡片的颜色。
shadowColor elevation > 0 时绘制的阴影颜色。
iconColor 覆盖主题图标颜色,以便轻松调整所有 actionsleadingActions 的图标颜色。
backdropColor 打开时填充可用空间的颜色。
margins 其父组件边缘的内边距。
padding 卡片的内边距。
insets leadingActions、输入字段和 actions 之间的内边距。
height 卡片的高度。
elevation 卡片的阴影。
width FloatingSearchBar 的宽度。
openWidth 打开时 FloatingSearchBar 的宽度。
axisAlignment 可用宽度大于 maxWidth 时,FloatingSearchBar 的对齐方式。
openAxisAlignment 可用宽度大于 openMaxWidth 时,FloatingSearchBar 的对齐方式。
border 卡片的边框。
borderRadius 卡片的圆角半径。
hintStyle TextField 中提示文本的样式。
queryStyle TextField 中输入文本的样式。
clearQueryOnClose 关闭时是否清除当前查询。
automaticallyImplyDrawerHamburger 是否显示汉堡菜单。
closeOnBackdropTap 点击背景时是否关闭 FloatingSearchBar
automaticallyImplyBackButton 是否自动显示返回按钮。
progress LinearProgressIndicator 的进度。
transitionDuration 打开和关闭状态之间动画的持续时间。
transitionCurve 打开和关闭状态之间动画的曲线。
debounceDelay 用户停止输入后调用 onQueryChanged 回调的延迟。
title TextField 关闭时显示的小部件。
hint TextField 提示文本的值。
actions TextField 后面显示的一组小部件。
leadingActions TextField 前面显示的一组小部件。
onQueryChanged TextField 输入变化时的回调。
onSubmitted 用户提交查询时的回调。
onFocusChanged FloatingSearchBar 获得或失去焦点时的回调。
transition 打开和关闭状态之间使用的过渡。
builder FloatingSearchBar 的主体构建器。
controller 控制此 FloatingSearchBar 的控制器。
isScrollControlled FloatingSearchBar 的主体是否使用自己的滚动视图。
initiallyHidden 初始是否隐藏搜索栏。

过渡效果

目前有三种类型的过渡效果:

过渡效果 描述
CircularFloatingSearchBarTransition 将其子节点剪裁在一个扩展的圆形中。
ExpandingFloatingSearchBarTransition FloatingSearchBar 的背景填充所有可用空间。
SlideFadeFloatingSearchBarTransition 垂直滑动并淡出其子节点。

您可以轻松地通过扩展 FloatingSearchBarTransition 来创建自己的自定义过渡效果。

滚动

浮动搜索栏的一个常见行为是在用户向下滚动 Scrollable 时消失,并在向上滚动时重新出现。这可以通过将您的小部件传递给 FloatingSearchBarbody 字段轻松实现。这样 FloatingSearchBar 可以监听 ScrollNotifications。为了使 FloatingSearchBar 不与小部件树中的其他 Scrollable 交互,应该在应与 FloatingSearchBar 交互的每个 Scrollable 上包装一个 FloatingSearchBarScrollNotifier

class Home extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return FloatingSearchBar(
      // 你的页面或者简单的 Scaffold...
      body: IndexedStack(
        children: [
          MyAwesomePage(),
        ],
      ),
    );
  }
}

class MyAwesomePage extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    /// 包装你的 Scrollable 在 FloatingSearchBarScrollNotifier 中,
    /// 表示 FloatingSearchBar 应该对这个 Scrollable 的滚动事件做出反应。
    return FloatingSearchBarScrollNotifier(
      child: ListView.builder(
        itemCount: 42,
        itemBuilder: (_, index) => Item('Item $index'),
      ),
    );
  }
}

FloatingSearchBarController

FloatingSearchBarController 可用于控制 FloatingSearchBar

方法 描述
open() 展开 FloatingSearchBar
close() 关闭 FloatingSearchBar
show() 当之前被隐藏时显示 FloatingSearchBar
hide() 视觉上隐藏 FloatingSearchBar(滑出屏幕)。
query 设置 InputField 中查询的输入。
clear() 清除查询。

Floating Search App Bar

有时,FloatingSearchBar 对于您的使用场景可能不是最合适的搜索方法。因此,还有一个 FloatingSearchAppBar。它是一个具有简单搜索集成的正常 AppBar,非常类似于标准的 FloatingSearchBar

除了大多数来自 FloatingSearchBar 的字段外,FloatingSearchAppBar 还有以下附加字段:

字段 描述
colorOnScroll Scrollablebody 中滚动时(即 Scrollable 不在顶部)的条形颜色。
liftOnScrollElevation Scrollablebody 中滚动时(即 Scrollable 不在顶部)的条形阴影。
alwaysOpened 是否始终处于打开状态。
hideKeyboardOnDownScroll 如果 Scrollablebody 中滚动,则隐藏键盘并在用户滚动到顶部时再次显示。

完整示例

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:implicitly_animated_reorderable_list/implicitly_animated_reorderable_list.dart';
import 'package:implicitly_animated_reorderable_list/transitions.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:material_floating_search_bar/material_floating_search_bar.dart';
import 'package:provider/provider.dart';

import 'place.dart';
import 'search_model.dart';

void main() {
  SystemChrome.setSystemUIOverlayStyle(
    const SystemUiOverlayStyle(
      systemNavigationBarColor: Colors.white,
    ),
  );

  runApp(
    MaterialApp(
      title: 'Material Floating Search Bar Example',
      debugShowCheckedModeBanner: false,
      theme: ThemeData.light().copyWith(
        iconTheme: const IconThemeData(
          color: Color(0xFF4d4d4d),
        ),
        floatingActionButtonTheme: const FloatingActionButtonThemeData(
          elevation: 4,
        ),
      ),
      home: Directionality(
        textDirection: TextDirection.ltr,
        child: ChangeNotifierProvider(
          create: (_) => SearchModel(),
          child: const Home(),
        ),
      ),
    ),
  );
}

class MyApp extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Home(),
    );
  }
}

class Home extends StatefulWidget {
  const Home({Key? key}) : super(key: key);

  [@override](/user/override)
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  final controller = FloatingSearchBarController();

  int _index = 0;
  int get index => _index;
  set index(int value) {
    _index = min(value, 2);
    _index == 2 ? controller.hide() : controller.show();
    setState(() {});
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomInset: false,
      drawer: Drawer(
        child: Container(
          width: 200,
        ),
      ),
      body: buildSearchBar(),
    );
  }

  Widget buildSearchBar() {
    final actions = [
      FloatingSearchBarAction(
        showIfOpened: false,
        child: CircularButton(
          icon: const Icon(Icons.place),
          onPressed: () {},
        ),
      ),
      FloatingSearchBarAction.searchToClear(
        showIfClosed: false,
      ),
    ];

    final isPortrait = MediaQuery.of(context).orientation == Orientation.portrait;

    return Consumer<SearchModel>(
      builder: (context, model, _) => FloatingSearchBar(
        automaticallyImplyBackButton: false,
        controller: controller,
        clearQueryOnClose: true,
        hint: '搜索...',
        iconColor: Colors.grey,
        transitionDuration: const Duration(milliseconds: 800),
        transitionCurve: Curves.easeInOutCubic,
        physics: const BouncingScrollPhysics(),
        axisAlignment: isPortrait ? 0.0 : -1.0,
        openAxisAlignment: 0.0,
        actions: actions,
        progress: model.isLoading,
        debounceDelay: const Duration(milliseconds: 500),
        onQueryChanged: model.onQueryChanged,
        onKeyEvent: (KeyEvent keyEvent) {
          if (keyEvent.logicalKey == LogicalKeyboardKey.escape) {
            controller.query = "";
            controller.close();
          }
        },
        scrollPadding: EdgeInsets.zero,
        transition: CircularFloatingSearchBarTransition(spacing: 16),
        builder: (context, _) => buildExpandableBody(model),
        body: buildBody(),
      ),
    );
  }

  Widget buildBody() {
    return Column(
      children: [
        Expanded(
          child: IndexedStack(
            index: min(index, 2),
            children: const [
              Map(),
              SomeScrollableContent(),
              FloatingSearchAppBarExample(),
            ],
          ),
        ),
        buildBottomNavigationBar(),
      ],
    );
  }

  Widget buildExpandableBody(SearchModel model) {
    return Container(
      padding: const EdgeInsets.symmetric(vertical: 16),
      child: Material(
        color: Colors.white,
        borderRadius: BorderRadius.circular(8),
        clipBehavior: Clip.antiAlias,
        child: ImplicitlyAnimatedList<Place>(
          shrinkWrap: true,
          physics: const NeverScrollableScrollPhysics(),
          items: model.suggestions,
          insertDuration: const Duration(milliseconds: 700),
          itemBuilder: (context, animation, item, i) {
            return SizeFadeTransition(
              animation: animation,
              child: buildItem(context, item),
            );
          },
          updateItemBuilder: (context, animation, item) {
            return FadeTransition(
              opacity: animation,
              child: buildItem(context, item),
            );
          },
          areItemsTheSame: (a, b) => a == b,
        ),
      ),
    );
  }

  Widget buildItem(BuildContext context, Place place) {
    final theme = Theme.of(context);
    final textTheme = theme.textTheme;

    final model = Provider.of<SearchModel>(context, listen: false);

    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        InkWell(
          onTap: () {
            FloatingSearchBar.of(context)?.close();
            Future.delayed(
              const Duration(milliseconds: 500),
              () => model.clear(),
            );
          },
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              children: [
                SizedBox(
                  width: 36,
                  child: AnimatedSwitcher(
                    duration: const Duration(milliseconds: 500),
                    child: model.suggestions == history
                        ? const Icon(Icons.history, key: Key('history'))
                        : const Icon(Icons.place, key: Key('place')),
                  ),
                ),
                const SizedBox(width: 16),
                Expanded(
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        place.name,
                        style: textTheme.subtitle1,
                      ),
                      const SizedBox(height: 2),
                      Text(
                        place.level2Address,
                        style: textTheme.bodyText2?.copyWith(color: Colors.grey.shade600),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
        if (model.suggestions.isNotEmpty && place != model.suggestions.last)
          const Divider(height: 0),
      ],
    );
  }

  Widget buildBottomNavigationBar() {
    return BottomNavigationBar(
      onTap: (value) => index = value,
      currentIndex: index,
      elevation: 16,
      type: BottomNavigationBarType.fixed,
      showUnselectedLabels: true,
      backgroundColor: Colors.white,
      selectedItemColor: Colors.blue,
      selectedFontSize: 11.5,
      unselectedFontSize: 11.5,
      unselectedItemColor: const Color(0xFF4d4d4d),
      items: const [
        BottomNavigationBarItem(
          icon: Icon(MdiIcons.homeVariantOutline),
          label: 'Explore',
        ),
        BottomNavigationBarItem(
          icon: Icon(MdiIcons.homeCityOutline),
          label: 'Commute',
        ),
        BottomNavigationBarItem(
          icon: Icon(MdiIcons.bookmarkOutline),
          label: 'Saved',
        ),
        BottomNavigationBarItem(
          icon: Icon(MdiIcons.plusCircleOutline),
          label: 'Contribute',
        ),
        BottomNavigationBarItem(
          icon: Icon(MdiIcons.bellOutline),
          label: 'Updates',
        ),
      ],
    );
  }

  [@override](/user/override)
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

class Map extends StatelessWidget {
  const Map({Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Stack(
      fit: StackFit.expand,
      children: [
        buildMap(),
        buildFabs(),
      ],
    );
  }

  Widget buildFabs() {
    return Align(
      alignment: AlignmentDirectional.bottomEnd,
      child: Padding(
        padding: const EdgeInsetsDirectional.only(bottom: 16, end: 16),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Builder(
              builder: (context) => FloatingActionButton(
                onPressed: () {
                  Navigator.push(
                    context,
                    MaterialPageRoute(
                      builder: (context) => SearchBar(),
                    ),
                  );
                },
                backgroundColor: Colors.white,
                child: const Icon(Icons.gps_fixed, color: Color(0xFF4d4d4d)),
              ),
            ),
            const SizedBox(height: 16),
            FloatingActionButton(
              onPressed: () {},
              heroTag: "öslkföl",
              backgroundColor: Colors.blue,
              child: const Icon(Icons.directions),
            ),
          ],
        ),
      ),
    );
  }

  Widget buildMap() {
    return Image.asset(
      'assets/map.jpg',
      fit: BoxFit.cover,
    );
  }
}

class SearchBar extends StatefulWidget {
  const SearchBar({
    Key? key,
  }) : super(key: key);

  [@override](/user/override)
  _SearchBarState createState() => _SearchBarState();
}

class _SearchBarState extends State<SearchBar> {
  final FloatingSearchBarController controller = FloatingSearchBarController();

  [@override](/user/override)
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      body: FloatingSearchBar(
        controller: controller,
        title: Text(
          "Aschaffenburg",
        ),
        hint: '搜索一个地点',
        builder: (context, _) {
          return Container();
        },
      ),
    );
  }
}

class SomeScrollableContent extends StatelessWidget {
  const SomeScrollableContent({Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return FloatingSearchBarScrollNotifier(
      child: ListView.separated(
        padding: const EdgeInsets.only(top: kToolbarHeight),
        itemCount: 100,
        separatorBuilder: (context, index) => const Divider(),
        itemBuilder: (context, index) {
          return ListTile(
            title: Text('Item $index'),
          );
        },
      ),
    );
  }
}

class FloatingSearchAppBarExample extends StatelessWidget {
  const FloatingSearchAppBarExample({Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return FloatingSearchAppBar(
      title: const Text('Title'),
      transitionDuration: const Duration(milliseconds: 800),
      color: Colors.greenAccent.shade100,
      colorOnScroll: Colors.greenAccent.shade200,
      body: ListView.separated(
        padding: EdgeInsets.zero,
        itemCount: 100,
        separatorBuilder: (context, index) => const Divider(),
        itemBuilder: (context, index) {
          return ListTile(
            title: Text('Item $index'),
          );
        },
      ),
    );
  }
}

更多关于Flutter浮动搜索栏插件material_floating_search_bar的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter浮动搜索栏插件material_floating_search_bar的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是如何在Flutter项目中使用material_floating_search_bar插件的一个示例代码。这个插件允许你在应用中实现一个浮动搜索栏,非常适合需要搜索功能的界面。

首先,确保你的pubspec.yaml文件中已经添加了material_floating_search_bar的依赖:

dependencies:
  flutter:
    sdk: flutter
  material_floating_search_bar: ^4.2.0  # 请检查最新版本号

然后运行flutter pub get来安装依赖。

下面是一个简单的示例,展示如何使用material_floating_search_bar插件:

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Floating Search Bar Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  final List<String> suggestions = List.generate(20, (i) => "Item ${i + 1}");
  String? searchQuery = "";

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Floating Search Bar Demo"),
      ),
      body: FloatingSearchBar(
        // 搜索栏的控制器
        controller: searchQueryController,
        // 搜索栏的提示文本
        hint: "Search...",
        // 搜索栏的初始值
        initialValue: searchQuery,
        // 搜索栏变化时的回调
        onChanged: (value) {
          setState(() {
            searchQuery = value;
          });
          // 这里可以处理搜索逻辑,例如过滤列表
        },
        // 搜索栏提交时的回调
        onSubmitted: (value) {
          setState(() {
            searchQuery = value;
          });
          // 这里可以处理搜索结果的提交逻辑
        },
        // 搜索栏的搜索结果列表
        leadingActions: [
          FloatingSearchBarAction(
            icon: Icons.clear,
            onPressed: () => setState(() {
              searchQueryController.clear();
              searchQuery = "";
            }),
          ),
        ],
        trailingActions: [
          FloatingSearchBarAction.searchToClear(
            showIfOpened: true,
          ),
        ],
        // 搜索栏下方的主体内容
        builder: (context, scrollController) => CustomScrollView(
          controller: scrollController,
          slivers: <Widget>[
            SliverList(
              delegate: SliverChildBuilderDelegate(
                (context, index) {
                  if (searchQuery == null || searchQuery!.isEmpty) {
                    return ListTile(
                      title: Text(suggestions[index]),
                    );
                  } else {
                    if (suggestions[index].toLowerCase().contains(searchQuery!.toLowerCase())) {
                      return ListTile(
                        title: Text(suggestions[index]),
                      );
                    }
                  }
                  return null; // 不匹配的项不会被渲染
                },
                childCount: suggestions.length,
              ),
            ),
          ],
        ),
        // 搜索栏的过渡动画
        transitionDuration: const Duration(milliseconds: 300),
        transitionCurve: Curves.easeInOut,
        // 搜索栏打开时的背景颜色
        openAxisAlignment: 0.5,
        physics: const BouncingScrollPhysics(),
      ),
    );
  }

  // 搜索栏的控制器
  final TextEditingController searchQueryController = TextEditingController();

  @override
  void dispose() {
    searchQueryController.dispose();
    super.dispose();
  }
}

在这个示例中,我们创建了一个简单的搜索界面,其中FloatingSearchBar用于处理搜索栏的显示和交互。搜索栏会根据用户输入的内容动态过滤列表中的项目。我们还添加了清除搜索栏的按钮以及搜索图标。

注意:

  • searchQueryController用于管理搜索栏的文本输入。
  • onChangedonSubmitted回调用于处理搜索文本的变化和提交。
  • builder参数用于构建搜索栏下方的主体内容,这里我们使用了CustomScrollView来显示一个列表。

希望这个示例对你有所帮助!

回到顶部