概述
鄙人于閑暇之日,自學(xué)
Flutter
已有兩月之久,古人曰:百聞不如一見(jiàn),百見(jiàn)不如一試
,特此利用生平之所學(xué),實(shí)戰(zhàn)微信以項(xiàng)目。Flutter
,學(xué)語(yǔ)法之輕易,用組件之簡(jiǎn)單,源碼開(kāi)源,插件豐富。然一份代碼,卻可完美運(yùn)行于iOS和Android之上,其運(yùn)行流暢,且效果杠杠,豈不拍案叫絕,牛B轟轟~。-
如有
iOS
、Android
、Web
開(kāi)發(fā)之經(jīng)驗(yàn),聯(lián)想之前之所學(xué),類(lèi)比之前之所用,除寫(xiě)法不同,但語(yǔ)法通用,若多加練習(xí),定能快速上手,耳熟藍(lán)翔,不多逼逼,推薦以下之文檔。 此文作
微信通訊錄
以文章,雖功能看似簡(jiǎn)單,但內(nèi)含技術(shù)豐富,且功能十分有趣。作為初學(xué)Flutter
,拿其小試牛刀,必將初有成效。于Flutter
而言, 鄙人也算是初生牛犢不怕虎
,并非是天神下凡一錘五
。當(dāng)然,筆者必將知無(wú)不言、言無(wú)不盡
,梳理實(shí)戰(zhàn)過(guò)程之問(wèn)題,總結(jié)解決問(wèn)題之方案,讓大家知其然,知其所以然
。望能拋玉引磚,擺渡眾生,如有紕漏,還望斧正。源碼地址:flutter_wechat
效果圖
列表 |
索引 |
側(cè)滑 |
---|---|---|
contacts_page_0.png
|
contacts_page_1.png
|
contacts_page_2.png
|
列表
一、功能分析
搭建通訊錄之列表,其知識(shí)點(diǎn)涵蓋A-Z 索引Bar
、懸停效果view
、自定義Header
、索引聯(lián)動(dòng)
、漢字轉(zhuǎn)拼音
,若想實(shí)現(xiàn)前面之功能,這里推薦以下之插件,好風(fēng)憑借力,送我上青云。
-
azlistview 實(shí)現(xiàn)
A-Z 索引Bar
、懸停效果view
、自定義Header
、索引聯(lián)動(dòng)
-
lpinyin 實(shí)現(xiàn)
漢字轉(zhuǎn)拼音
關(guān)于具體其使用,還請(qǐng)下載其Demo,運(yùn)行于電腦之上,查看其運(yùn)行效果,在此就不多逼逼。
二、數(shù)據(jù)配置
// 獲取聯(lián)系人列表
Future fetchContacts() async {
// 先清除掉數(shù)據(jù)
_contactsList.clear();
_contactsMap.clear();
// 獲取用戶(hù)信息列表
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);
// 返回?cái)?shù)據(jù)
return _contactsList;
}
三、UI搭建
由azlistview
組件提供的API
或Property
可知,需要提供以下之部件(Widget
):
// 列表中某一個(gè) 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 這個(gè)可以不寫(xiě),使用默認(rèn)的IndexBar
indexBarBuilder: (context, tagList, onTouch){},
// 自定義 點(diǎn)擊IndexBar 中的某個(gè) tag,放大顯示在屏幕中間的 hint,必須showIndexHint: true, 默認(rèn)就是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搭建
,這里不多贅述,還請(qǐng)移駕鄙人提供的Demo,翻閱查看其代碼。這里筆者以自定義懸浮View
和組頭View
為例,穿針引線,搭建符合要求之UI。效果圖如下所示:
A:懸浮View
B:組頭View
代碼實(shí)現(xiàn):
/// 構(gòu)建懸浮部件
/// [susTag] 標(biāo)簽名稱(chēng)
/// [isFloat] 是否懸浮 默認(rèn)是 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
中每個(gè)tag
的高度必須是int
類(lèi)型且不可動(dòng)態(tài)修改。如涉及屏幕適配
,還請(qǐng)向上(下)取整
。
/// 懸浮view 高度 向上取整
int _suspensionHeight =
(ScreenUtil.getInstance().setHeight(99.0) as double).ceil();
/// 每個(gè)item 高度 向上取整
int _itemHeight =
(ScreenUtil.getInstance().setHeight(168.0) as double).ceil();
-
AzListView
:只是對(duì)SuspensionView & IndexBar
的封裝,方便使用罷了,爾等完全可以使用SuspensionView & IndexBar
定制更加豐富的UI效果。
索引條
一、功能分析
由于,AzListView
提供的IndexBar
并不滿(mǎn)足微信通訊錄的要求,需求驅(qū)動(dòng)生產(chǎn)
,不可墨守成規(guī),爾等可運(yùn)行以下代碼,查看默認(rèn)和自定義的效果對(duì)比,爾等方能辨雌雄。
/// 構(gòu)建聯(lián)系人列表
/// [defaultMode] 是否使用默認(rèn)的IndexBar
Widget _buildContactsList({bool defaultMode = false}) {
if (defaultMode) {
return _buildDefaultIndexBarList();
} else {
return _buildCustomIndexBarList();
}
}
功能對(duì)比
類(lèi)型 | Custom | Default |
---|---|---|
效果 | contacts_page_1.png
|
contacts_page_4.png
|
組件 | AzListView | AzListView |
條件 |
showIndexHint: false, indexBarBuilder: (_, _, _) => MHIndexBar()
|
showIndexHint: true, |
功能 |
1、列表和IndexBar能相互聯(lián)動(dòng) 2、IndexBar當(dāng)前選中的Tag高亮 3、手指觸碰IndexBar中Tag, 彈出指向該Tag的氣泡 4、通過(guò)設(shè)置ignoreTags屬性,控制其中某個(gè)Tag,不高亮,不彈氣泡 4、通過(guò)設(shè)置mapTag和mapSelTag,可以將某個(gè)tag映射稱(chēng)自定義的默認(rèn)或選中樣式,eg: ♀ =>
|
1、只能通過(guò)IndexBar聯(lián)動(dòng)列表,反之不行 2、手指觸碰IndexBar中Tag, 彈出屏幕居中的氣泡 ????????????????????????????????????????????????????????????3、能控制某個(gè)Tag不彈氣泡 ???????????? |
二、魔改源碼
考慮到只是在AzListView
系統(tǒng)提供的IndexBar
上新增一些功能,故筆者完全復(fù)制IndexBar
之源碼,在其基礎(chǔ)之上,新增功能罷了,可謂是借東風(fēng)之力,成曠世之業(yè)。再此著重講講思路,若爾等想追根溯源,還以移駕/components/index_bar/mh_index_bar.dart
查看源碼。
- 列表滾動(dòng)聯(lián)動(dòng)
IndexBar
標(biāo)簽(tag
)滾動(dòng)功能實(shí)現(xiàn)
該功能的實(shí)現(xiàn),需要IndexBar
提供一個(gè)tag
屬性即可。 具體代碼實(shí)現(xiàn)如下
/// list.dart 索引標(biāo)簽改變
void _onSusTagChanged(String tag) {
setState(() {
_suspensionTag = tag;
});
}
/// 傳遞改變的tag 給 IndexBar
MHIndexBar(
tag: _suspensionTag,
)
/// mh_index_bar.dart 處理列表傳經(jīng)來(lái)的tag
// 配置 當(dāng)前 _indexModel, tag可能是用戶(hù)滾動(dòng)列表的傳進(jìn)來(lái)數(shù)據(jù),導(dǎo)致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高亮
,配置某個(gè)tag不高亮
、配置某個(gè)tag映射其他部件,例如:♀ =>
功能實(shí)現(xiàn)
選中tag高亮
: 可以通過(guò)IndexBar
內(nèi)部提供的私有對(duì)象_indexModel
得知哪個(gè)tag
高亮, 即 _indexModel.tag == tag
則此tag
選中。
配置某個(gè)tag不高亮
: IndexBar
提供一個(gè)List<String> ignoreTags
屬性,讓用戶(hù)去設(shè)置哪些標(biāo)簽不高亮。 例如:ignoreTags: ['♀'],
,可得知♀
這個(gè)標(biāo)簽不高亮。
配置某個(gè)tag映射其他部件,例如:♀ =>
: IndexBar
提供一個(gè)默認(rèn)的Map<String, Widget> mapTag
和一個(gè)選中(高亮)的Map<String, Widget> mapSelTag
來(lái)映射某個(gè)tag
默認(rèn)和高亮的部件。當(dāng)然,如有需要還需配置一個(gè)彈出氣泡的隱射部件Map<String, Widget> mapHintTag
。
以上功能實(shí)現(xiàn)所需屬性如下:
/// 當(dāng)前高亮顯示的標(biāo)簽
final String tag;
/// 忽略的Tags,這些忽略Tag, 不會(huì)高亮顯示,點(diǎn)擊或長(zhǎng)按 不會(huì)彈出 tagHint
final List<String> ignoreTags;
/// 針對(duì)某個(gè)Tag顯示其他部件的映射,一般都是映射 圖片/svg
final Map<String, Widget> mapTag;
/// 針對(duì)某個(gè)Tag顯示高亮其他部件的映射,一般都是映射 圖片/svg
final Map<String, Widget> mapSelTag;
/// 長(zhǎng)按彈出氣泡顯示的內(nèi)容,一般都是映射 圖片/svg
final Map<String, Widget> mapHintTag;
以上功能實(shí)現(xiàn)代碼邏輯如下:<注意注釋
>
/// 獲取標(biāo)簽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;
}
/// 構(gòu)建某個(gè)tag的部件
Widget _buildTagWidget(String tag) {
// 當(dāng)前選中的tag, 也就是高亮的場(chǎng)景
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 {
// 返回默認(rèn)的部件
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) {
// 返回映射默認(rèn)的部件
return widget.mapTag[tag];
} else {
// 返回默認(rèn)的部件
return Text(
tag,
textAlign: TextAlign.center,
style: widget.selectedTextStyle ??
TextStyle(
fontSize: 10.0,
color: Colors.white,
fontWeight: FontWeight.w500,
),
);
}
}
}
// 非高亮場(chǎng)景
// 獲取mapTag
if (widget.mapTag != null && widget.mapTag[tag] != null) {
// 返回映射的部件
return widget.mapTag[tag];
} else {
// 返回默認(rèn)的部件
return Text(
tag,
textAlign: TextAlign.center,
style: widget.textStyle ??
TextStyle(
fontSize: 10.0,
color: Color(0xFF555555),
fontWeight: FontWeight.w500,
),
);
}
}
- 手指按住某
tag
,彈出氣泡hint
的功能實(shí)現(xiàn)。
相比AzListView
默認(rèn)提供的一個(gè)屏幕居中的indexBarHint
,自定義的indexBarHint
,則是在手指按下的某個(gè)tag
的左側(cè)彈出一個(gè)hint
,且兩者中心點(diǎn)水平平行,其效果更加靈性而不失端莊,俏皮且略顯可愛(ài)
。
開(kāi)局一張圖,內(nèi)容全靠編
。
由上圖可知,考慮到hint(紅色)
和長(zhǎng)按tag(藍(lán)色)
水平居中且跟隨移動(dòng),這里采用Stack + Positioned
來(lái)布局tag
和hint
,由于要保證長(zhǎng)按or點(diǎn)擊tag
,才彈出hint
,所以需要使用Offstage
組件。注意:一定要設(shè)置Stack
的overflow: Overflow.visible,
為可見(jiàn)。偽代碼實(shí)現(xiàn)如下:
Stack(
// 設(shè)置超出部分可見(jiàn) 必須設(shè)置
overflow: Overflow.visible,
children: <Widget>[
// 標(biāo)簽組件
TagWidget,
// Hint組件
Positioned(
left: -80.0,
top: -17.0,
child: Offstage(
// 長(zhǎng)按或點(diǎn)擊: false(顯示) ; 其他則為: true(隱藏)
offstage: true/false,
child: HintWidget,
)
)
],
),
水平靠左居中,偽代碼實(shí)現(xiàn).
// 靠左 hintW = 60, spaceX = 20
left: -(HintW + spaceX),
// 水平居中 HintH = 50, TagH = 16
top: -(HintH - TagH) * 0.5,
這里以布局Hint為例,代碼實(shí)現(xiàn)如下。
/// 構(gòu)建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;
}
/// 構(gòu)建某個(gè)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,
),
);
}
- 自定義
標(biāo)簽
和自定義Hint
的樣式
當(dāng)然筆者為自定義的mh_index_bar
提供了許多可配置的屬性,基本上能滿(mǎn)足類(lèi)似微信聯(lián)系人
這樣的IndexBar
,具體各個(gè)屬性的使用,這里就不一一贅述了,有興趣的童鞋可以自行查看。
當(dāng)然,如果你想定制更加花里胡哨
的需求,且筆者提供的屬性也無(wú)法滿(mǎn)足時(shí)。莫慌,筆者也暴露了兩個(gè)方法,由用戶(hù)自行去構(gòu)建標(biāo)簽
和Hint
的部件。 API如下
/// Called to build index hint. 自定義氣泡彈出Hint
/// [tag] 標(biāo)簽值
/// [indexModel] 當(dāng)前選中的標(biāo)簽Model
typedef Widget IndexBarHintBuilder(
BuildContext context, String tag, IndexBarDetails indexModel);
/// Called to build index tag. 自定義氣標(biāo)簽
typedef Widget IndexBarTagBuilder(
BuildContext context, String tag, IndexBarDetails indexModel);
關(guān)于這兩個(gè)API的實(shí)現(xiàn),筆者已經(jīng)在 /views/contacts/contacts_page.dart
里面實(shí)現(xiàn)了,且只要運(yùn)行代碼,默認(rèn)就是通過(guò)這連個(gè)API
構(gòu)建。
三種場(chǎng)景的效果圖對(duì)比如下。<PS:圖三、多個(gè)氣泡只是用來(lái)證明自定義樣式Hint罷了,然并卵~>
默認(rèn) | 自定義(屬性) | 自定義(Builder) |
---|---|---|
contacts_page_4.png
|
contacts_page_1.png
|
contacts_page_6.png
|
側(cè)滑(備注)
一、功能分析
聯(lián)系人右邊側(cè)滑展開(kāi)備注
的功能。這里還是借助下面的插件來(lái)實(shí)現(xiàn),站在巨人的肩膀上編程。關(guān)于具體使用,還請(qǐng)查看插件的提供的Example
。
** 二、代碼實(shí)現(xiàn) **
利用flutter_slidable
插件,很快將之前的cell
的具有側(cè)滑功能,偽代碼實(shí)現(xiàn)如下:
// cell
Widget listTile = MHListTile();
// 頭部是不需要側(cè)滑的(新的朋友、群聊、標(biāo)簽、備注)
if (!needSlidable) {
return listTile;
}else{
// 這樣就具備了側(cè)滑
return Slidable(
child: listTile;
)
}
三、問(wèn)題處理
flutter_slidable
雖然引用和切入到已有代碼,非常的細(xì)膩絲滑,讓人嫉妒舒適。但是,為了完完全全實(shí)現(xiàn)微信通訊錄
的功能,其中還是遇到了少許問(wèn)題,這里筆者一一記錄以及處理心得。
每一個(gè)
Slidable
必須設(shè)置一個(gè)key
且不能為null
,否則報(bào)錯(cuò)。例如:Slidable(key: Key(title))
。不需要組件默認(rèn)提供的側(cè)滑到最左側(cè),執(zhí)行
dismiss
事件。
默認(rèn)該組件側(cè)滑到最左側(cè),會(huì)執(zhí)行onDismissed
回調(diào),如果不寫(xiě),程序會(huì)閃退。代碼如下:
Slidable(
// 必須的有key
key: Key(title),
dismissal: SlidableDismissal(
child: SlidableDrawerDismissal(),
onDismissed: (actionType) {
/// 一般都是 刪除這個(gè)cell, 如果啥都不干,則運(yùn)行報(bào)錯(cuò)
},
),
由于這是系統(tǒng)默認(rèn)的事件,且SlidableDismissal
提供了一個(gè)屬性(dragDismissible
)來(lái)阻止這個(gè)默認(rèn)事件。只需要設(shè)置為dragDismissible: false
即可。
這個(gè)方法雖然是解決了拖拽到最左側(cè),調(diào)用Dismiss
事件,但是,隨即帶來(lái)的是,側(cè)滑失去了原有的彈性效果,變得非常的死板和呆滯,瞬間失去了靈魂一般,得不償失。我們要的是:能側(cè)滑到左側(cè)回彈,且不執(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事件后,是否關(guān)閉item ,默認(rèn)是不關(guān)閉
dragDismissible: true, // 必須為true,否則沒(méi)側(cè)滑回彈動(dòng)畫(huà)
child: SlidableDrawerDismissal(),
onWillDismiss: (actionType) {
return false; // 告訴系統(tǒng),吾不死,爾等終究是臣
},
),
- 側(cè)滑時(shí),禁止掉按下
cell
置灰(高亮)的效果。
默認(rèn)情況下,按下或點(diǎn)擊某個(gè)Cell
時(shí),該Cell
會(huì)展示高亮(置灰)的效果,以此告知用戶(hù)具體按下哪個(gè)Cell
。但是當(dāng)我們側(cè)滑或側(cè)滑展開(kāi)時(shí),再去點(diǎn)擊Cell
,不應(yīng)該有這種高亮(置灰)的效果,否則,有點(diǎn)喧賓奪主的感覺(jué)。
解決方案:監(jiān)聽(tīng)Slidable
是否展開(kāi),來(lái)判斷Cell
是否需要點(diǎn)擊高亮的效果。具體代碼如下:
// 配置側(cè)滑監(jiān)聽(tīng)
ScrollController _slidableController = SlidableController(
onSlideIsOpenChanged: _handleSlideIsOpenChanged,
);
// 監(jiān)聽(tīng)側(cè)滑展開(kāi)與否
void _handleSlideIsOpenChanged(bool isOpen) {
setState(() {
_slideIsOpen = isOpen;
});
}
// Cell
Widget listTile = MHListTile(
// 不需要側(cè)滑的cell,還是默認(rèn)可點(diǎn)擊,如果需要側(cè)滑的cell,側(cè)滑展開(kāi),則不可點(diǎn)擊,否則,可點(diǎn)擊
allowTap: !_slideIsOpen || !needSlidable,
);
// Slidable
Slidable(
// 必須的有key
key: Key(title),
controller: _slidableController,
);
- 手動(dòng)(程序)關(guān)閉上一個(gè)展開(kāi)的側(cè)滑部件(
Cell
)。
程序關(guān)閉或展開(kāi)某個(gè)Cell
,這里用到組件提供的兩個(gè)API
:void close();
和void open({SlideActionType actionType});
具體關(guān)閉和展開(kāi)某個(gè)Cell的代碼實(shí)現(xiàn)如下:
Slidable.of(context)?.open();
Slidable.of(context)?.close();
特別提醒的是: Slidable.of(context)
中的context
必須是 Slidable.child
的context
。否則調(diào)用沒(méi)效果。
// Slidable
Slidable(
// 必須的有key
key: Key(title),
controller: _slidableController,
child: ItemWidget(),
);
// Slidable 的 child
class ItemWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
// 特別注意這里的context,如果你是封裝的組件,還請(qǐng)點(diǎn)擊事件中 將context回調(diào)出去!!!! SlidableRenderingMode.none 證明此cell未展開(kāi)
onTap: () =>
Slidable.of(context)?.renderingMode == SlidableRenderingMode.none
? Slidable.of(context)?.open()
: Slidable.of(context)?.close(),
child: Text('Hello world'),
);
}
}
上面的代碼實(shí)現(xiàn)的效果是:點(diǎn)擊 A Cell
,則A Cell
展開(kāi)或關(guān)閉 側(cè)滑。
但是,我們希望的效果是,如果A Cell
是關(guān)閉狀態(tài)時(shí),點(diǎn)擊 A Cell
是下鉆到用戶(hù)信息頁(yè)面。實(shí)現(xiàn)代碼如下:
// Cell
Widget listTile = MHListTile(
// 由于筆者是封裝組件,所以點(diǎn)擊事件中,將 context 回調(diào)出來(lái)
onTapValue: (cxt) {
// 該cell處于關(guān)閉狀態(tài), 直接下鉆到 用戶(hù)信息頁(yè)面
if (Slidable.of(cxt)?.renderingMode == SlidableRenderingMode.none) {
// 下鉆 用戶(hù)信息
NavigatorUtils.push(cxt,'${ContactsRouter.contactInfoPage}?idstr=${user.idstr}');
}else{
Slidable.of(cxt)?.close();
}
},
);
上面的代碼只是針對(duì)同一個(gè)Cell
(A Cell)的點(diǎn)擊事件處理邏輯罷了。如果和其他Cell
(B Cell)連用,就會(huì)出現(xiàn)問(wèn)題。
以A Cell
和 B Cell
為例,理想(現(xiàn)實(shí))場(chǎng)景如下:
-
同Cell點(diǎn)擊場(chǎng)景
- 當(dāng)點(diǎn)擊
A Cell
時(shí),若A Cell
是側(cè)滑關(guān)閉狀態(tài)時(shí),則下鉆A
的用戶(hù)信息頁(yè)面; 若A Cell
是側(cè)滑展開(kāi)狀態(tài)時(shí),則關(guān)閉A Cell
的側(cè)滑; - 當(dāng)點(diǎn)擊
B Cell
時(shí),邏輯同上。
- 當(dāng)點(diǎn)擊
-
不同Cell點(diǎn)擊場(chǎng)景
- 若
A Cell
和B Cell
都是側(cè)滑關(guān)閉狀態(tài)時(shí),點(diǎn)擊哪個(gè)Cell
,則下鉆哪個(gè)Cell
對(duì)應(yīng)的用戶(hù)信息頁(yè)面. - 不可能出現(xiàn)
A Cell
和B Cell
都是側(cè)滑展開(kāi)狀態(tài)的場(chǎng)景。 - 若
A Cell
是側(cè)滑展開(kāi)狀態(tài)時(shí),當(dāng)點(diǎn)擊B Cell
時(shí),則關(guān)閉A Cell
的側(cè)滑,下鉆到B
的用戶(hù)信息頁(yè)面. - 若
B Cell
是側(cè)滑展開(kāi)狀態(tài)時(shí),當(dāng)點(diǎn)擊A Cell
時(shí),則關(guān)閉B Cell
的側(cè)滑,下鉆到A
的用戶(hù)信息頁(yè)面.
- 若
俗話說(shuō):理想很豐滿(mǎn),現(xiàn)實(shí)很骨感
。現(xiàn)實(shí)場(chǎng)景是:若A Cell
是側(cè)滑展開(kāi)狀態(tài)時(shí),當(dāng)點(diǎn)擊B Cell
時(shí),能下鉆到B
的用戶(hù)信息頁(yè)面,但A Cell
是不會(huì)自動(dòng)關(guān)閉側(cè)滑,還是會(huì)保持側(cè)滑展開(kāi)狀態(tài).
事故產(chǎn)生的最主要原因是:當(dāng)點(diǎn)擊B Cell
時(shí),我們無(wú)法拿到A Cell
的context
。
知道了事故原因了,那么解決問(wèn)題就變得得心應(yīng)手了,這里講講筆者的幾種擺渡眾生解決方案。(PS:小伙伴們有更好的解決方案,歡迎文末評(píng)論留言!!!)
方案一:打開(kāi)一個(gè)空的左側(cè)滑(黑魔法)
首先,Slidable
是支持左側(cè)滑和右側(cè)滑,其對(duì)應(yīng)的屬性為: List<Widget> actions
和List<Widget> secondaryActions
,但是目前需求我們只需要右側(cè)滑
罷了,
其次,我們知道: 不可能出現(xiàn)A Cell
和 B Cell
都是側(cè)滑展開(kāi)狀態(tài)的場(chǎng)景。
所以,若A Cell
是右側(cè)滑展開(kāi)狀態(tài)時(shí),當(dāng)點(diǎn)擊B Cell
時(shí),我們打開(kāi)B Cell
的一個(gè)空的左側(cè)滑
,即:Slidable.of(cxt)?.open(actionType: SlideActionType.primary);
,
因?yàn)?code>B Cell的actions
是一個(gè)空數(shù)組,所以界面并沒(méi)有發(fā)生變化,且能將A Cell
的右側(cè)滑關(guān)閉。
局限性:首先,該方案適合沒(méi)有左側(cè)滑的場(chǎng)景;其次,我們手動(dòng)打開(kāi)一個(gè)空的左側(cè)滑
,雖然界面沒(méi)有變化,但是SlidableController.onSlideIsOpenChanged
回調(diào)的isOpen
一直為true
,如果有些場(chǎng)景需要使用這個(gè)isOpen
屬性,那么勢(shì)必會(huì)產(chǎn)生問(wèn)題;
最后,若A Cell
是右側(cè)滑展開(kāi)狀態(tài)時(shí),我們不是點(diǎn)擊B Cell
,而是點(diǎn)擊導(dǎo)航欄上的按鈕下鉆的場(chǎng)景,該方案也不適合。
方案一的功能代碼實(shí)現(xiàn)如下:
// Cell
Widget listTile = MHListTile(
// 由于筆者是封裝組件,所以點(diǎn)擊事件中,將 context 回調(diào)出來(lái)
onTapValue: (cxt) {
// 該cell處于關(guān)閉狀態(tài), 直接下鉆到 用戶(hù)信息頁(yè)面
if (Slidable.of(cxt)?.renderingMode == SlidableRenderingMode.none) {
// 方案一: 針對(duì)cell點(diǎn)擊 和下鉆容易處理 但是一但 點(diǎn)擊導(dǎo)航欄上的 添加聯(lián)系人按鈕 ,因?yàn)楂@取不到 cxt 而力不從心
// 細(xì)節(jié):這里由于 SlideActionType.primary 對(duì)應(yīng) actions 為空,所以雖然看似展開(kāi)空,目的就是關(guān)閉 上一個(gè)打開(kāi)的 secondary action
Slidable.of(cxt)?.open(actionType: SlideActionType.primary);
// 上面的雖然打開(kāi)了一個(gè)空的 但是系統(tǒng)還是會(huì)認(rèn)為是 打開(kāi)的 也就是 _slideIsOpen = true
// 手動(dòng)設(shè)置為false
_slideIsOpen = false;
// 下鉆 用戶(hù)信息
NavigatorUtils.push(cxt,'${ContactsRouter.contactInfoPage}?idstr=${user.idstr}');
}else{
Slidable.of(cxt)?.close();
}
},
);
方案二:每生成一個(gè)Cell,就將其Cell
對(duì)應(yīng)的context
記錄起來(lái)。Map[key] = cxt
;
該方案的核心點(diǎn)就是使用: Map
,而不是使用List
或Set
。一旦我們將每一個(gè)Cell
的context
記錄在案,那么我們就可以遍歷出每一個(gè)cxt
的狀態(tài),從而將某個(gè)context
關(guān)閉。
分析:首先,方案的實(shí)用性,遠(yuǎn)遠(yuǎn)高于方案一的且完美解決了方案一的存在局限性。其次,數(shù)據(jù)量一旦過(guò)大,每次遍歷可能存在一定的性能問(wèn)題,注意這里只是可能。
方案二的功能代碼實(shí)現(xiàn)如下:
// Cell
Widget listTile = MHListTile(
// 由于筆者是封裝組件,所以點(diǎn)擊事件中,將 context 回調(diào)出來(lái)
onTapValue: (cxt) {
// 沒(méi)有側(cè)滑展開(kāi)項(xiàng) 就直接下鉆
if (!_slideIsOpen) {
NavigatorUtils.push(cxt,
'${ContactsRouter.contactInfoPage}?idstr=${user.idstr}');
return;
}
// 該cell處于關(guān)閉狀態(tài), 直接下鉆到 用戶(hù)信息頁(yè)面
if (Slidable.of(cxt)?.renderingMode == SlidableRenderingMode.none) {
// 關(guān)閉上一個(gè)側(cè)滑
_closeSlidable();
// 下鉆 用戶(hù)信息
NavigatorUtils.push(cxt,'${ContactsRouter.contactInfoPage}?idstr=${user.idstr}');
}else{
Slidable.of(cxt)?.close();
}
},
// 回調(diào)context
callbackContext: (BuildContext cxt) {
_slidableCxtMap[title] = cxt;
},
);
/// 關(guān)閉slidable
void _closeSlidable() {
// 容錯(cuò)處理
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) {
// 關(guān)掉上一個(gè)
Slidable.of(value)?.close();
return;
}
}
}
方案三:使用 SlidableController.activeState
這個(gè)是筆者閱讀源碼,偶然發(fā)現(xiàn)的屬性。
方案三的功能代碼實(shí)現(xiàn)如下:
// Cell
Widget listTile = MHListTile(
// 由于筆者是封裝組件,所以點(diǎn)擊事件中,將 context 回調(diào)出來(lái)
onTapValue: (cxt) {
// 沒(méi)有側(cè)滑展開(kāi)項(xiàng) 就直接下鉆
if (!_slideIsOpen) {
NavigatorUtils.push(cxt,
'${ContactsRouter.contactInfoPage}?idstr=${user.idstr}');
return;
}
// 該cell處于關(guān)閉狀態(tài), 直接下鉆到 用戶(hù)信息頁(yè)面
if (Slidable.of(cxt)?.renderingMode == SlidableRenderingMode.none) {
// 關(guān)閉上一個(gè)側(cè)滑
// 方案三: 直接拿這個(gè)activaState
_slidableController.activeState?.close();
// 下鉆 用戶(hù)信息
NavigatorUtils.push(cxt,'${ContactsRouter.contactInfoPage}?idstr=${user.idstr}');
}else{
Slidable.of(cxt)?.close();
}
},
);
總結(jié)
首先,微信通訊錄
雖然看似只有搭建列表
、自定義IndexBar
、側(cè)滑備注
等三大功能模塊,但是內(nèi)部涵蓋的一些知識(shí)點(diǎn)和細(xì)節(jié)處理還需要各位親自體驗(yàn);而且也怪筆者才疏學(xué)淺,核心功能都是借助第三方插件來(lái)實(shí)現(xiàn)的,再此表示抱歉。
其次,本模塊的核心點(diǎn)主要落在: 自定義IndexBar
和 解決側(cè)滑關(guān)閉
上。 幸運(yùn)的是,筆者相信在這兩個(gè)核心點(diǎn)上解釋的已經(jīng)足夠詳細(xì),希望大家都過(guò)閱讀文章以及結(jié)合代碼,能夠領(lǐng)會(huì)筆者想表達(dá)的意圖和良苦用心。不求膜拜,只求點(diǎn)贊。
最后,希望大家通過(guò)閱讀本文,自己也能夠動(dòng)手寫(xiě)一個(gè)Flutter
版本的微信通訊錄,從而激發(fā)你的學(xué)習(xí)動(dòng)力,提升你的學(xué)習(xí)樂(lè)趣。
期待
- 文章若對(duì)您有些許幫助,請(qǐng)給個(gè)喜歡??,畢竟碼字不易;若對(duì)您沒(méi)啥幫助,請(qǐng)給點(diǎn)建議,切記學(xué)無(wú)止境。
- 針對(duì)文章所述內(nèi)容,閱讀期間任何疑問(wèn);請(qǐng)?jiān)谖恼碌撞吭u(píng)論指出,我會(huì)火速解決和修正問(wèn)題。
- GitHub地址:https://github.com/CoderMikeHe
- 源碼地址:flutter_wechat