Flutter 仿写微信通讯录页面

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

image.png

针对通讯录页面我们整体上可以分为列表部分跟右边索引条部分,因为索引条是在列表的上面,所以这个页面的 body 部分我们采用 Stack 部件。下面我们可以来按照每个部分来详细介绍下实现思路。

列表部分实现

列表部分整体我们可以分为顶部 4 个固定的 cell 跟底部的联系人 cell,所以这一块我们定义了两个数据源 _headerData_listDatas,但是 cell 我们用的是同一个 cellcell 对应的模型我们用的也是同一个,只是对参数进行的区分,cell 的参数也是一样。但是我们还可以看到,联系人部分相同首字母的头部会有一个索引头,在 iOS 中我们可以用组头视图来实现,但是 Flutter 中没有组的概念,所以我们就在 cell 中来添加这个索引头视图,只是通过逻辑跟参数来判断是否展示索引头视图部分。下面我们看一下关键代码部分的实现。

列表部分

Container(
              color: CahtThemColor,
              child: ListView.builder(itemBuilder: _itemForRow, itemCount: _headerData.length + _listDatas.length,)
          ),


Widget _itemForRow(BuildContext context, int index) {
    if (index < _headerData.length) {
      return _FriendCell(imageAssets: _headerData[index].imageAssets, name: _headerData[index].name);
    }
    index = index - _headerData.length;
    // 判断是否显示组名称
    return _FriendCell(
        imageUrl: _listDatas[index].imageUrl,
        name: _listDatas[index].name,
        groupTitle: (index > 0 && _listDatas[index].indexLetter == _listDatas[index -1].indexLetter) ? '' : _listDatas[index].indexLetter);
  }
复制代码

列表部分我们用 Container 包了一层,方便后期的改动。_itemForRow 方法部分我们通过判断 index 是否大于 _headerData.length 来传不同的参数对 cell 进行初始化,针对每组相同首字母的 cell 我们通过判断当前模型与下一个模型的索引字符 indexLetter 是否相等来判断是否展示索引头视图。

cell 部分的实现

class _FriendCell extends StatelessWidget {
  _FriendCell({this.imageUrl = '', this.name = '', this.groupTitle = '', this.imageAssets = ''});
  final String imageUrl;
  final String name;
  final String groupTitle;
  final String imageAssets;
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        groupTitle.length > 0 ? Container(
          height: 30,
          color: CahtThemColor,
          alignment: Alignment.centerLeft,
          padding: EdgeInsets.only(left: 15),
          child: groupTitle.length > 0 ? Text(groupTitle, style: TextStyle(color: Colors.grey),) : null,
        ) : Container(), //组头
        Container(
          color: Colors.white,
          child: Row(
            children: [
              Container(
                margin: EdgeInsets.all(10),
                width: 34,
                height: 34,
                decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(6.0),
                    image: imageUrl.length > 0
                        ? DecorationImage(
                        image: NetworkImage(imageUrl)
                    )
                        : DecorationImage(
                      image: AssetImage(imageAssets),
                    )
                ),
              ),//图片
              Container(
                // color: Colors.red,
                width: screenWidth(context) - 54,
                child: Column(
                  children: [
                    Container(
                      alignment: Alignment.centerLeft,
                      height: 54,
                      child: Text(
                        name,
                        style: TextStyle(fontSize: 18),
                      ),
                    ),
                    Container(
                      height: 0.5,
                      color: CahtThemColor,
                    ), //下划线
                  ],
                ),
              ),//昵称
            ],
          ),
        ), //cell的内容
      ],
    );
  }
}
复制代码

如果一个类我们只是想在当前文件中使用可以通过下划线的方式来定义。cell 的整体我们分为上下两部分,组头跟内容部分。所以整体我们使用 Column 部件来实现。

模型部分的实现

class Friends {
  final String imageUrl;
  final String name;
  final String indexLetter;
  final String imageAssets;
  Friends({this.imageUrl = '', this.name = '', this.indexLetter = '', this.imageAssets = ''});
}
复制代码

索引视图的实现

索引视图部分当我们点击的时候颜色会变成黑色透明的,所以整体是有状态的,继承于 StatefulWidget。因为索引条是悬浮在视图的右边居中,所以整体采用 Positioned 部件,整体高度我们这里设置为整个屏幕的二分之一,宽度为 30,距离右边为 0,距离顶部为屏幕的八分之一。

索引视图内容部分

for (int i = 0; i < INDEX_WORDS.length; i++) {
      _words.add(
          Expanded(child: Text(INDEX_WORDS[i],
            style: TextStyle(fontSize: 10, color: _textColor),))
      );
    }
复制代码
child: Container(
          color: _bkColor,
          child: Column(
            children: _words,
          ),
        ),
复制代码

INDEX_WORDS 数组存放的是索引字符,我们在 build 中方法中遍历数组,把每个字符对应的部件加入到 _words 中。索引条中的部件是上下排列并且等分的,索引每个索引字符对应的部件采用 Expanded,整体采用 Column 部件。

索引条点击改变状态颜色

image.png

child: GestureDetector(
        // 索引条点击
        onVerticalDragDown: (DragDownDetails details){
          setState(() {
            _bkColor = Color.fromRGBO(1, 1, 1, 0.4);
            _textColor = Colors.white;
          });
        },
        // 索引条点击取消
        onVerticalDragEnd: (DragEndDetails details){
          setState(() {
            _bkColor = Color.fromRGBO(1, 1, 1, 0.0);
            _textColor = Colors.black;
          });
        },
复制代码

当点击索引条的时候需要改变背景颜色跟文字颜色,索引我们定义了两个变量 _bkColor_textColor,当索引条被点击跟取消点击的时候调用 setState 方法,并且修改这两个变量的值。

索引条拖拽

//获取选中的 item 文字
String _getIndex(BuildContext context, Offset globalPosition) {
  //拿到点击小部件的盒子,也就是索引条
  RenderBox box = context.findRenderObject() as RenderBox;
  // 拿到 y 值, globalToLocal 当前位置距离索引条左上角 (0, 0) 位置的距离 (x, y)
  double y = box.globalToLocal(globalPosition).dy;
  //算出字符的高度
  var itemHeight = screenHight(context) / 2 / INDEX_WORDS.length;
  //算出第几个 item
  int index = (y ~/ itemHeight).clamp(0, INDEX_WORDS.length - 1);
  return INDEX_WORDS[index];
}
复制代码
onVerticalDragUpdate: (DragUpdateDetails details){
          print(_getIndex(context, details.globalPosition));
        },
复制代码

当我们上下拖拽索引条的时候,需要获取到点击位置对应的索引字符,我们通过 context.findRenderObject() as RenderBox 能拿到当前的部件,也就是索引条,然后通过 box.globalToLocal 可以得到当前触摸位置距离索引条左上角 (0, 0) 位置的距离 (x, y),这样就可以得到了 y 值,因为知道了索引条的高度,索引就可以计算出每个索引字符对应部件的高度,就能得到点击了第几个索引,也就能通过 INDEX_WORDS[index] 得到点击的字符。最后需要通过 clampindex 做个取值范围的判断。

全局变量的抽取

image.png

这里我们定义了一个 const 文件,对全局的变量进程了抽取。

おすすめ

転載: juejin.im/post/7032177283859218446