flutter 微信通讯录

Flutter  仿制微信通讯录效果,致效果如下:

有几个技术细节

  1. 总体可滑动,少于屏幕长度也可滑动
  2. 对于数据的处理。昵称 拼音首字母排序,
  3. 右侧字母导航,点击/滑动;移动到指定位置
  4. 当点击/滑动 右侧移动到最底部的时候; 防止 回弹现象(值滑动到最下面);

    

 ➥总体可滑动,少于屏幕长度也可滑动

主要还是配置  ListView 的 physics 字段

ListView(
  physics: const BouncingScrollPhysics(
    parent: AlwaysScrollableScrollPhysics(),
  ),
  controller: _scrollController,
  children: [],
)

➥对于数据的处理。昵称 拼音首字母排序

这个是我通讯录的一个表字段:

 主要一个排序处理实在 initState 中的:右侧字母导航栏也是根据 我通讯录数据生成的:

@override
void initState() {
  // TODO: implement initState
  super.initState();

  Map<String, List<Map>> cache = {};
  String firstLetter = '';

  for (int i = 0; i < widget.mapContact.length; i++) {
    firstLetter = widget.mapContact[i]['first_letter'];
    if (cache.containsKey(firstLetter)) {
      cache[firstLetter]?.add(widget.mapContact[i]);
    } else {
      cache[firstLetter] = [widget.mapContact[i]];
    }
  }
  List<String> keys = cache.keys.toList();
  keys.sort((a, b) => (a.compareTo(b)));

  for (var element in keys) {
    if (cache[element] != null) {
      _listContact[element] = cache[element]!;
    }
  }
}

➥右侧字母导航,点击/滑动;移动到指定位置

右侧的 字母导航栏是根据,通讯录信息生成的:布局采用 Stack -> Positioned 布局,

主要的包裹了一层 GestureDetector,用来监听手势:

Widget _contactIndex() {
    return Align(
      alignment: Alignment.centerRight,
      child: GestureDetector(
        onVerticalDragDown: _onDragDown,
        onVerticalDragUpdate: _onDragUpdate,
        onVerticalDragEnd: _onDragEnd,
        behavior: HitTestBehavior.opaque,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          mainAxisAlignment: MainAxisAlignment.center,
          children: _listContact.keys
              .toList()
              .asMap()
              .map(
                (key, value) => MapEntry(
                  key,
                  Container(
                    height: 45.cale,
                    width: 80.cale,
                    // color: Colors.red,
                    // padding: EdgeInsets.symmetric(
                    //   vertical: 5.cale,
                    //   horizontal: 20.cale,
                    // ),
                    child: Stack(
                      children: [
                        if (_currentIndex == key)
                          SizedBox(
                            width: 60.cale,
                            height: 60.cale,
                            child: CustomPaint(
                              painter: PainterLetter(
                                draggingLetter: _draggingLetter,
                              ),
                            ),
                          ),
                        Center(
                          child: Text(
                            value.toUpperCase(),
                            style: AppTextStyle.textStyle_22_000000,
                          ),
                        )
                      ],
                    ),
                  ),
                ),
              )
              .values
              .toList(),
        ),
      ),
    );
  }

三个 手势回调函数:

onVerticalDragDown,
onVerticalDragUpdate,
onVerticalDragEnd,
  _onDragDown(DragDownDetails details) {
    int i = details.localPosition.dy ~/ 45.cale;
    if (i >= 0 && i < _listContact.keys.length) {
      setState(() {
        _currentIndex = i;
        _draggingOffset = details.globalPosition;
        _draggingLetter = _listContact.keys.toList()[i];
        _scrollTo(_listContact.keys.toList()[i]);
      });
    }
  }

  _onDragUpdate(DragUpdateDetails details) {
    int i = details.localPosition.dy ~/ 45.cale;
    if (i >= 0 && i < _listContact.keys.length) {
      if (i != _currentIndex) {
        setState(() {
          _currentIndex = i;
          _draggingOffset = details.globalPosition;
          _draggingLetter = _listContact.keys.toList()[i];
          _scrollTo(_listContact.keys.toList()[i]);
        });
      }
    }
  }

  _onDragEnd(DragEndDetails details) {
    setState(() {
      _currentIndex = -1;
    });
  }

移动到指定位置:

这里有几个点需要注意下:( cale 是屏幕适配用的)

  • 480   是通讯录上面四个 常驻 按钮的高度 红色框
  • 60  是通讯录首字母高度   紫色框
  • 120 是 通讯录具体人的高度  橙色框

 

 滚动到具体的位置:

  为了防止 滚动到最底部出现 反弹的效果;这边加了判断:如果计算高度超过 listView 总高度(_scrollController.position.maxScrollExtent),则移动带 listview 高度度! 

  _scrollTo(String chat) {
    double offSet = 480.cale;
    int index = _listContact.keys.toList().indexOf(chat);
    if (index > -1) {
      for (int i = 0; i < index; i++) {
        offSet += 60.cale;
        List<Map>? list = _listContact[_listContact.keys.toList()[i]];
        if (list != null) {
          offSet += 120.cale * list.length;
        }
      }
      if (offSet > _scrollController.position.maxScrollExtent) {
        _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
      } else {
        _scrollController.jumpTo(offSet);
      }
    }
  }

➥ 完成代码

对于通讯录模块主要的就是三个文件:

  • contact_element.dart: 通讯录中 按字母分割现实的 具体好友
  • painter_letter.dart: 拖动时候现实的 椭圆,也可以用图片代替
  • tabbar1_contacts.dart 通讯录主要显示

 完整代码如下:

contact_element.dart

import 'package:flutter/material.dart';
import 'package:imflutter/wrap/extension/extension.dart';

import '../../const/app_colors.dart';
import '../../const/app_textStyle.dart';
import '../../wrap/widget/app_widget.dart';

class ContactElement extends StatefulWidget {
  final List<Map> datum;
  final String keyName;
  const ContactElement({Key? key, required this.keyName, required this.datum})
      : super(key: key);

  @override
  State<ContactElement> createState() => _ContactElementState();
}

class _ContactElementState extends State<ContactElement>
    with AutomaticKeepAliveClientMixin {
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 200.cale,
      // height: 200.cale,
      color: Colors.white,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Container(
            padding: EdgeInsets.only(left: 30.cale),
            height: 60.cale,
            width: double.infinity,
            color: AppColor.colorEDEDED,
            child: Align(
              alignment: Alignment.centerLeft,
              child: Text(
                widget.keyName,
                style: AppTextStyle.textStyle_24_505050,
              ),
            ),
          ),
          ...widget.datum
              .asMap()
              .map(
                (key, value) => MapEntry(
                  key,
                  Container(
                    padding: EdgeInsets.only(left: 28.cale),
                    height: 120.cale,
                    color: Colors.white,
                    child: Row(
                      children: [
                        ClipRRect(
                          borderRadius: BorderRadius.circular(7.cale),
                          child: AppWidget.cachedImage(
                            value['icon'],
                            width: 88.cale,
                            height: 88.cale,
                          ),
                        ),
                        Expanded(
                          child: Container(
                            height: 120.cale,
                            margin: EdgeInsets.only(
                              left: 30.cale,
                            ),
                            padding: EdgeInsets.only(
                              right: 80.cale,
                            ),
                            decoration: BoxDecoration(
                              border: Border(
                                bottom: key == widget.datum.length - 1
                                    ? BorderSide.none
                                    : BorderSide(
                                        width: 1, color: AppColor.colorEFEFEF),
                              ),
                            ),
                            child: Container(
                              alignment: Alignment.centerLeft,
                              child: Text(
                                value['nick_name'],
                                overflow: TextOverflow.ellipsis,
                                style: AppTextStyle.textStyle_30_000000,
                              ),
                            ),
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              )
              .values
              .toList()
        ],
      ),
    );
  }

  @override
  // TODO: implement wantKeepAlive
  bool get wantKeepAlive => true;
}

 painter_letter.dart

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:imflutter/wrap/extension/extension.dart';

class PainterLetter extends CustomPainter {
  final String draggingLetter;
  const PainterLetter({Key? key, required this.draggingLetter});

  @override
  void paint(Canvas canvas, Size size) {
    // TODO: implement paint
    // 创建画笔

    // final Paint paint = Paint(); // 创建画笔
    // paint
    //   ..color = Colors.blue //颜色
    //   ..strokeWidth = 4 //线宽
    //   ..style = PaintingStyle.stroke; //模式--线型
    // canvas.drawLine(Offset(0, 0), Offset(100, 100), paint); //绘制线
    // return;

    //原点移到左下角
    canvas.translate(0, 25.cale);
    Paint paint = Paint()
      ..color = Colors.grey
      ..strokeWidth = 2
      ..style = PaintingStyle.fill;

    Path path = Path();
    // 绘制文字
    path.lineTo(0, -size.width.cale);
    // path.conicTo(33, -28, 20, 0, 1);

    path.arcToPoint(Offset(size.width.cale * 1.2, 0),
        radius: Radius.circular(size.width.cale * 1.2),
        largeArc: true,
        clockwise: true);
    path.close();
    // var bounds = path.getBounds();
    canvas.save();
    // canvas.translate(-bounds.width / 2.cale, bounds.height / 2.cale);
    canvas.rotate(pi * 1.2);
    canvas.drawPath(path, paint);
    canvas.restore();
    // 绘制文字
    var textPainter = TextPainter(
        text: TextSpan(
            text: draggingLetter.toUpperCase(),
            style: TextStyle(
              fontSize: 38.cale,
              foreground: Paint()
                ..style = PaintingStyle.fill
                ..color = Colors.white,
            )),
        textAlign: TextAlign.center,
        textDirection: TextDirection.ltr);
    textPainter.layout();
    canvas.translate(-35.cale, -53.cale);
    // canvas.translate(
    //     -size.width.cale * 2 - 10.cale, (-size.height / 2).cale - 10.cale);
    textPainter.paint(canvas, Offset(-25.cale, 37.cale));
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    // TODO: implement shouldRepaint
    return true;
  }
}

tabbar1_contacts.dart

import 'package:flutter/material.dart';
import 'package:imflutter/pages/tabbar1/contact_element.dart';
import 'package:imflutter/pages/tabbar1/painter_letter.dart';
import 'package:imflutter/wrap/extension/extension.dart';
import '../../const/app_colors.dart';
import '../../const/app_textStyle.dart';

class TabBar1Contacts extends StatefulWidget {
  final List<Map> mapContact;
  const TabBar1Contacts({Key? key, required this.mapContact}) : super(key: key);

  @override
  State<TabBar1Contacts> createState() => _TabBar1ContactsState();
}

class _TabBar1ContactsState extends State<TabBar1Contacts> {
  final List<Map> _topOptions = [
    {'icon': 'assets/common/ic_new_friend.webp', 'title': '新朋友'},
    {'icon': 'assets/common/ic_group.webp', 'title': '群聊'},
    {'icon': 'assets/common/ic_tag.webp', 'title': '标签'},
    {'icon': 'assets/common/ic_offical.webp', 'title': '公众号'},
  ];
  final Map<String, List<Map>> _listContact = {};
  int _currentIndex = -1;
  Offset _draggingOffset = const Offset(0, 0);
  String _draggingLetter = 'd';
  final ScrollController _scrollController = ScrollController();

  @override
  void initState() {
    // TODO: implement initState
    super.initState();

    Map<String, List<Map>> cache = {};
    String firstLetter = '';

    for (int i = 0; i < widget.mapContact.length; i++) {
      firstLetter = widget.mapContact[i]['first_letter'];
      if (cache.containsKey(firstLetter)) {
        cache[firstLetter]?.add(widget.mapContact[i]);
      } else {
        cache[firstLetter] = [widget.mapContact[i]];
      }
    }
    List<String> keys = cache.keys.toList();
    keys.sort((a, b) => (a.compareTo(b)));

    for (var element in keys) {
      if (cache[element] != null) {
        _listContact[element] = cache[element]!;
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        ListView(
          physics: const BouncingScrollPhysics(
            parent: AlwaysScrollableScrollPhysics(),
          ),
          controller: _scrollController,
          children: [
            ..._topEle(),
            ..._listContact.keys
                .toList()
                .asMap()
                .map(
                  (key, value) => MapEntry(
                    key,
                    ContactElement(
                      keyName: _listContact.keys.toList()[key].toUpperCase(),
                      datum: _listContact.values.toList()[key],
                    ),
                  ),
                )
                .values
                .toList()
          ],
        ),
        Positioned(
          child: _contactIndex(),
        ),
      ],
    );
  }

  List<Widget> _topEle() {
    return _topOptions
        .asMap()
        .map(
          (key, value) => MapEntry(
            key,
            Container(
              padding: EdgeInsets.only(left: 28.cale),
              height: 120.cale,
              color: Colors.white,
              child: Row(
                children: [
                  ClipRRect(
                    borderRadius: BorderRadius.circular(7.cale),
                    child: Image.asset(
                      value['icon'],
                      width: 88.cale,
                      height: 88.cale,
                    ),
                  ),
                  Expanded(
                    child: Container(
                      height: 120.cale,
                      margin: EdgeInsets.only(left: 30.cale),
                      decoration: BoxDecoration(
                        border: Border(
                          bottom: key == _topOptions.length - 1
                              ? BorderSide.none
                              : BorderSide(
                                  width: 1, color: AppColor.colorEFEFEF),
                        ),
                      ),
                      child: Align(
                        alignment: Alignment.centerLeft,
                        child: Text(
                          value['title'],
                          style: AppTextStyle.textStyle_30_000000,
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        )
        .values
        .toList();
  }

  Widget _contactIndex() {
    return Align(
      alignment: Alignment.centerRight,
      child: GestureDetector(
        onVerticalDragDown: _onDragDown,
        onVerticalDragUpdate: _onDragUpdate,
        onVerticalDragEnd: _onDragEnd,
        behavior: HitTestBehavior.opaque,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          mainAxisAlignment: MainAxisAlignment.center,
          children: _listContact.keys
              .toList()
              .asMap()
              .map(
                (key, value) => MapEntry(
                  key,
                  Container(
                    height: 45.cale,
                    width: 80.cale,
                    // color: Colors.red,
                    // padding: EdgeInsets.symmetric(
                    //   vertical: 5.cale,
                    //   horizontal: 20.cale,
                    // ),
                    child: Stack(
                      children: [
                        if (_currentIndex == key)
                          SizedBox(
                            width: 60.cale,
                            height: 60.cale,
                            child: CustomPaint(
                              painter: PainterLetter(
                                draggingLetter: _draggingLetter,
                              ),
                            ),
                          ),
                        Center(
                          child: Text(
                            value.toUpperCase(),
                            style: AppTextStyle.textStyle_22_000000,
                          ),
                        )
                      ],
                    ),
                  ),
                ),
              )
              .values
              .toList(),
        ),
      ),
    );
  }

  _onDragDown(DragDownDetails details) {
    int i = details.localPosition.dy ~/ 45.cale;
    // print("-----------------details.localPosition:${details.localPosition}");
    // print("-----------------_onDragDown:$i");
    // print(
    //     "----------------- _listContact.keys.length:${_listContact.keys.length}");
    // print(
    //     "----------------- _listContact.keys.length:${_listContact.keys.toList()[i]}");
    // if(i<  _listContact.keys.length){
    //
    // }
    if (i >= 0 && i < _listContact.keys.length) {
      setState(() {
        _currentIndex = i;
        _draggingOffset = details.globalPosition;
        _draggingLetter = _listContact.keys.toList()[i];
        _scrollTo(_listContact.keys.toList()[i]);
      });
    }
  }

  _onDragUpdate(DragUpdateDetails details) {
    int i = details.localPosition.dy ~/ 45.cale;
    if (i >= 0 && i < _listContact.keys.length) {
      if (i != _currentIndex) {
        setState(() {
          _currentIndex = i;
          _draggingOffset = details.globalPosition;
          _draggingLetter = _listContact.keys.toList()[i];
          _scrollTo(_listContact.keys.toList()[i]);
        });
      }
    }
  }

  _onDragEnd(DragEndDetails details) {
    setState(() {
      _currentIndex = -1;
    });
  }

  _scrollTo(String chat) {
    double offSet = 480.cale;
    int index = _listContact.keys.toList().indexOf(chat);
    if (index > -1) {
      for (int i = 0; i < index; i++) {
        offSet += 60.cale;
        List<Map>? list = _listContact[_listContact.keys.toList()[i]];
        if (list != null) {
          offSet += 120.cale * list.length;
        }
      }
      if (offSet > _scrollController.position.maxScrollExtent) {
        _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
      } else {
        _scrollController.jumpTo(offSet);
      }
    }
  }
}

猜你喜欢

转载自blog.csdn.net/nicepainkiller/article/details/129158167