美文网首页Flutter圈子FlutterFlutter
Flutter 玩转微信——通讯录

Flutter 玩转微信——通讯录

作者: CoderMikeHe | 来源:发表于2019-11-28 17:23 被阅读0次

    概述

    • 鄙人于闲暇之日,自学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

    拓展

    相关文章

      网友评论

        本文标题:Flutter 玩转微信——通讯录

        本文链接:https://www.haomeiwen.com/subject/jnerwctx.html