Flutter 仿寫微信通訊錄頁面

語言: CN / TW / HK

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

image.png

針對通訊錄頁面我們整體上可以分為列表部分跟右邊索引條部分,因為索引條是在列表的上面,所以這個頁面的 body 部分我們採用 Stack 部件。下面我們可以來按照每個部分來詳細介紹下實現思路。

列表部分實現

列表部分整體我們可以分為頂部 4 個固定的 cell 跟底部的聯絡人 cell,所以這一塊我們定義了兩個資料來源 _headerData_listDatas,但是 cell 我們用的是同一個 cellcell 對應的模型我們用的也是同一個,只是對引數進行的區分,cell 的引數也是一樣。但是我們還可以看到,聯絡人部分相同首字母的頭部會有一個索引頭,在 iOS 中我們可以用組頭檢視來實現,但是 Flutter 中沒有組的概念,所以我們就在 cell 中來新增這個索引頭檢視,只是通過邏輯跟引數來判斷是否展示索引頭檢視部分。下面我們看一下關鍵程式碼部分的實現。

列表部分

``` Container( color: CahtThemColor, child: ListView.builder(itemBuilder: _itemForRow, itemCount: _headerData.length + _listDatas.length,) ),

Widget _itemForRow(BuildContext context, int index) { if (index < _headerData.length) { return _FriendCell(imageAssets: _headerData[index].imageAssets, name: _headerData[index].name); } index = index - _headerData.length; // 判斷是否顯示組名稱 return _FriendCell( imageUrl: _listDatas[index].imageUrl, name: _listDatas[index].name, groupTitle: (index > 0 && _listDatas[index].indexLetter == _listDatas[index -1].indexLetter) ? '' : _listDatas[index].indexLetter); } ```

列表部分我們用 Container 包了一層,方便後期的改動。_itemForRow 方法部分我們通過判斷 index 是否大於 _headerData.length 來傳不同的引數對 cell 進行初始化,針對每組相同首字母的 cell 我們通過判斷當前模型與下一個模型的索引字元 indexLetter 是否相等來判斷是否展示索引頭檢視。

cell 部分的實現

class _FriendCell extends StatelessWidget { _FriendCell({this.imageUrl = '', this.name = '', this.groupTitle = '', this.imageAssets = ''}); final String imageUrl; final String name; final String groupTitle; final String imageAssets; @override Widget build(BuildContext context) { return Column( children: [ groupTitle.length > 0 ? Container( height: 30, color: CahtThemColor, alignment: Alignment.centerLeft, padding: EdgeInsets.only(left: 15), child: groupTitle.length > 0 ? Text(groupTitle, style: TextStyle(color: Colors.grey),) : null, ) : Container(), //組頭 Container( color: Colors.white, child: Row( children: [ Container( margin: EdgeInsets.all(10), width: 34, height: 34, decoration: BoxDecoration( borderRadius: BorderRadius.circular(6.0), image: imageUrl.length > 0 ? DecorationImage( image: NetworkImage(imageUrl) ) : DecorationImage( image: AssetImage(imageAssets), ) ), ),//圖片 Container( // color: Colors.red, width: screenWidth(context) - 54, child: Column( children: [ Container( alignment: Alignment.centerLeft, height: 54, child: Text( name, style: TextStyle(fontSize: 18), ), ), Container( height: 0.5, color: CahtThemColor, ), //下劃線 ], ), ),//暱稱 ], ), ), //cell的內容 ], ); } }

如果一個類我們只是想在當前檔案中使用可以通過下劃線的方式來定義。cell 的整體我們分為上下兩部分,組頭跟內容部分。所以整體我們使用 Column 部件來實現。

模型部分的實現

class Friends { final String imageUrl; final String name; final String indexLetter; final String imageAssets; Friends({this.imageUrl = '', this.name = '', this.indexLetter = '', this.imageAssets = ''}); }

索引檢視的實現

索引檢視部分當我們點選的時候顏色會變成黑色透明的,所以整體是有狀態的,繼承於 StatefulWidget。因為索引條是懸浮在檢視的右邊居中,所以整體採用 Positioned 部件,整體高度我們這裡設定為整個螢幕的二分之一,寬度為 30,距離右邊為 0,距離頂部為螢幕的八分之一。

索引檢視內容部分

for (int i = 0; i < INDEX_WORDS.length; i++) { _words.add( Expanded(child: Text(INDEX_WORDS[i], style: TextStyle(fontSize: 10, color: _textColor),)) ); }

child: Container( color: _bkColor, child: Column( children: _words, ), ),

INDEX_WORDS 陣列存放的是索引字元,我們在 build 中方法中遍歷陣列,把每個字元對應的部件加入到 _words 中。索引條中的部件是上下排列並且等分的,索引每個索引字元對應的部件採用 Expanded,整體採用 Column 部件。

索引條點選改變狀態顏色

image.png

child: GestureDetector( // 索引條點選 onVerticalDragDown: (DragDownDetails details){ setState(() { _bkColor = Color.fromRGBO(1, 1, 1, 0.4); _textColor = Colors.white; }); }, // 索引條點選取消 onVerticalDragEnd: (DragEndDetails details){ setState(() { _bkColor = Color.fromRGBO(1, 1, 1, 0.0); _textColor = Colors.black; }); },

當點選索引條的時候需要改變背景顏色跟文字顏色,索引我們定義了兩個變數 _bkColor_textColor,當索引條被點選跟取消點選的時候呼叫 setState 方法,並且修改這兩個變數的值。

索引條拖拽

//獲取選中的 item 文字 String _getIndex(BuildContext context, Offset globalPosition) { //拿到點選小部件的盒子,也就是索引條 RenderBox box = context.findRenderObject() as RenderBox; // 拿到 y 值, globalToLocal 當前位置距離索引條左上角 (0, 0) 位置的距離 (x, y) double y = box.globalToLocal(globalPosition).dy; //算出字元的高度 var itemHeight = screenHight(context) / 2 / INDEX_WORDS.length; //算出第幾個 item int index = (y ~/ itemHeight).clamp(0, INDEX_WORDS.length - 1); return INDEX_WORDS[index]; }

onVerticalDragUpdate: (DragUpdateDetails details){ print(_getIndex(context, details.globalPosition)); },

當我們上下拖拽索引條的時候,需要獲取到點選位置對應的索引字元,我們通過 context.findRenderObject() as RenderBox 能拿到當前的部件,也就是索引條,然後通過 box.globalToLocal 可以得到當前觸控位置距離索引條左上角 (0, 0) 位置的距離 (x, y),這樣就可以得到了 y 值,因為知道了索引條的高度,索引就可以計算出每個索引字元對應部件的高度,就能得到點選了第幾個索引,也就能通過 INDEX_WORDS[index] 得到點選的字元。最後需要通過 clampindex 做個取值範圍的判斷。

全域性變數的抽取

image.png

這裡我們定義了一個 const 檔案,對全域性的變數程序了抽取。