Flutter 仿寫微信搜尋頁

語言: CN / TW / HK

這是我參與11月更文挑戰的第11天,活動詳情檢視:2021最後一次更文挑戰

IMG_0189.GIF

如上圖所示,我們用 Flutter 來仿寫搜尋頁面,這裡聊天首頁點選搜尋欄會跳轉到搜尋頁,搜尋頁面包含頂部搜尋框跟底部 ListView,在搜尋框內我們輸入搜尋詞會檢索聊天列表模型中 name 屬性中包含搜尋詞的模型,並在底部列表中展示,且搜尋詞高亮顯示。下面我們分別來介紹下這些功能的實現。

頂部搜尋欄

image.png

``` class SearchBar extends StatefulWidget { final ValueChanged? onChanged; const SearchBar({this.onChanged});

@override _SearchBarState createState() => _SearchBarState(); }

class _SearchBarState extends State { final TextEditingController _editingController = TextEditingController(); bool _showClose = false; void _onChange(text) { if (null != widget.onChanged) { widget.onChanged!(text); } setState(() { _showClose = ((text as String).length > 0); }); }

@override Widget build(BuildContext context) { return Container( height: 84, color: CahtThemColor, child: Column( children: [ SizedBox(height: 40,), //上半部分留空 Container( height: 44, child: Row( children: [ Container( width: screenWidth(context) - 50, height: 34, margin: EdgeInsets.only(left: 5, right: 10), padding: EdgeInsets.only(left: 5, right: 5), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(6.0), ), child: Row( children: [ Image(image: AssetImage('images/放大鏡b.png'), width: 20, color: Colors.grey,), //放大鏡 Expanded(child: TextField( controller: _editingController, onChanged: _onChange, autofocus: true, cursorColor: Colors.green, style: TextStyle( fontSize: 16.0, color: Colors.black87, fontWeight: FontWeight.w400, ), decoration: InputDecoration( contentPadding: EdgeInsets.only(left: 5, right: 5, bottom: 12,), border: InputBorder.none, hintText: '搜尋', hintStyle: TextStyle( fontSize: 16.0, color: Colors.grey, fontWeight: FontWeight.w400, ), ), ),), if (_showClose) GestureDetector( onTap: () { //清空搜尋框 _editingController.clear(); setState(() { _onChange(''); }); }, child: Icon( Icons.cancel, color: Colors.grey, size: 20, ), ), ], mainAxisAlignment: MainAxisAlignment.spaceBetween, ), ), //左邊白色背景 GestureDetector ( onTap: () { Navigator.pop(context); }, child: Text('取消'), ), //右邊取消按鈕 ], ), ), //搜尋條部分 ], ), ); } } ```

針對搜尋頁的整體佈局我們可以分為頂部的搜尋欄跟底部的搜尋列表兩部分,針對搜尋欄我們單獨定義了一個 SearchBar 的類來實現,底部搜尋列表我們用 Expanded 部件進行了包裝。

SearchBar 實現細節

針對 SearchBar 我們這裡整體高度設定為了 84,這裡最好高度定義為一個變數,根據機型來設定。針對搜尋欄我們分為上下兩部分,頂部留白及內容部分,頂部留白用 SizedBox 部件實現,底部內容用 Row 部件來實現,分為左邊搜尋框及右邊取消按鈕。

左邊搜尋框實現

搜尋框整體部分我們用 Container 部件來實現,可以設定寬、高及圓角屬性。child 屬性我們也是用 Row 部件來實現,分為左邊搜尋圖示、中間 TextField 及右邊 關閉按鈕

TextField 實現細節

TextField 部件有兩個比較重要的屬性,onChangedcontrolleronChanged 我們傳入了一個外部方法 _onChange,可以監聽輸入框文字的變化,當有文字時展示關閉按鈕,沒有文字時隱藏關閉按鈕。controller 我們傳入了一個外部定義的屬性 _editingController,可以用來控制 TextField 的編輯。

關閉按鈕的實現

關閉按鈕我們根據 _showClose 來判斷是否展示,且通過 GestureDetector 部件進行包裝,點選的時候清空輸入框及呼叫 _onChange 方法,引數傳入空字串,_onChange 方法中字串為空的時候就會隱藏關閉按鈕。

右邊取消按鈕實現

取消按鈕點選的時候就是返回到上一個頁面。

內容的檢索

我們監聽到文字輸入框的變化後,就需要進行內容的檢索,並且在底部列表中進行展示。

內容的傳遞

class SearchCell extends StatelessWidget { final List<ChatModel>? datas; const SearchCell({this.datas}); }

class SearchPage extends StatefulWidget { final List<ChatModel>? datas; const SearchPage({this.datas}); }

這裡我們是基於聊天頁面列表資料模型進行檢索,找到名稱中包含搜尋詞的模型進行展示。所以我們在 SearchCell 中定義了 datas 屬性,並且在 SearchPage 中也定義了 datas 屬性,分別在初始化的時候進行傳遞,這樣我們在搜尋頁面就可以拿到了聊天列表的模型資料。

內容的檢索

//滿足查詢條件的資料陣列 List<ChatModel> _models = []; //正在搜尋的內容 String _searchStr = ''; // 搜尋資料查詢 void _searchData(String text) { //每次搜尋前先清空 _models.clear(); _searchStr = text; if (text.length < 1) { setState(() {}); return; } for (int i = 0; i < datas.length; i++) { String name = widget.datas?[i].name as String; if(name.contains(text)) { _models.add(widget.datas?[i] as ChatModel); } } setState(() {}); }

我們把檢索邏輯都抽取到了 _searchData 方法中,輸入內容變化的時候呼叫 _searchData 方法。這裡我們定義了一個 _models 陣列,專門存放檢索資料,我們遍歷 datas,把檢索到的模型新增到 _models 中。

搜尋列表實現

``` TextStyle _normalStyle = TextStyle( fontSize: 16, color: Colors.black, ); TextStyle _hightlightedStyle = TextStyle( fontSize: 16, color: Colors.green, );

Widget _searchTitle(String name) { List textSpans = [];

List<String> searchStrs = name.split(_searchStr);
for (int i = 0; i < searchStrs.length; i++) {
  String str = searchStrs[i];
  if (str == '' && i < searchStrs.length - 1) {
    textSpans.add(TextSpan(text: _searchStr, style: _hightlightedStyle));
  } else {
    textSpans.add(TextSpan(text: str, style: _normalStyle));
    if (i < searchStrs.length - 1) {
      textSpans.add(TextSpan(text: _searchStr, style: _hightlightedStyle));
    }
  }
}
return RichText(text: TextSpan(children: textSpans));

} ```

搜尋列表的 cell 就是複用聊天列表的 cell,但是需要變化的是搜尋內容的高亮顯示,所以 ListTile 部件的 title 屬性我們用 RichText 來實現,實現內容這裡抽取到了 _searchTitle 方法中。name.split(_searchStr) 會返回一個根據搜尋詞 _searchStr 進行分割的陣列,但是當字串的開頭或者結尾有跟 _searchStr 相同的字元的時候在 searchStrs 陣列中的元素就是空字串,所以我們就需要進行特別判 str == ''。我們迴圈遍歷 searchStrs 陣列來建立 TextSpan 部件,並且根據內容是否跟搜尋內容一樣來分別指定樣式 _normalStyle_hightlightedStyle