Flutter implements custom search AppBar

Get into the habit of writing together! This is the 10th day of my participation in the "Nuggets Daily New Plan · April Update Challenge", click to view the details of the event .

  • In development, it is very common to design the search style at the head of the page. In order to be used like a system AppBar, this article records the customizing of a general search box AppBar in Flutter.

Function points: search box, return key, clear search content function, keyboard processing.

Effect picture:

47c3288e-77c6-4a95-9126-3d561a90e022.gif
First of all, let's look at the source code of AppBar and implement the PreferredSizeWidgetclass. We can know that this class mainly controls the height of ScaffoldAppBar, and the parameter type of AppBar in the scaffold is the PreferredSizeWidgettype.

class AppBar extends StatefulWidget implements PreferredSizeWidget{
...
preferredSize = _PreferredAppBarSize(toolbarHeight, bottom?.preferredSize.height),
...

/// {@template flutter.material.appbar.toolbarHeight}
/// Defines the height of the toolbar component of an [AppBar].
///
/// By default, the value of `toolbarHeight` is [kToolbarHeight].
/// {@endtemplate}
final double? toolbarHeight;

...
/// The height of the toolbar component of the [AppBar].
const double kToolbarHeight = 56.0;

}

abstract class PreferredSizeWidget implements Widget {
  // 设置在不受约束下希望的大小
  // 设置高度:Size.fromHeight(myAppBarHeight)
  Size get preferredSize;
}
复制代码

In order to facilitate the extension and can be Scaffoldused in it, we need to create a AppBarSearchclass that inherits the stateful StatefulWidgetclass and implement PreferredSizeWidgetthe class, implement the preferredSizemethod, and set the height.

class AppBarSearch extends StatefulWidget implements PreferredSizeWidget {

@override
Size get preferredSize => Size.fromHeight(height);

}
复制代码

Because Scaffoldof AppBarthe adaptation of the status bar, see the source code below for the core:

//获取状态栏高度
MediaQuery.of(context).padding.top;
复制代码

image.pngHere we go back directly AppBarand make a transformation. (Of course, we can also handle the height of the status bar ourselves without returning the AppBar).

Idea: AppBar title The field customizes the input box, mainly through the text box monitoring to achieve the function of clearing the search content and displaying the clear button, refreshing the layout by monitoring whether the input box has focus, and monitoring the search content by defining the callback function.

// 输入框控制
_controller = widget.controller ?? TextEditingController();
// 焦点控制
_focusNode = widget.focusNode ?? FocusNode();
// 焦点获取失去监听
_focusNode?.addListener(() => setState(() {}));
// 文本输入监听
_controller?.addListener(() => setState(() {}));
复制代码
  • Keyboard search listener:

Just set TextFieldthese two properties.

textInputAction: TextInputAction.search,
onSubmitted: widget.onSearch, //输入框完成触发
复制代码
  • The keyboard pops up and closes the process:

The processing of the keyboard in iOS needs to be handled by ourselves. The function we need is to click outside the search box to lose the focus to close the keyboard. Here I use a plug-in that handles the keyboard: flutter_keyboard_visibility: ^5.1.0, where we need to handle the focus event The root layout of the page can be wrapped by KeyboardDismissOnTap. This plug-in can also actively control the pop-up and retraction of the keyboard. Interested friends can learn about it.

return KeyboardDismissOnTap(
    child: Material();
复制代码
  • Complete source code:
/// 搜索AppBar
class AppBarSearch extends StatefulWidget implements PreferredSizeWidget {
  AppBarSearch({
    Key? key,
    this.borderRadius = 10,
    this.autoFocus = false,
    this.focusNode,
    this.controller,
    this.height = 40,
    this.value,
    this.leading,
    this.backgroundColor,
    this.suffix,
    this.actions = const [],
    this.hintText,
    this.onTap,
    this.onClear,
    this.onCancel,
    this.onChanged,
    this.onSearch,
    this.onRightTap,
  }) : super(key: key);
  final double? borderRadius;
  final bool? autoFocus;
  final FocusNode? focusNode;
  final TextEditingController? controller;

  // 输入框高度 默认40
  final double height;

  // 默认值
  final String? value;

  // 最前面的组件
  final Widget? leading;

  // 背景色
  final Color? backgroundColor;

  // 搜索框内部后缀组件
  final Widget? suffix;

  // 搜索框右侧组件
  final List<Widget> actions;

  // 输入框提示文字
  final String? hintText;

  // 输入框点击回调
  final VoidCallback? onTap;

  // 清除输入框内容回调
  final VoidCallback? onClear;

  // 清除输入框内容并取消输入
  final VoidCallback? onCancel;

  // 输入框内容改变
  final ValueChanged<String>? onChanged;

  // 点击键盘搜索
  final ValueChanged<String>? onSearch;

  // 点击右边widget
  final VoidCallback? onRightTap;

  @override
  _AppBarSearchState createState() => _AppBarSearchState();

  @override
  Size get preferredSize => Size.fromHeight(height);
}

class _AppBarSearchState extends State<AppBarSearch> {
  TextEditingController? _controller;
  FocusNode? _focusNode;

  bool get isFocus => _focusNode?.hasFocus ?? false; //是否获取焦点

  bool get isTextEmpty => _controller?.text.isEmpty ?? false; //输入框是否为空

  bool get isActionEmpty => widget.actions.isEmpty; // 右边布局是否为空

  bool isShowCancel = false;

  @override
  void initState() {
    _controller = widget.controller ?? TextEditingController();
    _focusNode = widget.focusNode ?? FocusNode();
    if (widget.value != null) _controller?.text = widget.value ?? "";
    // 焦点获取失去监听
    _focusNode?.addListener(() => setState(() {}));
    // 文本输入监听
    _controller?.addListener(() {
      setState(() {});
    });
    super.initState();
  }

  // 清除输入框内容
  void _onClearInput() {
    setState(() {
      _controller?.clear();
    });
    widget.onClear?.call();
  }

  // 取消输入框编辑失去焦点
  void _onCancelInput() {
    setState(() {
      _controller?.clear();
      _focusNode?.unfocus(); //失去焦点
    });
    // 执行onCancel
    widget.onCancel?.call();
  }

  Widget _suffix() {
    if (!isTextEmpty) {
      return InkWell(
        onTap: _onClearInput,
        child: SizedBox(
          width: widget.height,
          height: widget.height,
          child: Icon(Icons.cancel, size: 22, color: Color(0xFF999999)),
        ),
      );
    }
    return widget.suffix ?? SizedBox();
  }

  List<Widget> _actions() {
    List<Widget> list = [];
    if (isFocus || !isTextEmpty) {
      list.add(InkWell(
        onTap: widget.onRightTap ?? _onCancelInput,
        child: Container(
          constraints: BoxConstraints(minWidth: 48.w),
          alignment: Alignment.center,
          child: MyText(
            '搜索',
            fontColor: MyColors.color_666666,
            fontSize: 14.sp,
          ),
        ),
      ));
    } else if (!isActionEmpty) {
      list.addAll(widget.actions);
    }
    return list;
  }

  @override
  Widget build(BuildContext context) {
    return AppBar(
      backgroundColor: widget.backgroundColor,
      //阴影z轴
      elevation: 0,
      // 标题与其他控件的间隔
      titleSpacing: 0,
      leadingWidth: 40.w,
      leading: widget.leading ??
          InkWell(
            child: Icon(
              Icons.arrow_back_ios_outlined,
              color: MyColors.color_666666,
              size: 16.w,
            ),
            onTap: () {
              Routes.finish(context);
            },
          ),
      title: Container(
          margin: EdgeInsetsDirectional.only(end: 10.w),
          height: widget.height,
          decoration: BoxDecoration(
            color: Color(0xFFF2F2F2),
            borderRadius: BorderRadius.circular(widget.borderRadius ?? 0),
          ),
          child: Container(
            child: Row(
              children: [
                SizedBox(
                  width: widget.height,
                  height: widget.height,
                  child:
                      Icon(Icons.search, size: 20.w, color: Color(0xFF999999)),
                ),
                Expanded(
                  // 权重
                  flex: 1,
                  child: TextField(
                    autofocus: widget.autoFocus ?? false,
                    // 是否自动获取焦点
                    focusNode: _focusNode,
                    // 焦点控制
                    controller: _controller,
                    // 与输入框交互控制器
                    //装饰
                    decoration: InputDecoration(
                      isDense: true,
                      border: InputBorder.none,
                      hintText: widget.hintText ?? '请输入关键字',
                      hintStyle: TextStyle(
                          fontSize: 14.sp, color: MyColors.color_666666),
                    ),
                    style: TextStyle(
                      fontSize: 14.sp,
                      color: MyColors.color_333333,
                    ),
                    // 键盘动作右下角图标
                    textInputAction: TextInputAction.search,
                    onTap: widget.onTap,
                    // 输入框内容改变回调
                    onChanged: widget.onChanged,
                    onSubmitted: widget.onSearch, //输入框完成触发
                  ),
                ),
                _suffix(),
              ],
            ),
          )),
      actions: _actions(),
    );
  }

  @override
  void dispose() {
    _controller?.dispose();
    _focusNode?.dispose();
    super.dispose();
  }
}
复制代码

Summarize

The overall design idea is still very simple, mainly to control the interaction effect we want to achieve through two monitors, and to deepen the understanding dartof the function Funcationas an object, and to understand some design ideas from the system to the AppBar through the custom search AppBar , here is mainly to record one of my personal thoughts in the process of making this component, I hope it will be helpful to everyone~

Guess you like

Origin juejin.im/post/7085976335398486029