概述
鄙人于閑暇之日,自學
Flutter已有兩月之久,古人曰:百聞不如一見,百見不如一試,特此利用生平之所學,實戰(zhàn)微信以項目。Flutter,學語法之輕易,用組件之簡單,源碼開源,插件豐富。然一份代碼,卻可完美運行于iOS和Android之上,其運行流暢,且效果杠杠,豈不拍案叫絕,牛B轟轟~。-
如有
iOS、Android、Web開發(fā)之經(jīng)驗,聯(lián)想之前之所學,類比之前之所用,除寫法不同,但語法通用,若多加練習,定能快速上手,耳熟藍翔,不多逼逼,推薦以下之文檔。 此文作
微信通訊錄以文章,雖功能看似簡單,但內(nèi)含技術豐富,且功能十分有趣。作為初學Flutter,拿其小試牛刀,必將初有成效。于Flutter而言, 鄙人也算是初生牛犢不怕虎,并非是天神下凡一錘五。當然,筆者必將知無不言、言無不盡,梳理實戰(zhàn)過程之問題,總結解決問題之方案,讓大家知其然,知其所以然。望能拋玉引磚,擺渡眾生,如有紕漏,還望斧正。源碼地址:flutter_wechat
效果圖
列表 |
索引 |
側滑 |
|---|---|---|
![]() contacts_page_0.png
|
![]() contacts_page_1.png
|
![]() contacts_page_2.png
|
列表
一、功能分析
搭建通訊錄之列表,其知識點涵蓋A-Z 索引Bar、懸停效果view、自定義Header、索引聯(lián)動 、漢字轉拼音,若想實現(xiàn)前面之功能,這里推薦以下之插件,好風憑借力,送我上青云。
-
azlistview 實現(xiàn)
A-Z 索引Bar、懸停效果view、自定義Header、索引聯(lián)動 -
lpinyin 實現(xiàn)
漢字轉拼音
關于具體其使用,還請下載其Demo,運行于電腦之上,查看其運行效果,在此就不多逼逼。
二、數(shù)據(jù)配置
// 獲取聯(lián)系人列表
Future fetchContacts() async {
// 先清除掉數(shù)據(jù)
_contactsList.clear();
_contactsMap.clear();
// 獲取用戶信息列表
final jsonStr =
await rootBundle.loadString(Constant.mockData + 'contacts.json');
// contactsJson
final List contactsJson = json.decode(jsonStr);
// 遍歷
contactsJson.forEach((json) {
final User user = User.fromJson(json);
_contactsList.add(user);
_contactsMap[user.idstr] = user;
});
for (int i = 0, length = _contactsList.length; i < length; i++) {
String pinyin = PinyinHelper.getPinyinE(_contactsList[i].screenName);
String tag = pinyin.substring(0, 1).toUpperCase();
_contactsList[i].screenNamePinyin = pinyin;
if (RegExp("[A-Z]").hasMatch(tag)) {
_contactsList[i].tagIndex = tag;
} else {
_contactsList[i].tagIndex = "#";
}
}
// 根據(jù)A-Z排序
SuspensionUtil.sortListBySuspensionTag(_contactsList);
// 返回數(shù)據(jù)
return _contactsList;
}
三、UI搭建
由azlistview組件提供的API或Property可知,需要提供以下之部件(Widget):
// 列表中某一個 item 部件
itemBuilder: (context, model) => _buildListItem(model),
// 頂部懸浮的Widget
suspensionWidget: _buildSusWidget(_suspensionTag, isFloat: true),
// 自定義header
header: AzListViewHeader(
// - [特殊字符](https://blog.csdn.net/cfxy666/article/details/87609526)
// - [特殊字符](http://www.fhdq.net/)
tag: "♀",
height: 5 * _itemHeight,
builder: (context) {
return _buildHeader();
},
),
// IndexBar 這個可以不寫,使用默認的IndexBar
indexBarBuilder: (context, tagList, onTouch){},
// 自定義 點擊IndexBar 中的某個 tag,放大顯示在屏幕中間的 hint,必須showIndexHint: true, 默認就是true
indexHintBuilder: (context, hint) {
return Container(
alignment: Alignment.center,
width: 80.0,
height: 80.0,
decoration: BoxDecoration(color: Color(0xFFC7C7CB), shape: BoxShape.circle),
child:Text(hint, style: TextStyle(color: Colors.white, fontSize: 30.0)),
);
},
具體UI搭建,這里不多贅述,還請移駕鄙人提供的Demo,翻閱查看其代碼。這里筆者以自定義懸浮View和組頭View為例,穿針引線,搭建符合要求之UI。效果圖如下所示:

A:懸浮ViewB:組頭View
代碼實現(xiàn):
/// 構建懸浮部件
/// [susTag] 標簽名稱
/// [isFloat] 是否懸浮 默認是 false
Widget _buildSusWidget(String susTag, {bool isFloat = false}) {
return Container(
height: _suspensionHeight.toDouble(),
padding: EdgeInsets.only(left: ScreenUtil.getInstance().setWidth(51.0)),
decoration: BoxDecoration(
color: isFloat ? Colors.white : Style.pBackgroundColor,
border: isFloat
? Border(bottom: BorderSide(color: Color(0xFFE6E6E6), width: 0.5))
: null,
),
alignment: Alignment.centerLeft,
child: Text(
'$susTag',
softWrap: false,
style: TextStyle(
fontSize: ScreenUtil.getInstance().setSp(39.0),
color: isFloat ? Style.pTintColor : Color(0xff777777),
),
),
);
}
四、特別提醒
-
azlistview中要求itemCell、懸停View、自定義的Header、以及IndexBar中每個tag的高度必須是int類型且不可動態(tài)修改。如涉及屏幕適配,還請向上(下)取整。
/// 懸浮view 高度 向上取整
int _suspensionHeight =
(ScreenUtil.getInstance().setHeight(99.0) as double).ceil();
/// 每個item 高度 向上取整
int _itemHeight =
(ScreenUtil.getInstance().setHeight(168.0) as double).ceil();
-
AzListView:只是對SuspensionView & IndexBar的封裝,方便使用罷了,爾等完全可以使用SuspensionView & IndexBar定制更加豐富的UI效果。
索引條
一、功能分析
由于,AzListView提供的IndexBar并不滿足微信通訊錄的要求,需求驅動生產(chǎn),不可墨守成規(guī),爾等可運行以下代碼,查看默認和自定義的效果對比,爾等方能辨雌雄。
/// 構建聯(lián)系人列表
/// [defaultMode] 是否使用默認的IndexBar
Widget _buildContactsList({bool defaultMode = false}) {
if (defaultMode) {
return _buildDefaultIndexBarList();
} else {
return _buildCustomIndexBarList();
}
}
功能對比
| 類型 | Custom | Default |
|---|---|---|
| 效果 | ![]() contacts_page_1.png
|
![]() contacts_page_4.png
|
| 組件 | AzListView | AzListView |
| 條件 |
showIndexHint: false, indexBarBuilder: (_, _, _) => MHIndexBar()
|
showIndexHint: true, |
| 功能 |
1、列表和IndexBar能相互聯(lián)動 2、IndexBar當前選中的Tag高亮 3、手指觸碰IndexBar中Tag, 彈出指向該Tag的氣泡4、通過設置ignoreTags屬性,控制其中某個Tag,不高亮,不彈氣泡 4、通過設置mapTag和mapSelTag,可以將某個tag映射稱自定義的默認或選中樣式,eg: ♀ =>
|
1、只能通過IndexBar聯(lián)動列表,反之不行 2、手指觸碰IndexBar中Tag, 彈出屏幕居中的氣泡 ????????????????????????????????????????????????????????????3、能控制某個Tag不彈氣泡???????????? |
二、魔改源碼
考慮到只是在AzListView系統(tǒng)提供的IndexBar上新增一些功能,故筆者完全復制IndexBar之源碼,在其基礎之上,新增功能罷了,可謂是借東風之力,成曠世之業(yè)。再此著重講講思路,若爾等想追根溯源,還以移駕/components/index_bar/mh_index_bar.dart查看源碼。
- 列表滾動聯(lián)動
IndexBar標簽(tag)滾動功能實現(xiàn)
該功能的實現(xiàn),需要IndexBar提供一個tag屬性即可。 具體代碼實現(xiàn)如下
/// list.dart 索引標簽改變
void _onSusTagChanged(String tag) {
setState(() {
_suspensionTag = tag;
});
}
/// 傳遞改變的tag 給 IndexBar
MHIndexBar(
tag: _suspensionTag,
)
/// mh_index_bar.dart 處理列表傳經(jīng)來的tag
// 配置 當前 _indexModel, tag可能是用戶滾動列表的傳進來數(shù)據(jù),導致tag不一致
if (widget.tag != null &&
widget.tag.isNotEmpty &&
widget.tag != _indexModel.tag) {
_indexModel.tag = widget.tag;
_indexModel.isTouchDown = false;
_indexModel.position = widget.data.indexOf(widget.tag);
}
-
IndexBar中選中tag高亮,配置某個tag不高亮、配置某個tag映射其他部件,例如:♀ =>功能實現(xiàn)
選中tag高亮: 可以通過IndexBar內(nèi)部提供的私有對象_indexModel得知哪個tag高亮, 即 _indexModel.tag == tag 則此tag選中。
配置某個tag不高亮: IndexBar提供一個List<String> ignoreTags屬性,讓用戶去設置哪些標簽不高亮。 例如:ignoreTags: ['♀'],,可得知♀這個標簽不高亮。
配置某個tag映射其他部件,例如:♀ =>: IndexBar提供一個默認的Map<String, Widget> mapTag和一個選中(高亮)的Map<String, Widget> mapSelTag來映射某個tag默認和高亮的部件。當然,如有需要還需配置一個彈出氣泡的隱射部件Map<String, Widget> mapHintTag。
以上功能實現(xiàn)所需屬性如下:
/// 當前高亮顯示的標簽
final String tag;
/// 忽略的Tags,這些忽略Tag, 不會高亮顯示,點擊或長按 不會彈出 tagHint
final List<String> ignoreTags;
/// 針對某個Tag顯示其他部件的映射,一般都是映射 圖片/svg
final Map<String, Widget> mapTag;
/// 針對某個Tag顯示高亮其他部件的映射,一般都是映射 圖片/svg
final Map<String, Widget> mapSelTag;
/// 長按彈出氣泡顯示的內(nèi)容,一般都是映射 圖片/svg
final Map<String, Widget> mapHintTag;
以上功能實現(xiàn)代碼邏輯如下:<注意注釋>
/// 獲取標簽tag背景色
Color _fetchColor(String tag) {
if (_indexModel.tag == tag) {
final List<String> ignoreTags = widget.ignoreTags ?? [];
return ignoreTags.indexOf(tag) != -1
? widget.tagColor ?? Colors.transparent
: widget.selectedTagColor ?? Color(0xFF07C160);
}
return widget.tagColor ?? Colors.transparent;
}
/// 構建某個tag的部件
Widget _buildTagWidget(String tag) {
// 當前選中的tag, 也就是高亮的場景
if (_indexModel.tag == tag) {
final List<String> ignoreTags = widget.ignoreTags ?? [];
final isIgnore = ignoreTags.indexOf(tag) != -1;
// 如果是忽略
if (isIgnore) {
// 獲取mapTag
if (widget.mapTag != null && widget.mapTag[tag] != null) {
// 返回映射的部件
return widget.mapTag[tag];
} else {
// 返回默認的部件
return Text(
tag,
textAlign: TextAlign.center,
style: widget.textStyle ??
TextStyle(
fontSize: 10.0,
color: Color(0xFF555555),
fontWeight: FontWeight.w500,
),
);
}
} else {
// 不忽略,則顯示高亮組件
if (widget.mapSelTag != null && widget.mapSelTag[tag] != null) {
// 返回映射高亮的部件
return widget.mapSelTag[tag];
} else if (widget.mapTag != null && widget.mapTag[tag] != null) {
// 返回映射默認的部件
return widget.mapTag[tag];
} else {
// 返回默認的部件
return Text(
tag,
textAlign: TextAlign.center,
style: widget.selectedTextStyle ??
TextStyle(
fontSize: 10.0,
color: Colors.white,
fontWeight: FontWeight.w500,
),
);
}
}
}
// 非高亮場景
// 獲取mapTag
if (widget.mapTag != null && widget.mapTag[tag] != null) {
// 返回映射的部件
return widget.mapTag[tag];
} else {
// 返回默認的部件
return Text(
tag,
textAlign: TextAlign.center,
style: widget.textStyle ??
TextStyle(
fontSize: 10.0,
color: Color(0xFF555555),
fontWeight: FontWeight.w500,
),
);
}
}
- 手指按住某
tag,彈出氣泡hint的功能實現(xiàn)。
相比AzListView默認提供的一個屏幕居中的indexBarHint,自定義的indexBarHint,則是在手指按下的某個tag的左側彈出一個hint,且兩者中心點水平平行,其效果更加靈性而不失端莊,俏皮且略顯可愛。
開局一張圖,內(nèi)容全靠編。

由上圖可知,考慮到hint(紅色)和長按tag(藍色)水平居中且跟隨移動,這里采用Stack + Positioned來布局tag和hint,由于要保證長按or點擊tag,才彈出hint,所以需要使用Offstage組件。注意:一定要設置Stack的overflow: Overflow.visible,為可見。偽代碼實現(xiàn)如下:
Stack(
// 設置超出部分可見 必須設置
overflow: Overflow.visible,
children: <Widget>[
// 標簽組件
TagWidget,
// Hint組件
Positioned(
left: -80.0,
top: -17.0,
child: Offstage(
// 長按或點擊: false(顯示) ; 其他則為: true(隱藏)
offstage: true/false,
child: HintWidget,
)
)
],
),
水平靠左居中,偽代碼實現(xiàn).
// 靠左 hintW = 60, spaceX = 20
left: -(HintW + spaceX),
// 水平居中 HintH = 50, TagH = 16
top: -(HintH - TagH) * 0.5,
這里以布局Hint為例,代碼實現(xiàn)如下。
/// 構建indexBar hint
Widget _buildIndexBarHintWidget(
BuildContext context, String tag, IndexBarDetails indexModel) {
// 如果外界自定義 indexbarHint
if (widget.indexBarHintBuilder != null) {
return widget.indexBarHintBuilder(context, tag, indexModel);
} else {
return Positioned(
left: -(60 + widget.hintOffsetX ?? 20),
top: -(50 - widget.itemHeight) * 0.5,
child: Offstage(
offstage: _fetchOffstage(tag),
child: Container(
width: 60.0,
height: 50.0,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(
'assets/images/contacts/ContactIndexShape_60x50.png'),
fit: BoxFit.contain,
),
),
alignment: Alignment(-0.25, 0.0),
child: _buildHintChildWidget(tag),
),
),
);
}
}
// 獲取Offstage 是否隱居幕后
bool _fetchOffstage(String tag) {
if (_indexModel.tag == tag) {
final List<String> ignoreTags = widget.ignoreTags ?? [];
return ignoreTags.indexOf(tag) != -1 ? true : !_indexModel.isTouchDown;
}
return true;
}
/// 構建某個hint中子部件
Widget _buildHintChildWidget(String tag) {
if (widget.mapHintTag != null && widget.mapHintTag[tag] != null) {
// 返回映射高亮的部件
return widget.mapHintTag[tag];
}
return Text(
tag,
style: TextStyle(
color: Colors.white70,
fontSize: 30.0,
fontWeight: FontWeight.w700,
),
);
}
- 自定義
標簽和自定義Hint的樣式
當然筆者為自定義的mh_index_bar提供了許多可配置的屬性,基本上能滿足類似微信聯(lián)系人這樣的IndexBar,具體各個屬性的使用,這里就不一一贅述了,有興趣的童鞋可以自行查看。
當然,如果你想定制更加花里胡哨的需求,且筆者提供的屬性也無法滿足時。莫慌,筆者也暴露了兩個方法,由用戶自行去構建標簽和Hint的部件。 API如下
/// Called to build index hint. 自定義氣泡彈出Hint
/// [tag] 標簽值
/// [indexModel] 當前選中的標簽Model
typedef Widget IndexBarHintBuilder(
BuildContext context, String tag, IndexBarDetails indexModel);
/// Called to build index tag. 自定義氣標簽
typedef Widget IndexBarTagBuilder(
BuildContext context, String tag, IndexBarDetails indexModel);
關于這兩個API的實現(xiàn),筆者已經(jīng)在 /views/contacts/contacts_page.dart里面實現(xiàn)了,且只要運行代碼,默認就是通過這連個API構建。
三種場景的效果圖對比如下。<PS:圖三、多個氣泡只是用來證明自定義樣式Hint罷了,然并卵~>
| 默認 | 自定義(屬性) | 自定義(Builder) |
|---|---|---|
![]() contacts_page_4.png
|
![]() contacts_page_1.png
|
![]() contacts_page_6.png
|
側滑(備注)
一、功能分析
聯(lián)系人右邊側滑展開備注的功能。這里還是借助下面的插件來實現(xiàn),站在巨人的肩膀上編程。關于具體使用,還請查看插件的提供的Example。
** 二、代碼實現(xiàn) **
利用flutter_slidable插件,很快將之前的cell的具有側滑功能,偽代碼實現(xiàn)如下:
// cell
Widget listTile = MHListTile();
// 頭部是不需要側滑的(新的朋友、群聊、標簽、備注)
if (!needSlidable) {
return listTile;
}else{
// 這樣就具備了側滑
return Slidable(
child: listTile;
)
}
三、問題處理
flutter_slidable雖然引用和切入到已有代碼,非常的細膩絲滑,讓人嫉妒舒適。但是,為了完完全全實現(xiàn)微信通訊錄的功能,其中還是遇到了少許問題,這里筆者一一記錄以及處理心得。
每一個
Slidable必須設置一個key且不能為null,否則報錯。例如:Slidable(key: Key(title))。不需要組件默認提供的側滑到最左側,執(zhí)行
dismiss事件。
默認該組件側滑到最左側,會執(zhí)行onDismissed回調,如果不寫,程序會閃退。代碼如下:
Slidable(
// 必須的有key
key: Key(title),
dismissal: SlidableDismissal(
child: SlidableDrawerDismissal(),
onDismissed: (actionType) {
/// 一般都是 刪除這個cell, 如果啥都不干,則運行報錯
},
),
由于這是系統(tǒng)默認的事件,且SlidableDismissal提供了一個屬性(dragDismissible)來阻止這個默認事件。只需要設置為dragDismissible: false即可。
這個方法雖然是解決了拖拽到最左側,調用Dismiss事件,但是,隨即帶來的是,側滑失去了原有的彈性效果,變得非常的死板和呆滯,瞬間失去了靈魂一般,得不償失。我們要的是:能側滑到左側回彈,且不執(zhí)行dismiss事件。
翻閱SlidableDismissal提供的屬性,驚奇的發(fā)現(xiàn)onWillDismiss屬性,查看其注釋便知,這不正是我們要的滑板鞋?!。
/// Called before the widget is dismissed. If the call returns false, the
/// item will not be dismissed.
///
/// If null, the widget will always be dismissed.
final SlideActionWillBeDismissed onWillDismiss;
所以最終解決方案如下:
Slidable(
// 必須的有key
key: Key(title),
dismissal: SlidableDismissal(
closeOnCanceled: false, // 取消 dismiss事件后,是否關閉item ,默認是不關閉
dragDismissible: true, // 必須為true,否則沒側滑回彈動畫
child: SlidableDrawerDismissal(),
onWillDismiss: (actionType) {
return false; // 告訴系統(tǒng),吾不死,爾等終究是臣
},
),
- 側滑時,禁止掉按下
cell置灰(高亮)的效果。
默認情況下,按下或點擊某個Cell時,該Cell會展示高亮(置灰)的效果,以此告知用戶具體按下哪個Cell。但是當我們側滑或側滑展開時,再去點擊Cell,不應該有這種高亮(置灰)的效果,否則,有點喧賓奪主的感覺。
解決方案:監(jiān)聽Slidable是否展開,來判斷Cell是否需要點擊高亮的效果。具體代碼如下:
// 配置側滑監(jiān)聽
ScrollController _slidableController = SlidableController(
onSlideIsOpenChanged: _handleSlideIsOpenChanged,
);
// 監(jiān)聽側滑展開與否
void _handleSlideIsOpenChanged(bool isOpen) {
setState(() {
_slideIsOpen = isOpen;
});
}
// Cell
Widget listTile = MHListTile(
// 不需要側滑的cell,還是默認可點擊,如果需要側滑的cell,側滑展開,則不可點擊,否則,可點擊
allowTap: !_slideIsOpen || !needSlidable,
);
// Slidable
Slidable(
// 必須的有key
key: Key(title),
controller: _slidableController,
);
- 手動(程序)關閉上一個展開的側滑部件(
Cell)。
程序關閉或展開某個Cell,這里用到組件提供的兩個API:void close();和void open({SlideActionType actionType});
具體關閉和展開某個Cell的代碼實現(xiàn)如下:
Slidable.of(context)?.open();
Slidable.of(context)?.close();
特別提醒的是: Slidable.of(context)中的context必須是 Slidable.child的context。否則調用沒效果。
// Slidable
Slidable(
// 必須的有key
key: Key(title),
controller: _slidableController,
child: ItemWidget(),
);
// Slidable 的 child
class ItemWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
// 特別注意這里的context,如果你是封裝的組件,還請點擊事件中 將context回調出去!!!! SlidableRenderingMode.none 證明此cell未展開
onTap: () =>
Slidable.of(context)?.renderingMode == SlidableRenderingMode.none
? Slidable.of(context)?.open()
: Slidable.of(context)?.close(),
child: Text('Hello world'),
);
}
}
上面的代碼實現(xiàn)的效果是:點擊 A Cell,則A Cell 展開或關閉 側滑。
但是,我們希望的效果是,如果A Cell是關閉狀態(tài)時,點擊 A Cell 是下鉆到用戶信息頁面。實現(xiàn)代碼如下:
// Cell
Widget listTile = MHListTile(
// 由于筆者是封裝組件,所以點擊事件中,將 context 回調出來
onTapValue: (cxt) {
// 該cell處于關閉狀態(tài), 直接下鉆到 用戶信息頁面
if (Slidable.of(cxt)?.renderingMode == SlidableRenderingMode.none) {
// 下鉆 用戶信息
NavigatorUtils.push(cxt,'${ContactsRouter.contactInfoPage}?idstr=${user.idstr}');
}else{
Slidable.of(cxt)?.close();
}
},
);
上面的代碼只是針對同一個Cell(A Cell)的點擊事件處理邏輯罷了。如果和其他Cell(B Cell)連用,就會出現(xiàn)問題。
以A Cell 和 B Cell為例,理想(現(xiàn)實)場景如下:
-
同Cell點擊場景
- 當點擊
A Cell時,若A Cell是側滑關閉狀態(tài)時,則下鉆A的用戶信息頁面; 若A Cell是側滑展開狀態(tài)時,則關閉A Cell的側滑; - 當點擊
B Cell時,邏輯同上。
- 當點擊
-
不同Cell點擊場景
- 若
A Cell和B Cell都是側滑關閉狀態(tài)時,點擊哪個Cell,則下鉆哪個Cell對應的用戶信息頁面. - 不可能出現(xiàn)
A Cell和B Cell都是側滑展開狀態(tài)的場景。 - 若
A Cell是側滑展開狀態(tài)時,當點擊B Cell時,則關閉A Cell的側滑,下鉆到B的用戶信息頁面. - 若
B Cell是側滑展開狀態(tài)時,當點擊A Cell時,則關閉B Cell的側滑,下鉆到A的用戶信息頁面.
- 若
俗話說:理想很豐滿,現(xiàn)實很骨感?,F(xiàn)實場景是:若A Cell是側滑展開狀態(tài)時,當點擊B Cell時,能下鉆到B的用戶信息頁面,但A Cell是不會自動關閉側滑,還是會保持側滑展開狀態(tài).
事故產(chǎn)生的最主要原因是:當點擊B Cell時,我們無法拿到A Cell的context。
知道了事故原因了,那么解決問題就變得得心應手了,這里講講筆者的幾種擺渡眾生解決方案。(PS:小伙伴們有更好的解決方案,歡迎文末評論留言!?。。?/p>
方案一:打開一個空的左側滑(黑魔法)
首先,Slidable是支持左側滑和右側滑,其對應的屬性為: List<Widget> actions和List<Widget> secondaryActions,但是目前需求我們只需要右側滑罷了,
其次,我們知道: 不可能出現(xiàn)A Cell 和 B Cell都是側滑展開狀態(tài)的場景。
所以,若A Cell是右側滑展開狀態(tài)時,當點擊B Cell時,我們打開B Cell的一個空的左側滑,即:Slidable.of(cxt)?.open(actionType: SlideActionType.primary);,
因為B Cell的actions是一個空數(shù)組,所以界面并沒有發(fā)生變化,且能將A Cell的右側滑關閉。
局限性:首先,該方案適合沒有左側滑的場景;其次,我們手動打開一個空的左側滑,雖然界面沒有變化,但是SlidableController.onSlideIsOpenChanged回調的isOpen一直為true,如果有些場景需要使用這個isOpen屬性,那么勢必會產(chǎn)生問題;
最后,若A Cell是右側滑展開狀態(tài)時,我們不是點擊B Cell,而是點擊導航欄上的按鈕下鉆的場景,該方案也不適合。
方案一的功能代碼實現(xiàn)如下:
// Cell
Widget listTile = MHListTile(
// 由于筆者是封裝組件,所以點擊事件中,將 context 回調出來
onTapValue: (cxt) {
// 該cell處于關閉狀態(tài), 直接下鉆到 用戶信息頁面
if (Slidable.of(cxt)?.renderingMode == SlidableRenderingMode.none) {
// 方案一: 針對cell點擊 和下鉆容易處理 但是一但 點擊導航欄上的 添加聯(lián)系人按鈕 ,因為獲取不到 cxt 而力不從心
// 細節(jié):這里由于 SlideActionType.primary 對應 actions 為空,所以雖然看似展開空,目的就是關閉 上一個打開的 secondary action
Slidable.of(cxt)?.open(actionType: SlideActionType.primary);
// 上面的雖然打開了一個空的 但是系統(tǒng)還是會認為是 打開的 也就是 _slideIsOpen = true
// 手動設置為false
_slideIsOpen = false;
// 下鉆 用戶信息
NavigatorUtils.push(cxt,'${ContactsRouter.contactInfoPage}?idstr=${user.idstr}');
}else{
Slidable.of(cxt)?.close();
}
},
);
方案二:每生成一個Cell,就將其Cell對應的context記錄起來。Map[key] = cxt;
該方案的核心點就是使用: Map,而不是使用List或Set。一旦我們將每一個Cell的context記錄在案,那么我們就可以遍歷出每一個cxt的狀態(tài),從而將某個context關閉。
分析:首先,方案的實用性,遠遠高于方案一的且完美解決了方案一的存在局限性。其次,數(shù)據(jù)量一旦過大,每次遍歷可能存在一定的性能問題,注意這里只是可能。
方案二的功能代碼實現(xiàn)如下:
// Cell
Widget listTile = MHListTile(
// 由于筆者是封裝組件,所以點擊事件中,將 context 回調出來
onTapValue: (cxt) {
// 沒有側滑展開項 就直接下鉆
if (!_slideIsOpen) {
NavigatorUtils.push(cxt,
'${ContactsRouter.contactInfoPage}?idstr=${user.idstr}');
return;
}
// 該cell處于關閉狀態(tài), 直接下鉆到 用戶信息頁面
if (Slidable.of(cxt)?.renderingMode == SlidableRenderingMode.none) {
// 關閉上一個側滑
_closeSlidable();
// 下鉆 用戶信息
NavigatorUtils.push(cxt,'${ContactsRouter.contactInfoPage}?idstr=${user.idstr}');
}else{
Slidable.of(cxt)?.close();
}
},
// 回調context
callbackContext: (BuildContext cxt) {
_slidableCxtMap[title] = cxt;
},
);
/// 關閉slidable
void _closeSlidable() {
// 容錯處理
if (!_slideIsOpen) return;
final cxts = _slidableCxtMap.values.toList();
final len = cxts.length;
for (var i = 0; i < len; i++) {
final value = cxts[i];
if (Slidable.of(value)?.renderingMode != SlidableRenderingMode.none) {
// 關掉上一個
Slidable.of(value)?.close();
return;
}
}
}
方案三:使用 SlidableController.activeState
這個是筆者閱讀源碼,偶然發(fā)現(xiàn)的屬性。
方案三的功能代碼實現(xiàn)如下:
// Cell
Widget listTile = MHListTile(
// 由于筆者是封裝組件,所以點擊事件中,將 context 回調出來
onTapValue: (cxt) {
// 沒有側滑展開項 就直接下鉆
if (!_slideIsOpen) {
NavigatorUtils.push(cxt,
'${ContactsRouter.contactInfoPage}?idstr=${user.idstr}');
return;
}
// 該cell處于關閉狀態(tài), 直接下鉆到 用戶信息頁面
if (Slidable.of(cxt)?.renderingMode == SlidableRenderingMode.none) {
// 關閉上一個側滑
// 方案三: 直接拿這個activaState
_slidableController.activeState?.close();
// 下鉆 用戶信息
NavigatorUtils.push(cxt,'${ContactsRouter.contactInfoPage}?idstr=${user.idstr}');
}else{
Slidable.of(cxt)?.close();
}
},
);
總結
首先,微信通訊錄雖然看似只有搭建列表、自定義IndexBar、側滑備注等三大功能模塊,但是內(nèi)部涵蓋的一些知識點和細節(jié)處理還需要各位親自體驗;而且也怪筆者才疏學淺,核心功能都是借助第三方插件來實現(xiàn)的,再此表示抱歉。
其次,本模塊的核心點主要落在: 自定義IndexBar 和 解決側滑關閉 上。 幸運的是,筆者相信在這兩個核心點上解釋的已經(jīng)足夠詳細,希望大家都過閱讀文章以及結合代碼,能夠領會筆者想表達的意圖和良苦用心。不求膜拜,只求點贊。
最后,希望大家通過閱讀本文,自己也能夠動手寫一個Flutter版本的微信通訊錄,從而激發(fā)你的學習動力,提升你的學習樂趣。
期待
- 文章若對您有些許幫助,請給個喜歡??,畢竟碼字不易;若對您沒啥幫助,請給點建議,切記學無止境。
- 針對文章所述內(nèi)容,閱讀期間任何疑問;請在文章底部評論指出,我會火速解決和修正問題。
- GitHub地址:https://github.com/CoderMikeHe
- 源碼地址:flutter_wechat




