当前位置:首页 > 移动端开发 > 正文内容

Flutter 凭借SearchDelegate完成查找页面,完成查找主张、查找成果,处理IOS拼音问题

邻居的猫1个月前 (12-09)移动端开发390

查找界面运用Flutter自带的SearchDelegate组件完结,经过魔改完结如下作用:

  1. 搜素主张
  2. 查找成果,支撑改写和加载更多
  3. IOS中文输入拼音问题

界面预览

复制源码

将SearchDelegate的源码复制一份,修正内容如下:

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

/// 修正此处为 showMySearch
Future<T?> showMySearch<T>({
  required BuildContext context,
  required MySearchDelegate<T> delegate,
  String? query = '',
  bool useRootNavigator = false,
}) {
  delegate.query = query ?? delegate.query;
  delegate._currentBody = _SearchBody.suggestions;
  return Navigator.of(context, rootNavigator: useRootNavigator)
      .push(_SearchPageRoute<T>(
    delegate: delegate,
  ));
}

/// https://juejin.cn/post/7090374603951833118
abstract class MySearchDelegate<T> {
  MySearchDelegate({
    this.searchFieldLabel,
    this.searchFieldStyle,
    this.searchFieldDecorationTheme,
    this.keyboardType,
    this.textInputAction = TextInputAction.search,
  }) : assert(searchFieldStyle == null || searchFieldDecorationTheme == null);

  Widget buildSuggestions(BuildContext context);

  Widget buildResults(BuildContext context);

  Widget? buildLeading(BuildContext context);

  bool? automaticallyImplyLeading;

  double? leadingWidth;

  List<Widget>? buildActions(BuildContext context);

  PreferredSizeWidget? buildBottom(BuildContext context) => null;

  Widget? buildFlexibleSpace(BuildContext context) => null;

  ThemeData appBarTheme(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    final ColorScheme colorScheme = theme.colorScheme;
    return theme.copyWith(
      appBarTheme: AppBarTheme(
        systemOverlayStyle: colorScheme.brightness == Brightness.dark
            ? SystemUiOverlayStyle.light
            : SystemUiOverlayStyle.dark,
        backgroundColor: colorScheme.brightness == Brightness.dark
            ? Colors.grey[900]
            : Colors.white,
        iconTheme: theme.primaryIconTheme.copyWith(color: Colors.grey),
        titleTextStyle: theme.textTheme.titleLarge,
        toolbarTextStyle: theme.textTheme.bodyMedium,
      ),
      inputDecorationTheme: searchFieldDecorationTheme ??
          InputDecorationTheme(
            hintStyle: searchFieldStyle ?? theme.inputDecorationTheme.hintStyle,
            border: InputBorder.none,
          ),
    );
  }

  String get query => _queryTextController.completeText;

  set query(String value) {
    _queryTextController.completeText = value; // 更新实践查找内容
    _queryTextController.text = value; // 更新输入框内容
    if (_queryTextController.text.isNotEmpty) {
      _queryTextController.selection = TextSelection.fromPosition(
          TextPosition(offset: _queryTextController.text.length));
    }
  }

  void showResults(BuildContext context) {
    _focusNode?.unfocus();
    _currentBody = _SearchBody.results;
  }

  void showSuggestions(BuildContext context) {
    assert(_focusNode != null,
        '_focusNode must be set by route before showSuggestions is called.');
    _focusNode!.requestFocus();
    _currentBody = _SearchBody.suggestions;
  }

  void close(BuildContext context, T result) {
    _currentBody = null;
    _focusNode?.unfocus();
    Navigator.of(context)
      ..popUntil((Route<dynamic> route) => route == _route)
      ..pop(result);
  }

  final String? searchFieldLabel;

  final TextStyle? searchFieldStyle;

  final InputDecorationTheme? searchFieldDecorationTheme;

  final TextInputType? keyboardType;

  final TextInputAction textInputAction;

  Animation<double> get transitionAnimation => _proxyAnimation;

  FocusNode? _focusNode;

  final ChinaTextEditController _queryTextController = ChinaTextEditController();

  final ProxyAnimation _proxyAnimation =
      ProxyAnimation(kAlwaysDismissedAnimation);

  final ValueNotifier<_SearchBody?> _currentBodyNotifier =
      ValueNotifier<_SearchBody?>(null);

  _SearchBody? get _currentBody => _currentBodyNotifier.value;
  set _currentBody(_SearchBody? value) {
    _currentBodyNotifier.value = value;
  }

  _SearchPageRoute<T>? _route;

  /// Releases the resources.
  @mustCallSuper
  void dispose() {
    _currentBodyNotifier.dispose();
    _focusNode?.dispose();
    _queryTextController.dispose();
    _proxyAnimation.parent = null;
  }
}

/// search page.
enum _SearchBody {
  suggestions,

  results,
}

class _SearchPageRoute<T> extends PageRoute<T> {
  _SearchPageRoute({
    required this.delegate,
  }) {
    assert(
      delegate._route == null,
      'The ${delegate.runtimeType} instance is currently used by another active '
      'search. Please close that search by calling close() on the MySearchDelegate '
      'before opening another search with the same delegate instance.',
    );
    delegate._route = this;
  }

  final MySearchDelegate<T> delegate;

  @override
  Color? get barrierColor => null;

  @override
  String? get barrierLabel => null;

  @override
  Duration get transitionDuration => const Duration(milliseconds: 300);

  @override
  bool get maintainState => false;

  @override
  Widget buildTransitions(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    return FadeTransition(
      opacity: animation,
      child: child,
    );
  }

  @override
  Animation<double> createAnimation() {
    final Animation<double> animation = super.createAnimation();
    delegate._proxyAnimation.parent = animation;
    return animation;
  }

  @override
  Widget buildPage(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
  ) {
    return _SearchPage<T>(
      delegate: delegate,
      animation: animation,
    );
  }

  @override
  void didComplete(T? result) {
    super.didComplete(result);
    assert(delegate._route == this);
    delegate._route = null;
    delegate._currentBody = null;
  }
}

class _SearchPage<T> extends StatefulWidget {
  const _SearchPage({
    required this.delegate,
    required this.animation,
  });

  final MySearchDelegate<T> delegate;
  final Animation<double> animation;

  @override
  State<StatefulWidget> createState() => _SearchPageState<T>();
}

class _SearchPageState<T> extends State<_SearchPage<T>> {
  // This node is owned, but not hosted by, the search page. Hosting is done by
  // the text field.
  FocusNode focusNode = FocusNode();

  @override
  void initState() {
    super.initState();
    widget.delegate._queryTextController.addListener(_onQueryChanged);
    widget.animation.addStatusListener(_onAnimationStatusChanged);
    widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged);
    focusNode.addListener(_onFocusChanged);
    widget.delegate._focusNode = focusNode;
  }

  @override
  void dispose() {
    super.dispose();
    widget.delegate._queryTextController.removeListener(_onQueryChanged);
    widget.animation.removeStatusListener(_onAnimationStatusChanged);
    widget.delegate._currentBodyNotifier.removeListener(_onSearchBodyChanged);
    widget.delegate._focusNode = null;
    focusNode.dispose();
  }

  void _onAnimationStatusChanged(AnimationStatus status) {
    if (status != AnimationStatus.completed) {
      return;
    }
    widget.animation.removeStatusListener(_onAnimationStatusChanged);
    if (widget.delegate._currentBody == _SearchBody.suggestions) {
      focusNode.requestFocus();
    }
  }

  @override
  void didUpdateWidget(_SearchPage<T> oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.delegate != oldWidget.delegate) {
      oldWidget.delegate._queryTextController.removeListener(_onQueryChanged);
      widget.delegate._queryTextController.addListener(_onQueryChanged);
      oldWidget.delegate._currentBodyNotifier
          .removeListener(_onSearchBodyChanged);
      widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged);
      oldWidget.delegate._focusNode = null;
      widget.delegate._focusNode = focusNode;
    }
  }

  void _onFocusChanged() {
    if (focusNode.hasFocus &&
        widget.delegate._currentBody != _SearchBody.suggestions) {
      widget.delegate.showSuggestions(context);
    }
  }

  void _onQueryChanged() {
    setState(() {
      // rebuild ourselves because query changed.
    });
  }

  void _onSearchBodyChanged() {
    setState(() {
      // rebuild ourselves because search body changed.
    });
  }

  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMaterialLocalizations(context));
    final ThemeData theme = widget.delegate.appBarTheme(context);
    final String searchFieldLabel = widget.delegate.searchFieldLabel ??
        MaterialLocalizations.of(context).searchFieldLabel;
    Widget? body;
    switch (widget.delegate._currentBody) {
      case _SearchBody.suggestions:
        body = KeyedSubtree(
          key: const ValueKey<_SearchBody>(_SearchBody.suggestions),
          child: widget.delegate.buildSuggestions(context),
        );
      case _SearchBody.results:
        body = KeyedSubtree(
          key: const ValueKey<_SearchBody>(_SearchBody.results),
          child: widget.delegate.buildResults(context),
        );
      case null:
        break;
    }

    late final String routeName;
    switch (theme.platform) {
      case TargetPlatform.iOS:
      case TargetPlatform.macOS:
        routeName = '';
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
      case TargetPlatform.linux:
      case TargetPlatform.windows:
        routeName = searchFieldLabel;
    }

    return Semantics(
      explicitChildNodes: true,
      scopesRoute: true,
      namesRoute: true,
      label: routeName,
      child: Theme(
        data: theme,
        child: Scaffold(
          appBar: AppBar(
            leadingWidth: widget.delegate.leadingWidth,
            automaticallyImplyLeading:
                widget.delegate.automaticallyImplyLeading ?? true,
            leading: widget.delegate.buildLeading(context),
            title: TextField(
              controller: widget.delegate._queryTextController,
              focusNode: focusNode,
              style: widget.delegate.searchFieldStyle ??
                  theme.textTheme.titleLarge,
              textInputAction: widget.delegate.textInputAction,
              keyboardType: widget.delegate.keyboardType,
              onSubmitted: (String _) => widget.delegate.showResults(context),
              decoration: InputDecoration(hintText: searchFieldLabel),
            ),
            flexibleSpace: widget.delegate.buildFlexibleSpace(context),
            actions: widget.delegate.buildActions(context),
            bottom: widget.delegate.buildBottom(context),
          ),
          body: AnimatedSwitcher(
            duration: const Duration(milliseconds: 300),
            child: body,
          ),
        ),
      ),
    );
  }
}

class ChinaTextEditController extends TextEditingController {
  ///拼音输入完结后的文字
  var completeText = '';

  @override
  TextSpan buildTextSpan(
      {required BuildContext context,
      TextStyle? style,
      required bool withComposing}) {
    ///拼音输入完结
    if (!value.composing.isValid || !withComposing) {
      if (completeText != value.text) {
        completeText = value.text;
        WidgetsBinding.instance.addPostFrameCallback((_) {
          notifyListeners();
        });
      }
      return TextSpan(style: style, text: text);
    }

    ///回来输入款式,可自定义款式
    final TextStyle composingStyle = style?.merge(
      const TextStyle(decoration: TextDecoration.underline),
    ) ?? const TextStyle(decoration: TextDecoration.underline);
    return TextSpan(style: style, children: <TextSpan>[
      TextSpan(text: value.composing.textBefore(value.text)),
      TextSpan(
        style: composingStyle,
        text: value.composing.isValid && !value.composing.isCollapsed
            ? value.composing.textInside(value.text)
            : "",
      ),
      TextSpan(text: value.composing.textAfter(value.text)),
    ]);
  }
}

完结查找

创立SearchPage承继MySearchDelegate,修正款式,完结页面。需求重写下面5个办法

  • appBarTheme:修正查找款式
  • buildActions:查找框右侧的办法
  • buildLeading:查找框左边的回来按钮
  • buildResults:查找成果
  • buildSuggestions:查找主张
import 'package:e_book_clone/pages/search/MySearchDelegate.dart';
import 'package:flutter/src/material/theme_data.dart';
import 'package:flutter/src/widgets/framework.dart';

class Demo extends MySearchDelegate {

  @override
  ThemeData appBarTheme(BuildContext context) {
    // TODO: implement appBarTheme
    return super.appBarTheme(context);
  }
  
  @override
  List<Widget>? buildActions(BuildContext context) {
    // TODO: implement buildActions
    throw UnimplementedError();
  }

  @override
  Widget? buildLeading(BuildContext context) {
    // TODO: implement buildLeading
    throw UnimplementedError();
  }

  @override
  Widget buildResults(BuildContext context) {
    // TODO: implement buildResults
    throw UnimplementedError();
  }

  @override
  Widget buildSuggestions(BuildContext context) {
    // TODO: implement buildSuggestions
    throw UnimplementedError();
  }
}

修正款式

@override
ThemeData appBarTheme(BuildContext context) {
  final ThemeData theme = Theme.of(context);
  final ColorScheme colorScheme = theme.colorScheme;
  return theme.copyWith( // 运用copyWith,适配大局主题
    appBarTheme: AppBarTheme( // AppBar款式修正
      systemOverlayStyle: colorScheme.brightness == Brightness.dark
          ? SystemUiOverlayStyle.light
          : SystemUiOverlayStyle.dark,
      surfaceTintColor: Theme.of(context).colorScheme.surface,
      titleSpacing: 0, // textfield前面的距离
      elevation: 0, // 暗影
    ),
    inputDecorationTheme: InputDecorationTheme(
      isCollapsed: true,
      hintStyle: TextStyle( // 提示文字色彩
          color: Theme.of(ToastUtils.context).colorScheme.inversePrimary),
      filled: true,  // 填充色彩
      contentPadding: EdgeInsets.symmetric(vertical: 10.h, horizontal: 15.w),
      fillColor: Theme.of(context).colorScheme.secondary, // 填充色彩,需求合作 filled
      enabledBorder: OutlineInputBorder( // testified 边框
        borderRadius: BorderRadius.circular(12.r),
        borderSide: BorderSide(
          color: Theme.of(context).colorScheme.surface,
        ),
      ),
      focusedBorder: OutlineInputBorder( // testified 边框
        borderRadius: BorderRadius.circular(12.r),
        borderSide: BorderSide(
          color: Theme.of(context).colorScheme.surface,
        ),
      ),
    ),
  );
}

@override
TextStyle? get searchFieldStyle => TextStyle(fontSize: 14.sp); // 字体大小设置,主要是掩盖默许款式

按钮功用

左边回来按钮,右侧就放了一个查找文本,点击之后显现查找成果

@override
Widget? buildLeading(BuildContext context) {
  return IconButton(
    onPressed: () {
      close(context, null);
    },
    icon: Icon(
      color: Theme.of(context).colorScheme.onSurface,
      Icons.arrow_back_ios_new,
      size: 20.r,
    ),
  );
}

@override
List<Widget>? buildActions(BuildContext context) {
  return [
    Padding(
      padding: EdgeInsets.only(right: 15.w, left: 15.w),
      child: GestureDetector(
        onTap: () {
          showResults(context);
        },
        child: Text(
          '查找',
          style: TextStyle(
              color: Theme.of(context).colorScheme.primary, fontSize: 15.sp),
        ),
      ),
    )
  ];
}

查找主张

当 TextField 输入改变时,就会调用buildSuggestions办法,改写布局,因而考虑运用FlutterBuilder办理页面和数据。

final SearchViewModel _viewModel = SearchViewModel();

@override
Widget buildSuggestions(BuildContext context) {
  if (query.isEmpty) {
    // 这儿能够展现抢手查找等,有查找主张时,抢手查找会被替换成查找主张
    return const SizedBox();
  }
  return FutureBuilder(
    future: _viewModel.getSuggest(query),
    builder: (BuildContext context, AsyncSnapshot<List<Suggest>> snapshot) {
      if (snapshot.connectionState == ConnectionState.waiting) {
        // 数据加载中
        return const Center(child: CircularProgressIndicator());
      } else if (snapshot.hasError) {
        // 数据加载过错
        return Center(child: Text('Error: ${snapshot.error}'));
      } else if (snapshot.hasData) {
        // 数据加载成功,展现成果
        final List<Suggest> searchResults = snapshot.data ?? [];
        return ListView.builder(
            padding: EdgeInsets.all(15.r),
            itemCount: searchResults.length,
            itemBuilder: (context, index) {
              return GestureDetector(
                onTap: () {
                  // 更新输入框
                  query = searchResults[index].text ?? query;
                  showResults(context);
                },
                child: Container(
                  padding: EdgeInsets.symmetric(vertical: 10.h),
                  decoration: BoxDecoration(
                    border: BorderDirectional(
                      bottom: BorderSide(
                        width: 0.6,
                        color: Theme.of(context).colorScheme.surfaceContainer,
                      ),
                    ),
                  ),
                  child: Text('${searchResults[index].text}'),
                ),
              );
            });
      } else {
        // 数据为空
        return const Center(child: Text('No results found'));
      }
    },
  );
}

实体类代码如下:

class Suggest {
  Suggest({
    this.id,
    this.url,
    this.text,
    this.isHot,
    this.hotLevel,
  });

  Suggest.fromJson(dynamic json) {
    id = json['id'];
    url = json['url'];
    text = json['text'];
    isHot = json['is_hot'];
    hotLevel = json['hot_level'];
  }

  String? id;
  String? url;
  String? text;
  bool? isHot;
  int? hotLevel;
}

ViewModel代码如下:

class SearchViewModel {
  Future<List<Suggest>> getSuggest(String keyword) async {
    if (keyword.isEmpty) {
      return [];
    }
    return await JsonApi.instance().fetchSuggestV3(keyword);
  }
}

查找成果

咱们需求查找成果页面支撑加载更多,这儿用到了 SmartRefrsh 组件

flutter pub add pull_to_refresh

buildResults办法是经过调用showResults(context);办法改写页面,因而为了便利数据动态改变,新建search_result_page.dart页面

import 'package:e_book_clone/components/book_tile/book_tile_vertical/my_book_tile_vertical_item.dart';
import 'package:e_book_clone/components/book_tile/book_tile_vertical/my_book_tile_vertical_item_skeleton.dart';
import 'package:e_book_clone/components/my_smart_refresh.dart';
import 'package:e_book_clone/models/book.dart';
import 'package:e_book_clone/models/types.dart';
import 'package:e_book_clone/pages/search/search_vm.dart';
import 'package:e_book_clone/utils/navigator_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:provider/provider.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';

class SearchResultPage extends StatefulWidget {
  final String query; // 

扫描二维码推送至手机访问。

版权声明:本文由51Blog发布,如需转载请注明出处。

本文链接:https://www.51blog.vip/?id=492

标签: 安卓开发
分享给朋友:

“Flutter 凭借SearchDelegate完成查找页面,完成查找主张、查找成果,处理IOS拼音问题” 的相关文章

Flutter组件

Flutter组件

两个常用的组件:Material和Scaffold润饰App和H5相同很固定。 1.Container 2.Text 3.picture import 'package:flutter/material.dart'; void main() { runApp(MaterialApp(...

手机怎么关闭开发者模式,手机开发者模式怎么关闭?轻松操作指南

手机怎么关闭开发者模式,手机开发者模式怎么关闭?轻松操作指南

关闭开发者模式的具体步骤可能会因手机型号和操作系统版本而有所不同,但一般来说,可以按照以下步骤进行操作:1. 打开手机的“设置”应用。2. 在设置菜单中找到“关于手机”或“关于设备”选项,并点击进入。3. 在“关于手机”或“关于设备”页面中,找到“版本号”或“软件版本”选项,并连续点击该选项7次。每...

鸿蒙战神,穿越时空的传奇之旅

鸿蒙战神,穿越时空的传奇之旅

您好,请问您提到的“鸿蒙战神”是指小说还是其他内容呢?根据您的描述,我找到了一些相关信息:1. 小说《鸿蒙战神》:这是由落世凡创作的一部异世争霸类小说,目前在小说阅读网连载。故事讲述了一个得天独厚的少年,他的命运被鸿蒙第一人掌握,而他的前世是五行圣星属中的帝王星。鸿蒙第一人为了让他恢复前生记忆,为他...

鸿蒙界主游洪荒,国运开局选择洪荒世界化身鸿蒙掌控者

鸿蒙界主游洪荒,国运开局选择洪荒世界化身鸿蒙掌控者

《鸿蒙界主游洪荒》是由作者梦中de花朵创作的一部穿越、玄幻奇幻类小说。这部小说主要讲述了主角在鸿蒙时期经历的各种冒险和修炼故事。小说通过精彩的情节和丰富的想象力,吸引了众多读者的喜爱。 小说简介《鸿蒙界主游洪荒》的故事背景设定在鸿蒙时期,主角在这个充满神秘和奇幻的世界中,经历了各种挑战和冒险。小说融...

帝临鸿蒙境界划分,基础境界划分

帝临鸿蒙境界划分,基础境界划分

《帝临鸿蒙》中的境界划分较为复杂,涵盖了多个世界和境界层次。以下是详细的境界划分: 三千世界1. 人阶2. 地阶3. 将阶4. 王阶5. 霸阶6. 君阶7. 天阶8. 极限强者 大千世界1. 仙士2. 神灵3. 神明4. 天主5. 大祖6. 主宰7. 皇级(包括明道、悟道、掌道、化道) 鸿蒙世界1....

鸿蒙概念股票有哪些,把握科技新风口

鸿蒙概念股票有哪些,把握科技新风口

1. 软通动力(301236.SZ):国内IT外包市场头部企业,为众多知名企业提供综合性软件与信息技术服务。2. 润和软件(300339.SZ):开放鸿蒙发起单位之一,也是华为鸿蒙操作系统生态共建者。3. 常山北明(000158.SZ):纺织、软件双主业发展的国资控股企业,组建了华为鸿蒙开发团队。4...