網站首頁 編程語言 正文
介紹
開發中,頁面頭部為搜索樣式的設計非常常見,為了可以像系統AppBar
那樣使用,這篇文章記錄下在Flutter中自定義一個通用的搜索框AppBar記錄。
功能點: 搜索框、返回鍵、清除搜索內容功能、鍵盤處理。
效果圖
實現步驟
首先我們先來看下AppBar的源碼,實現了PreferredSizeWidget
類,我們可以知道這個類主要是控制AppBar的高度的,Scaffold
腳手架里的AppBar的參數類型就是PreferredSizeWidget
類型。
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; }
為了方便擴展,可以在Scaffold
里使用,我們需要創建AppBarSearch
類繼承有狀態StatefulWidget
類并實現PreferredSizeWidget
類,實現preferredSize
方法,并設置高度。
class AppBarSearch extends StatefulWidget implements PreferredSizeWidget { @override Size get preferredSize => Size.fromHeight(height); }
因為Scaffold
對AppBar
實現了狀態欄的適配,核心見下方源碼:
//獲取狀態欄高度 MediaQuery.of(context).padding.top;
這里我們直接返回AppBar
,并進行改造。(當然這里也可以不返回AppBar我們自己處理狀態欄的高度也行)。
思路: AppBar
title
字段自定義輸入框,主要通過文本框監聽實現清除搜索內容和顯示清除按鈕的功能,通過輸入框是否有焦點監聽進行刷新布局,通過定義回調函數的方式來進行搜索內容的監聽。
// 輸入框控制 _controller = widget.controller ?? TextEditingController(); // 焦點控制 _focusNode = widget.focusNode ?? FocusNode(); // 焦點獲取失去監聽 _focusNode?.addListener(() => setState(() {})); // 文本輸入監聽 _controller?.addListener(() => setState(() {}));
鍵盤搜素監聽:
只需設置TextField
的這兩個屬性即可。
textInputAction: TextInputAction.search, onSubmitted: widget.onSearch, //輸入框完成觸發
鍵盤彈出收起處理:
在iOS中鍵盤的處理是需要我們自己來進行處理的,我們需要的功能是點擊搜索框之外的地方失去焦點從而關閉鍵盤,這里我使用了處理鍵盤的一個插件:flutter_keyboard_visibility: ^5.1.0
,在我們需要處理焦點事件頁面根布局使用KeyboardDismissOnTap外部包裹即可,這個插件還可以主動控制鍵盤的彈出和收起,有興趣的小伙伴可以了解下。
return KeyboardDismissOnTap( child: Material();
完整源碼
/// 搜索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(); } }
總結
整體設計思路還是非常簡單的,主要就是通過兩個監聽來控制我們想要達到的交互效果,還有就是對dart
中函數Funcation
作為對象的加深理解,通過自定義搜索AppBar可以了解系統到AppBar的一些設計思路,這里主要還是記錄下我個人在做這個組件過程中的一個思路,希望對大家有所幫助~
原文鏈接:https://juejin.cn/post/7085976335398486029
相關推薦
- 2022-09-13 Android?Studio實現簡單補間動畫_Android
- 2022-06-23 C語言一看就懂的選擇與循環語句及函數介紹_C 語言
- 2021-12-10 Golang?Gin框架實現文件下載功能的示例代碼_Golang
- 2023-03-11 Go語言break跳轉語句怎么使用_Golang
- 2023-10-27 解決webpack打包后圖片加載失敗的bug(適用于所有本地靜態資源)
- 2022-07-27 python如何查找列表中元素的位置_python
- 2022-12-23 Kubernetes應用配置管理創建使用詳解_云其它
- 2023-06-04 Flask框架中的session設置詳解_python
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支