Flutter 玩转微信——通讯录

概述

  • 鄙人于闲暇之日,自学Flutter已有两月之久,古人曰:百闻不如一见,百见不如一试,特此利用生平之所学,实战微信以项目。Flutter,学语法之轻易,用组件之简单,源码开源,插件丰富。然一份代码,却可完美运行于iOS和Android之上,其运行流畅,且效果杠杠,岂不拍案叫绝,牛B轰轰~。

  • 如有iOSAndroidWeb开发之经验,联想之前之所学,类比之前之所用,除写法不同,但语法通用,若多加练习,定能快速上手,耳熟蓝翔,不多逼逼,推荐以下之文档。

  • 此文作微信通讯录以文章,虽功能看似简单,但内含技术丰富,且功能十分有趣。作为初学Flutter,拿其小试牛刀,必将初有成效。于Flutter而言, 鄙人也算是初生牛犊不怕虎,并非是天神下凡一锤五。当然,笔者必将知无不言、言无不尽,梳理实战过程之问题,总结解决问题之方案,让大家知其然,知其所以然。望能抛玉引砖,摆渡众生,如有纰漏,还望斧正。

  • 源码地址:flutter_wechat

效果图

列表 索引 侧滑
contacts_page_0.png
contacts_page_1.png
contacts_page_2.png

列表

一、功能分析
搭建通讯录之列表,其知识点涵盖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组件提供的APIProperty可知,需要提供以下之部件(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。效果图如下所示:

contacts_page_3.png

  • 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),
        ),
      ),
    );
  }

四、特别提醒

  1. 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();
  1. AzListView:只是对SuspensionView & IndexBar的封装,方便使用罢了,尔等完全可以使用 SuspensionView & IndexBar 定制更加丰富的UI效果。

索引条

一、功能分析
由于,AzListView提供的IndexBar并不满足微信通讯录的要求,需求驱动生产,不可墨守成规,尔等可运行以下代码,查看默认和自定义的效果对比,尔等方能辨雌雄。

/// 构建联系人列表
  /// [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能相互联动
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查看源码。

  1. 列表滚动联动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);
  }
  1. 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,
            ),
      );
    }
  }

  1. 手指按住某tag,弹出气泡hint的功能实现。

相比AzListView默认提供的一个屏幕居中的indexBarHint,自定义的indexBarHint,则是在手指按下的某个tag的左侧弹出一个hint,且两者中心点水平平行,其效果更加灵性而不失端庄,俏皮且略显可爱
开局一张图,内容全靠编

contacts_page_5.png

由上图可知,考虑到hint(红色)和长按tag(蓝色)水平居中且跟随移动,这里采用Stack + Positioned来布局taghint,由于要保证长按or点击tag,才弹出hint,所以需要使用Offstage组件。注意:一定要设置Stackoverflow: 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,
      ),
    );
  }

  1. 自定义标签和自定义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)
contacts_page_4.png
contacts_page_1.png
contacts_page_6.png

侧滑(备注)

一、功能分析
联系人右边侧滑展开备注的功能。这里还是借助下面的插件来实现,站在巨人的肩膀上编程。关于具体使用,还请查看插件的提供的Example

** 二、代码实现 **
利用flutter_slidable插件,很快将之前的cell的具有侧滑功能,伪代码实现如下:

    // cell
    Widget listTile = MHListTile();
    // 头部是不需要侧滑的(新的朋友、群聊、标签、备注)
    if (!needSlidable) {
      return listTile; 
    }else{
      // 这样就具备了侧滑
      return Slidable(
         child: listTile; 
     )
  }

三、问题处理
flutter_slidable虽然引用和切入到已有代码,非常的细腻丝滑,让人嫉妒舒适。但是,为了完完全全实现微信通讯录的功能,其中还是遇到了少许问题,这里笔者一一记录以及处理心得。

  1. 每一个Slidable必须设置一个key且不能为null,否则报错。例如:Slidable(key: Key(title))

  2. 不需要组件默认提供的侧滑到最左侧,执行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; // 告诉系统,吾不死,尔等终究是臣
   },
 ),
  1. 侧滑时,禁止掉按下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,
  );
  1. 手动(程序)关闭上一个展开的侧滑部件(Cell)。
    程序关闭或展开某个Cell,这里用到组件提供的两个API : void close();void open({SlideActionType actionType});
    具体关闭和展开某个Cell的代码实现如下:
Slidable.of(context)?.open();
Slidable.of(context)?.close();

特别提醒的是: Slidable.of(context)中的context必须是 Slidable.childcontext。否则调用没效果。

// 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 CellB Cell为例,理想(现实)场景如下:

  • 同Cell点击场景

    • 当点击A Cell时,若A Cell是侧滑关闭状态时,则下钻A的用户信息页面; 若 A Cell是侧滑展开状态时,则关闭A Cell的侧滑;
    • 当点击B Cell时,逻辑同上。
  • 不同Cell点击场景

    • A CellB Cell都是侧滑关闭状态时,点击哪个Cell,则下钻哪个Cell对应的用户信息页面.
    • 不可能出现A CellB 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 Cellcontext
知道了事故原因了,那么解决问题就变得得心应手了,这里讲讲笔者的几种摆渡众生解决方案。(PS:小伙伴们有更好的解决方案,欢迎文末评论留言!!!)

方案一:打开一个空的左侧滑(黑魔法)

首先,Slidable是支持左侧滑和右侧滑,其对应的属性为: List<Widget> actionsList<Widget> secondaryActions,但是目前需求我们只需要右侧滑罢了,
其次,我们知道: 不可能出现A CellB Cell都是侧滑展开状态的场景。
所以,若A Cell是右侧滑展开状态时,当点击B Cell时,我们打开B Cell的一个空的左侧滑,即:Slidable.of(cxt)?.open(actionType: SlideActionType.primary);
因为B Cellactions是一个空数组,所以界面并没有发生变化,且能将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,而不是使用ListSet。一旦我们将每一个Cellcontext记录在案,那么我们就可以遍历出每一个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版本的微信通讯录,从而激发你的学习动力,提升你的学习乐趣。

期待

  1. 文章若对您有些许帮助,请给个喜欢❤️,毕竟码字不易;若对您没啥帮助,请给点建议,切记学无止境。
  2. 针对文章所述内容,阅读期间任何疑问;请在文章底部评论指出,我会火速解决和修正问题。
  3. GitHub地址:https://github.com/CoderMikeHe
  4. 源码地址:flutter_wechat

拓展

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,817评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,329评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,354评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,498评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,600评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,829评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,979评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,722评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,189评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,519评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,654评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,940评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,762评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,993评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,382评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,543评论 2 349