Flutter搜索界面插件flutter_searchbox_ui的使用

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

Flutter搜索界面插件flutter_searchbox_ui的使用

flutter_searchbox_ui 提供了用于Elasticsearch和Appbase.io的UI组件,支持多种类型的查询。

目前,我们支持 [RangeInput][ReactiveGoogleMap] 组件。

安装

要安装 flutter_searchbox_ui,请按以下步骤操作:

  1. 添加依赖

    在你的项目 pubspec.yaml 文件中添加以下依赖:

    dependencies:
      flutter_searchbox: ^3.1.0
      searchbase: ^3.4.0-beta
      flutter_searchbox_ui: 1.0.16-alpha
    
  2. 获取依赖

    你可以在命令行中运行以下命令来获取这些包:

    $ flutter pub get
    
  3. 使用 [ReactiveGoogleMap]

    如果你要使用 [ReactiveGoogleMap],请参阅此处的安装指南: google_maps_flutter

基本用法

[ReactiveGoogleMap] 示例与 [RangeInput]

import 'package:flutter/material.dart';
import 'package:searchbase/searchbase.dart';
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart';
import 'dart:async';
import 'dart:ui';
import 'package:flutter_searchbox/flutter_searchbox.dart';
import 'package:flutter_searchbox_ui/flutter_searchbox_ui.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:dart_geohash/dart_geohash.dart';
import 'package:google_maps_cluster_manager/google_maps_cluster_manager.dart';
import 'dart:io';

void main() {
  FlutterError.onError = (FlutterErrorDetails details) {
    FlutterError.presentError(details);
    if (kReleaseMode) exit(1);
  };
  runApp(FlutterSearchBoxUIApp());
}

class FlutterSearchBoxUIApp extends StatelessWidget {
  // 避免在构建方法中创建 searchbase 实例
  // 以保持热重载时的状态
  final searchbaseInstance = SearchBase(
      'earthquakes',
      'https://appbase-demo-ansible-abxiydt-arc.searchbase.io',
      'a03a1cb71321:75b6603d-9456-4a5a-af6b-a487b309eb61',
      appbaseConfig: AppbaseSettings(
          recordAnalytics: true,
          // 使用唯一的用户ID来个性化最近的搜索
          userId: 'jon@appbase.io'));

  FlutterSearchBoxUIApp({Key? key}) : super(key: key);

  // 构建聚合图标的方法
  Future<BitmapDescriptor> _getMarkerBitmap(int size, {String? text}) async {
    if (kIsWeb) size = (size / 2).floor();

    final PictureRecorder pictureRecorder = PictureRecorder();
    final Canvas canvas = Canvas(pictureRecorder);
    final Paint paint1 = Paint()..color = Colors.orange;
    final Paint paint2 = Paint()..color = Colors.white;

    canvas.drawCircle(Offset(size / 2, size / 2), size / 2.0, paint1);
    canvas.drawCircle(Offset(size / 2, size / 2), size / 2.2, paint2);
    canvas.drawCircle(Offset(size / 2, size / 2), size / 2.8, paint1);

    if (text != null) {
      TextPainter painter = TextPainter(textDirection: TextDirection.ltr);
      painter.text = TextSpan(
        text: text,
        style: TextStyle(
            fontSize: size / 3,
            color: Colors.white,
            fontWeight: FontWeight.normal),
      );
      painter.layout();
      painter.paint(
        canvas,
        Offset(size / 2 - painter.width / 2, size / 2 - painter.height / 2),
      );
    }

    final img = await pictureRecorder.endRecording().toImage(size, size);
    final data = await img.toByteData(format: ImageByteFormat.png) as ByteData;

    return BitmapDescriptor.fromBytes(data.buffer.asUint8List());
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    // SearchBaseProvider 应该包裹你的 MaterialApp 或 WidgetsApp。这将确保所有路由都能访问到存储。
    return SearchBaseProvider(
      // 将 searchbase 实例传递给 SearchBaseProvider。任何祖先 SearchWidgetConnector 小部件都将找到并使用此值作为 SearchController。
      searchbase: searchbaseInstance,
      child: MaterialApp(
        title: "SearchBox Demo",
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: Scaffold(
          appBar: AppBar(
            // 一个过滤器,根据震级更新地震数据
            title: RangeInput(
              id: 'range-selector',
              beforeValueChange: (dynamic value) async {
                if (value is Map<String, dynamic>) {
                  final Map<String, dynamic> mapValue = value;
                  if (mapValue['start'] == 0 && mapValue['end'] == null) {
                    return Future.error(value);
                  }
                }
                print('beforeValueChange $value');
                return value;
              },
              buildTitle: () {
                return const Text(
                  "Filter by Magnitude",
                  style: TextStyle(
                    fontWeight: FontWeight.bold,
                    fontSize: 16.0,
                    color: Colors.amber,
                  ),
                );
              },
              buildRangeLabel: () {
                return const Text(
                  "to",
                  style: TextStyle(
                    fontWeight: FontWeight.bold,
                    fontSize: 16.0,
                    color: Colors.blue,
                  ),
                );
              },
              dataField: 'magnitude',
              range: const RangeType(
                start: ['other', 4, 5, 6, 7],
                end: 10,
              ),
              rangeLabels: RangeLabelsType(
                start: (value) {
                  return value == 'other'
                      ? 'Custom Other'
                      : (value == 'no_limit' ? 'No Limit' : '$value');
                },
                end: (value) {
                  return value == 'other'
                      ? 'Custom Other'
                      : (value == 'no_limit' ? 'No Limit' : '$value');
                },
              ),
              validateRange: (start, end) {
                if (start < end) {
                  return true;
                }
                return false;
              },
              buildErrorMessage: (start, end) {
                return Text(
                  'Custom error $start > $end',
                  style: const TextStyle(
                    fontSize: 15.0,
                    color: Colors.yellowAccent,
                  ),
                );
              },
              inputStyle: const TextStyle(
                fontSize: 18,
                height: 1,
                color: Colors.deepPurple,
              ),
              dropdownStyle: const TextStyle(
                fontSize: 18,
                height: 1,
                color: Colors.deepPurpleAccent,
              ),
              customContainer: (showError, childWidget) {
                return Container(
                  padding: const EdgeInsets.only(left: 6.0, right: 1.0),
                  height: 50,
                  decoration: BoxDecoration(
                    border: Border.all(
                      color: showError ? Colors.orangeAccent : Colors.black,
                      width: 1.5,
                    ),
                    borderRadius: BorderRadius.circular(3),
                  ),
                  child: childWidget,
                );
              },
              closeIcon: () {
                return const Text(
                  "X",
                  style: TextStyle(
                    fontWeight: FontWeight.bold,
                    fontSize: 16.0,
                    color: Colors.blueAccent,
                  ),
                );
              },
              dropdownIcon: (showError) {
                return Icon(
                  Icons.arrow_drop_down,
                  color: showError ? Colors.red : Colors.black,
                );
              },
            ),
            toolbarHeight: 120,
            backgroundColor: Colors.white.withOpacity(.9),
          ),
          bottomNavigationBar: Padding(
            padding: EdgeInsets.all(20.0),
            // SelectedFilters: 一个跟踪所有活动过滤器的小部件
            child: SelectedFilters(
              subscribeTo: const ['range-selector'],
              filterLabel: (id, value) {
                if (id == 'range-selector') {
                  return 'Range: $value';
                }
                return '$id: $value';
              },
              showClearAll: true,
              clearAllLabel: "Vanish All",
              onClearAll: () {
                // 这里可以执行一些操作
                print('Clear all called');
              },
              onClear: (id, value) {
                // 这里可以执行一些操作
                print('Filter $id with value: ${value.toString()} cleared');
              },
              resetToDefault: true,
              defaultValues: const {
                "range-selector": {'start': 5, 'end': 10}
              },
              // hideDefaultValues: false,
              // 取消注释以下属性以渲染自定义的 SelectedFilters 小部件 UI
              // buildFilters: (options) {
              //   List<Widget> widgets = [];
              //   options.selectedValues.forEach((id, filterValue) {
              //     widgets.add(
              //       Chip(
              //         label: Text(
              //             ' $id --- ${options.getValueAsString(filterValue)}'),
              //         onDeleted: () {
              //           options.clearValue(id);
              //         },
              //       ),
              //     );
              //   });
              //   return Wrap(
              //     spacing: 16.0,
              //     crossAxisAlignment: WrapCrossAlignment.start,
              //     // 相邻芯片之间的间距
              //     children: widgets,
              //   );
              // },
            ),
          ),
          body: ReactiveGoogleMap(
            id: 'map-widget',
            // 当震级发生变化时更新标记
            react: const {
              "and": "range-selector",
            },
            // 初始地图中心
            initialCameraPosition: const CameraPosition(
              target: LatLng(37.42796133580664, -122.085749655962),
              zoom: 4,
            ),
            // 启用标记聚类
            showMarkerClusters: true,
            // 构建聚类标记
            // 我们在这里根据集群中的项目数量显示 [Marker] 图标和文本。
            buildClusterMarker: (Cluster<Place> cluster) async {
              return Marker(
                markerId: MarkerId(cluster.getId()),
                position: cluster.location,
                icon: await _getMarkerBitmap(cluster.isMultiple ? 125 : 75,
                    text: cluster.isMultiple
                        ? cluster.count.toString()
                        : cluster.items.first.source?["magnitude"]),
              );
            },
            // 当 `showMarkerClusters` 设置为 `false` 时构建标记
            // buildMarker: (Place place) {
            //   return Marker(
            //       markerId: MarkerId(place.id), position: place.position);
            // },
            // 数据库字段映射到地理点。
            dataField: 'location',
            // Elasticsearch hits 的大小
            // 我们将 `size` 设置为零,因为我们正在使用聚合来构建标记。
            size: 0,
            // Elasticsearch 聚合的大小
            aggregationSize: 50,
            // 获取初始结果
            triggerQueryOnInit: false,
            // 当地图边界变化时更新标记
            searchAsMove: true,
            // [可选] 使用默认查询来使用 Elasticsearch `geohash_grid` 查询。
            defaultQuery: (SearchController controller) {
              return {
                "aggregations": {
                  "location": {
                    "geohash_grid": {"field": "location", "precision": 3},
                    "aggs": {
                      "top_earthquakes": {
                        "top_hits": {
                          "_source": {
                            "includes": ["magnitude"]
                          },
                          "size": 1
                        }
                      }
                    }
                  },
                }
              };
            },
            // [可选] 从聚合数据计算标记
            calculateMarkers: (SearchController controller) {
              List<Place> places = [];
              for (var bucket
                  in controller.aggregationData?.raw?["buckets"] ?? []) {
                try {
                  var locationDecode = GeoHash(bucket["key"]);
                  var source = bucket["top_earthquakes"]?["hits"]?["hits"]?[0]
                      ?["_source"];
                  places.add(
                    Place(
                        id: bucket["key"],
                        position: LatLng(locationDecode.latitude(),
                            locationDecode.longitude()),
                        source: source),
                  );
                } catch (e) {}
              }
              return places;
            },
          ),
        ),
      ),
    );
  }
}

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

1 回复

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


当然,以下是一个关于如何在Flutter项目中使用flutter_searchbox_ui插件来创建搜索界面的示例代码。这个插件提供了一个简洁的搜索界面,并且易于集成。

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

dependencies:
  flutter:
    sdk: flutter
  flutter_searchbox_ui: ^latest_version  # 请替换为最新版本号

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

接下来,在你的Flutter应用中,你可以按照以下步骤使用flutter_searchbox_ui插件:

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

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

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

class SearchScreen extends StatefulWidget {
  @override
  _SearchScreenState createState() => _SearchScreenState();
}

class _SearchScreenState extends State<SearchScreen> {
  final List<String> items = List.generate(100, (index) => "Item $index");
  String? searchQuery;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('SearchBox UI Demo'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: SearchBoxUI(
          searchQuery: searchQuery,
          onChanged: (query) {
            setState(() {
              searchQuery = query;
            });
          },
          onSearchPressed: (query) {
            // 处理搜索逻辑
            print("Searching for: $query");
          },
          clearIconPressed: () {
            setState(() {
              searchQuery = null;
            });
          },
          searchResults: searchQuery == null || searchQuery!.isEmpty
              ? items
              : items
                  .where((item) =>
                      item.toLowerCase().contains(searchQuery!.toLowerCase()))
                  .toList(),
          itemBuilder: (context, index) {
            final item = searchResults![index];
            return ListTile(
              title: Text(item),
            );
          },
          noResultsFoundWidget: Center(
            child: Text('No Results Found'),
          ),
        ),
      ),
    );
  }
}

在这个示例中,我们做了以下事情:

  1. 引入依赖:在文件顶部导入了flutter_searchbox_ui包。
  2. 创建主应用:在MyApp类中,设置了应用的主题和主页。
  3. 创建搜索界面:在SearchScreen类中,创建了一个包含搜索框的界面。
  4. 处理搜索逻辑:使用SearchBoxUI组件,并实现了onChangedonSearchPressedclearIconPressed回调来处理用户输入、搜索操作和清除操作。
  5. 显示搜索结果:根据用户的搜索查询,过滤并显示搜索结果。如果没有找到结果,显示一个“No Results Found”的提示。

请注意,这个示例假设你已经有一个包含100个项目的列表,并且根据用户的搜索查询动态地过滤这些项目。你可以根据自己的需求调整这个逻辑。

此外,flutter_searchbox_ui插件可能提供了更多的配置选项和自定义功能,你可以查阅其官方文档以获取更多信息。

回到顶部