Flutter 玩轉微信——通訊錄

概述

  • 鄙人于閑暇之日,自學Flutter已有兩月之久,古人曰:百聞不如一見,百見不如一試,特此利用生平之所學,實戰(zhàn)微信以項目。Flutter,學語法之輕易,用組件之簡單,源碼開源,插件豐富。然一份代碼,卻可完美運行于iOS和Android之上,其運行流暢,且效果杠杠,豈不拍案叫絕,牛B轟轟~。

  • 如有iOSAndroid、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組件提供的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

代碼實現(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),
        ),
      ),
    );
  }

四、特別提醒

  1. 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();
  1. 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查看源碼。

  1. 列表滾動聯(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);
  }
  1. 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,
            ),
      );
    }
  }

  1. 手指按住某tag,彈出氣泡hint的功能實現(xiàn)。

相比AzListView默認提供的一個屏幕居中的indexBarHint,自定義的indexBarHint,則是在手指按下的某個tag的左側彈出一個hint,且兩者中心點水平平行,其效果更加靈性而不失端莊,俏皮且略顯可愛。
開局一張圖,內(nèi)容全靠編。

contacts_page_5.png

由上圖可知,考慮到hint(紅色)和長按tag(藍色)水平居中且跟隨移動,這里采用Stack + Positioned來布局taghint,由于要保證長按or點擊tag,才彈出hint,所以需要使用Offstage組件。注意:一定要設置Stackoverflow: 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,
      ),
    );
  }

  1. 自定義標簽和自定義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)微信通訊錄的功能,其中還是遇到了少許問題,這里筆者一一記錄以及處理心得。

  1. 每一個Slidable必須設置一個key且不能為null,否則報錯。例如:Slidable(key: Key(title))。

  2. 不需要組件默認提供的側滑到最左側,執(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),吾不死,爾等終究是臣
   },
 ),
  1. 側滑時,禁止掉按下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,
  );
  1. 手動(程序)關閉上一個展開的側滑部件(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.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'),
    );
  }
}

上面的代碼實現(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 CellB Cell為例,理想(現(xiàn)實)場景如下:

  • 同Cell點擊場景

    • 當點擊A Cell時,若A Cell是側滑關閉狀態(tài)時,則下鉆A的用戶信息頁面; 若 A Cell是側滑展開狀態(tài)時,則關閉A Cell的側滑;
    • 當點擊B Cell時,邏輯同上。
  • 不同Cell點擊場景

    • A CellB Cell都是側滑關閉狀態(tài)時,點擊哪個Cell,則下鉆哪個Cell對應的用戶信息頁面.
    • 不可能出現(xiàn)A CellB 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 Cellcontext。
知道了事故原因了,那么解決問題就變得得心應手了,這里講講筆者的幾種擺渡眾生解決方案。(PS:小伙伴們有更好的解決方案,歡迎文末評論留言!?。。?/p>

方案一:打開一個空的左側滑(黑魔法)

首先,Slidable是支持左側滑和右側滑,其對應的屬性為: List<Widget> actionsList<Widget> secondaryActions,但是目前需求我們只需要右側滑罷了,
其次,我們知道: 不可能出現(xiàn)A CellB Cell都是側滑展開狀態(tài)的場景。
所以,若A Cell是右側滑展開狀態(tài)時,當點擊B Cell時,我們打開B Cell的一個空的左側滑,即:Slidable.of(cxt)?.open(actionType: SlideActionType.primary);,
因為B Cellactions是一個空數(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,而不是使用ListSet。一旦我們將每一個Cellcontext記錄在案,那么我們就可以遍歷出每一個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ā)你的學習動力,提升你的學習樂趣。

期待

  1. 文章若對您有些許幫助,請給個喜歡??,畢竟碼字不易;若對您沒啥幫助,請給點建議,切記學無止境。
  2. 針對文章所述內(nèi)容,閱讀期間任何疑問;請在文章底部評論指出,我會火速解決和修正問題。
  3. GitHub地址:https://github.com/CoderMikeHe
  4. 源碼地址:flutter_wechat

拓展

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容