概述
鄙人于闲暇之日,自学
Flutter
已有两月之久,古人曰:百闻不如一见,百见不如一试
,特此利用生平之所学,实战微信以项目。Flutter
,学语法之轻易,用组件之简单,源码开源,插件丰富。然一份代码,却可完美运行于iOS和Android之上,其运行流畅,且效果杠杠,岂不拍案叫绝,牛B轰轰~。-
如有
iOS
、Android
、Web
开发之经验,联想之前之所学,类比之前之所用,除写法不同,但语法通用,若多加练习,定能快速上手,耳熟蓝翔,不多逼逼,推荐以下之文档。 此文作
微信通讯录
以文章,虽功能看似简单,但内含技术丰富,且功能十分有趣。作为初学Flutter
,拿其小试牛刀,必将初有成效。于Flutter
而言, 鄙人也算是初生牛犊不怕虎
,并非是天神下凡一锤五
。当然,笔者必将知无不言、言无不尽
,梳理实战过程之问题,总结解决问题之方案,让大家知其然,知其所以然
。望能抛玉引砖,摆渡众生,如有纰漏,还望斧正。源码地址:flutter_wechat
效果图
列表 |
索引 |
侧滑 |
---|---|---|
列表
一、功能分析
搭建通讯录之列表,其知识点涵盖A-Z 索引Bar
、悬停效果view
、自定义Header
、索引联动
、汉字转拼音
,若想实现前面之功能,这里推荐以下之插件,好风凭借力,送我上青云。
-
azlistview 实现
A-Z 索引Bar
、悬停效果view
、自定义Header
、索引联动
-
lpinyin 实现
汉字转拼音
关于具体其使用,还请下载其Demo,运行于电脑之上,查看其运行效果,在此就不多逼逼。
二、数据配置
// 获取联系人列表
Future fetchContacts() async {
// 先清除掉数据
_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 = "#";
}
}
// 根据A-Z排序
SuspensionUtil.sortListBySuspensionTag(_contactsList);
// 返回数据
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:悬浮View
B:组头View
代码实现:
/// 构建悬浮部件
/// [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
类型且不可动态修改。如涉及屏幕适配
,还请向上(下)取整
。
/// 悬浮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
并不满足微信通讯录的要求,需求驱动生产
,不可墨守成规,尔等可运行以下代码,查看默认和自定义的效果对比,尔等方能辨雌雄。
/// 构建联系人列表
/// [defaultMode] 是否使用默认的IndexBar
Widget _buildContactsList({bool defaultMode = false}) {
if (defaultMode) {
return _buildDefaultIndexBarList();
} else {
return _buildCustomIndexBarList();
}
}
功能对比
类型 | Custom | Default |
---|---|---|
效果 | ||
组件 | AzListView | AzListView |
条件 |
showIndexHint: false, indexBarBuilder: (_, _, _) => MHIndexBar()
|
showIndexHint: true, |
功能 |
1、列表和IndexBar能相互联动 2、IndexBar当前选中的Tag高亮 3、手指触碰IndexBar中Tag, 弹出指向该Tag的气泡 4、通过设置ignoreTags属性,控制其中某个Tag,不高亮,不弹气泡 4、通过设置mapTag和mapSelTag,可以将某个tag映射称自定义的默认或选中样式,eg: ♀ =>
|
1、只能通过IndexBar联动列表,反之不行 2、手指触碰IndexBar中Tag, 弹出屏幕居中的气泡 3、能控制某个Tag不弹气泡 |
二、魔改源码
考虑到只是在AzListView
系统提供的IndexBar
上新增一些功能,故笔者完全复制IndexBar
之源码,在其基础之上,新增功能罢了,可谓是借东风之力,成旷世之业。再此着重讲讲思路,若尔等想追根溯源,还以移驾/components/index_bar/mh_index_bar.dart
查看源码。
- 列表滚动联动
IndexBar
标签(tag
)滚动功能实现
该功能的实现,需要IndexBar
提供一个tag
属性即可。 具体代码实现如下
/// list.dart 索引标签改变
void _onSusTagChanged(String tag) {
setState(() {
_suspensionTag = tag;
});
}
/// 传递改变的tag 给 IndexBar
MHIndexBar(
tag: _suspensionTag,
)
/// mh_index_bar.dart 处理列表传经来的tag
// 配置 当前 _indexModel, tag可能是用户滚动列表的传进来数据,导致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映射其他部件,例如:♀ =>
功能实现
选中tag高亮
: 可以通过IndexBar
内部提供的私有对象_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
。
以上功能实现所需属性如下:
/// 当前高亮显示的标签
final String tag;
/// 忽略的Tags,这些忽略Tag, 不会高亮显示,点击或长按 不会弹出 tagHint
final List<String> ignoreTags;
/// 针对某个Tag显示其他部件的映射,一般都是映射 图片/svg
final Map<String, Widget> mapTag;
/// 针对某个Tag显示高亮其他部件的映射,一般都是映射 图片/svg
final Map<String, Widget> mapSelTag;
/// 长按弹出气泡显示的内容,一般都是映射 图片/svg
final Map<String, Widget> mapHintTag;
以上功能实现代码逻辑如下:<注意注释
>
/// 获取标签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
的功能实现。
相比AzListView
默认提供的一个屏幕居中的indexBarHint
,自定义的indexBarHint
,则是在手指按下的某个tag
的左侧弹出一个hint
,且两者中心点水平平行,其效果更加灵性而不失端庄,俏皮且略显可爱
。
开局一张图,内容全靠编
。
由上图可知,考虑到hint(红色)
和长按tag(蓝色)
水平居中且跟随移动,这里采用Stack + Positioned
来布局tag
和hint
,由于要保证长按or点击tag
,才弹出hint
,所以需要使用Offstage
组件。注意:一定要设置Stack
的overflow: Overflow.visible,
为可见。伪代码实现如下:
Stack(
// 设置超出部分可见 必须设置
overflow: Overflow.visible,
children: <Widget>[
// 标签组件
TagWidget,
// Hint组件
Positioned(
left: -80.0,
top: -17.0,
child: Offstage(
// 长按或点击: false(显示) ; 其他则为: true(隐藏)
offstage: true/false,
child: HintWidget,
)
)
],
),
水平靠左居中,伪代码实现.
// 靠左 hintW = 60, spaceX = 20
left: -(HintW + spaceX),
// 水平居中 HintH = 50, TagH = 16
top: -(HintH - TagH) * 0.5,
这里以布局Hint为例,代码实现如下。
/// 构建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
提供了许多可配置的属性,基本上能满足类似微信联系人
这样的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的实现,笔者已经在 /views/contacts/contacts_page.dart
里面实现了,且只要运行代码,默认就是通过这连个API
构建。
三种场景的效果图对比如下。<PS:图三、多个气泡只是用来证明自定义样式Hint罢了,然并卵~>
默认 | 自定义(属性) | 自定义(Builder) |
---|---|---|
侧滑(备注)
一、功能分析
联系人右边侧滑展开备注
的功能。这里还是借助下面的插件来实现,站在巨人的肩膀上编程。关于具体使用,还请查看插件的提供的Example
。
** 二、代码实现 **
利用flutter_slidable
插件,很快将之前的cell
的具有侧滑功能,伪代码实现如下:
// cell
Widget listTile = MHListTile();
// 头部是不需要侧滑的(新的朋友、群聊、标签、备注)
if (!needSlidable) {
return listTile;
}else{
// 这样就具备了侧滑
return Slidable(
child: listTile;
)
}
三、问题处理
flutter_slidable
虽然引用和切入到已有代码,非常的细腻丝滑,让人嫉妒舒适。但是,为了完完全全实现微信通讯录
的功能,其中还是遇到了少许问题,这里笔者一一记录以及处理心得。
每一个
Slidable
必须设置一个key
且不能为null
,否则报错。例如:Slidable(key: Key(title))
。不需要组件默认提供的侧滑到最左侧,执行
dismiss
事件。
默认该组件侧滑到最左侧,会执行onDismissed
回调,如果不写,程序会闪退。代码如下:
Slidable(
// 必须的有key
key: Key(title),
dismissal: SlidableDismissal(
child: SlidableDrawerDismissal(),
onDismissed: (actionType) {
/// 一般都是 删除这个cell, 如果啥都不干,则运行报错
},
),
由于这是系统默认的事件,且SlidableDismissal
提供了一个属性(dragDismissible
)来阻止这个默认事件。只需要设置为dragDismissible: false
即可。
这个方法虽然是解决了拖拽到最左侧,调用Dismiss
事件,但是,随即带来的是,侧滑失去了原有的弹性效果,变得非常的死板和呆滞,瞬间失去了灵魂一般,得不偿失。我们要的是:能侧滑到左侧回弹,且不执行dismiss事件。
翻阅SlidableDismissal
提供的属性,惊奇的发现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; // 告诉系统,吾不死,尔等终究是臣
},
),
- 侧滑时,禁止掉按下
cell
置灰(高亮)的效果。
默认情况下,按下或点击某个Cell
时,该Cell
会展示高亮(置灰)的效果,以此告知用户具体按下哪个Cell
。但是当我们侧滑或侧滑展开时,再去点击Cell
,不应该有这种高亮(置灰)的效果,否则,有点喧宾夺主的感觉。
解决方案:监听Slidable
是否展开,来判断Cell
是否需要点击高亮的效果。具体代码如下:
// 配置侧滑监听
ScrollController _slidableController = SlidableController(
onSlideIsOpenChanged: _handleSlideIsOpenChanged,
);
// 监听侧滑展开与否
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的代码实现如下:
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'),
);
}
}
上面的代码实现的效果是:点击 A Cell
,则A Cell
展开或关闭 侧滑。
但是,我们希望的效果是,如果A Cell
是关闭状态时,点击 A Cell
是下钻到用户信息页面。实现代码如下:
// Cell
Widget listTile = MHListTile(
// 由于笔者是封装组件,所以点击事件中,将 context 回调出来
onTapValue: (cxt) {
// 该cell处于关闭状态, 直接下钻到 用户信息页面
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)连用,就会出现问题。
以A Cell
和 B Cell
为例,理想(现实)场景如下:
-
同Cell点击场景
- 当点击
A Cell
时,若A Cell
是侧滑关闭状态时,则下钻A
的用户信息页面; 若A Cell
是侧滑展开状态时,则关闭A Cell
的侧滑; - 当点击
B Cell
时,逻辑同上。
- 当点击
-
不同Cell点击场景
- 若
A Cell
和B Cell
都是侧滑关闭状态时,点击哪个Cell
,则下钻哪个Cell
对应的用户信息页面. - 不可能出现
A Cell
和B Cell
都是侧滑展开状态的场景。 - 若
A Cell
是侧滑展开状态时,当点击B Cell
时,则关闭A Cell
的侧滑,下钻到B
的用户信息页面. - 若
B Cell
是侧滑展开状态时,当点击A Cell
时,则关闭B Cell
的侧滑,下钻到A
的用户信息页面.
- 若
俗话说:理想很丰满,现实很骨感
。现实场景是:若A Cell
是侧滑展开状态时,当点击B Cell
时,能下钻到B
的用户信息页面,但A Cell
是不会自动关闭侧滑,还是会保持侧滑展开状态.
事故产生的最主要原因是:当点击B Cell
时,我们无法拿到A Cell
的context
。
知道了事故原因了,那么解决问题就变得得心应手了,这里讲讲笔者的几种摆渡众生解决方案。(PS:小伙伴们有更好的解决方案,欢迎文末评论留言!!!)
方案一:打开一个空的左侧滑(黑魔法)
首先,Slidable
是支持左侧滑和右侧滑,其对应的属性为: List<Widget> actions
和List<Widget> secondaryActions
,但是目前需求我们只需要右侧滑
罢了,
其次,我们知道: 不可能出现A Cell
和 B Cell
都是侧滑展开状态的场景。
所以,若A Cell
是右侧滑展开状态时,当点击B Cell
时,我们打开B Cell
的一个空的左侧滑
,即:Slidable.of(cxt)?.open(actionType: SlideActionType.primary);
,
因为B Cell
的actions
是一个空数组,所以界面并没有发生变化,且能将A Cell
的右侧滑关闭。
局限性:首先,该方案适合没有左侧滑的场景;其次,我们手动打开一个空的左侧滑
,虽然界面没有变化,但是SlidableController.onSlideIsOpenChanged
回调的isOpen
一直为true
,如果有些场景需要使用这个isOpen
属性,那么势必会产生问题;
最后,若A Cell
是右侧滑展开状态时,我们不是点击B Cell
,而是点击导航栏上的按钮下钻的场景,该方案也不适合。
方案一的功能代码实现如下:
// Cell
Widget listTile = MHListTile(
// 由于笔者是封装组件,所以点击事件中,将 context 回调出来
onTapValue: (cxt) {
// 该cell处于关闭状态, 直接下钻到 用户信息页面
if (Slidable.of(cxt)?.renderingMode == SlidableRenderingMode.none) {
// 方案一: 针对cell点击 和下钻容易处理 但是一但 点击导航栏上的 添加联系人按钮 ,因为获取不到 cxt 而力不从心
// 细节:这里由于 SlideActionType.primary 对应 actions 为空,所以虽然看似展开空,目的就是关闭 上一个打开的 secondary action
Slidable.of(cxt)?.open(actionType: SlideActionType.primary);
// 上面的虽然打开了一个空的 但是系统还是会认为是 打开的 也就是 _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
的状态,从而将某个context
关闭。
分析:首先,方案的实用性,远远高于方案一的且完美解决了方案一的存在局限性。其次,数据量一旦过大,每次遍历可能存在一定的性能问题,注意这里只是可能。
方案二的功能代码实现如下:
// Cell
Widget listTile = MHListTile(
// 由于笔者是封装组件,所以点击事件中,将 context 回调出来
onTapValue: (cxt) {
// 没有侧滑展开项 就直接下钻
if (!_slideIsOpen) {
NavigatorUtils.push(cxt,
'${ContactsRouter.contactInfoPage}?idstr=${user.idstr}');
return;
}
// 该cell处于关闭状态, 直接下钻到 用户信息页面
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
这个是笔者阅读源码,偶然发现的属性。
方案三的功能代码实现如下:
// Cell
Widget listTile = MHListTile(
// 由于笔者是封装组件,所以点击事件中,将 context 回调出来
onTapValue: (cxt) {
// 没有侧滑展开项 就直接下钻
if (!_slideIsOpen) {
NavigatorUtils.push(cxt,
'${ContactsRouter.contactInfoPage}?idstr=${user.idstr}');
return;
}
// 该cell处于关闭状态, 直接下钻到 用户信息页面
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
、侧滑备注
等三大功能模块,但是内部涵盖的一些知识点和细节处理还需要各位亲自体验;而且也怪笔者才疏学浅,核心功能都是借助第三方插件来实现的,再此表示抱歉。
其次,本模块的核心点主要落在: 自定义IndexBar
和 解决侧滑关闭
上。 幸运的是,笔者相信在这两个核心点上解释的已经足够详细,希望大家都过阅读文章以及结合代码,能够领会笔者想表达的意图和良苦用心。不求膜拜,只求点赞。
最后,希望大家通过阅读本文,自己也能够动手写一个Flutter
版本的微信通讯录,从而激发你的学习动力,提升你的学习乐趣。
期待
- 文章若对您有些许帮助,请给个喜欢❤️,毕竟码字不易;若对您没啥帮助,请给点建议,切记学无止境。
- 针对文章所述内容,阅读期间任何疑问;请在文章底部评论指出,我会火速解决和修正问题。
- GitHub地址:https://github.com/CoderMikeHe
- 源码地址:flutter_wechat