滚动ListView与聊天页面

DartSDK向下兼容问题

dart_sdk2.12.0版本以上会存在空安全的问题,例如声明属性final String? imageUrl;需要添加?

// 查看dart_sdk版本
$ dart --version
Dart SDK version: 2.13.4 (stable) (Wed Jun 23 13:08:41 2021 +0200) on "macos_x64"
配置dart_sdk兼容版本

如果声明属性final String imageUrl;报错,说明pubspec.yaml文件配置的dart_sdk版本是>=2.12.0,不兼容2.12.0以下的版本。可以更改dart_sdk兼容版本为>=2.7.0 <3.0.0,同时点击Pub get进行配置就能解决报错。

配置dart_sdk的两种方式

  • 点击Pub get进行配置
  • Terminal终端执行$ flutter pub get命令进行配置

推荐dart_sdk兼容版本是>=2.12.0,该版本以上是flutter添加空安全的一次变革。

之前我们在开发friends_page.dart页面时,自定义了_FriendCell类,通讯录页面顶部四个栏目用的是本地图片资源,底部列表排序用的是网络图片资源,代码如下

// dart_sdk 2.7.0写法
class _FriendCell extends StatelessWidget {
......
Container(
  margin: EdgeInsets.all(10),
  width: 34,
  height: 34,
  decoration: BoxDecoration(
    borderRadius: BorderRadius.circular(6.0),
    image: DecorationImage(
      image: imageUrl != null
        ? NetworkImage(imageUrl)
        : AssetImage(imageAssets),
    )
  ),
), //图片

// dart_sdk 2.12.0以上写法
Container(
  margin: EdgeInsets.all(10),
  width: 34,
  height: 34,
  decoration: BoxDecoration(
    borderRadius: BorderRadius.circular(6.0),
    // 装饰器image
    image: DecorationImage(
      image: imageUrl != null
        ? NetworkImage(imageUrl!) as ImageProvider
        : AssetImage(imageAssets!),
      ),
    ),
), //图片

滚动ListView

上篇文章通讯录与索引条中,我们实现了拖动索引条可以定位到索引条的具体标识,下面我们需要把索引条标识回调出去给到ListView,让ListView能够准确滚动到具体分组。

  • index_bar.dart文件添加回调
class IndexBar extends StatefulWidget {
  // 对外提供回调,告诉friends_page当前选中的是标识
  final void Function(String str) indexBarCallBack;
  IndexBar({this.indexBarCallBack});

  @override
  State<StatefulWidget> createState() => _IndexBarState();
}
  • 拖动IndexBar把具体标识回调出去
// 添加手势
child: GestureDetector(
  // 拖拽手势
  onVerticalDragUpdate: (DragUpdateDetails details) {
    // String str = getIndex(context, details.globalPosition);
    // print('选中的是$str');
    // 把具体索引标识回调出去
    widget.indexBarCallBack(getIndex(context, details.globalPosition));
  },
  // 点击索引
  onVerticalDragDown:(DragDownDetails details){
    // 把具体索引标识回调出去
    widget.indexBarCallBack(getIndex(context, details.globalPosition));
    setState(() {
      _bkColor = Color.fromRGBO(1, 1, 1, 0.5);
      _textColor = Colors.white;
    });
   },
......
  • 悬浮索引条获取回调标识
// 悬浮索引条
IndexBar(
  indexBarCallBack: (String str){
    // 获取索引标识
  },
)
  • ListView在构建的时候可以指定ScrollController,这里实例化_scrollController用于ListView的滚动偏移
class _FriendsPageState extends State<FriendsPage> {

  ScrollController _scrollController;

  @override
  // _FriendsPageState页面载入的时候会执行,热重载是不会执行的
  void initState() {
    // TODO: implement initState
    super.initState();
    _scrollController = ScrollController();
  ......

body: Stack(
  children: [
    Container(
      color: WeChatThemeColor,
      child: ListView.builder(
        // ListView指定_scrollController
        controller: _scrollController,
        itemBuilder: _itemForRow,
        itemCount: _listDatas.length + _headerData.length,
      ),
    ),
......
  • 构造数据源计算出索引条标识具体偏移量
    _groupOffsetMap字典用于存储索引条标识偏移量
class _FriendsPageState extends State<FriendsPage> {
  double _cellHeight = 54.5;
  double _groupHeight = 30.0;
  // 字典,里面放item和高度对应的数据
  final Map _groupOffsetMap = {
    INDEX_WORDS[0]: 0.0,
    INDEX_WORDS[1]: 0.0,
  };

  final List<Friends> _headerData = [
    Friends(imageAssets: 'images/新的朋友.png', name: '新的朋友'),
    Friends(imageAssets: 'images/群聊.png', name: '群聊'),
    Friends(imageAssets: 'images/标签.png', name: '标签'),
    Friends(imageAssets: 'images/公众号.png', name: '公众号'),
  ];

  final List<Friends> _listDatas = [];
  ScrollController _scrollController;

  @override
  // _FriendsPageState页面载入的时候会执行,热重载是不会执行的
  void initState() {
    // TODO: implement initState
    super.initState();
    _scrollController = ScrollController();
    // 创建数据, 链式表达
    _listDatas..addAll(datas)..addAll(datas); // 等价于 _listDatas.addAll(datas);_listDatas.addAll(datas);
    // 数据排序
    _listDatas.sort((Friends a, Friends b) {
      return a.indexLetter.compareTo(b.indexLetter);
    });

    // 注意⚠️⚠️⚠️ 这里是计算具体标识偏移量
    var _groupOffset = _cellHeight * _headerData.length;
    //进过循环计算,将每一个头的位置算出来。放入字典
    for (int i = 0; i < _listDatas.length; i++) {
      if (i < 1) {
        //第一个cell一定有头!
        _groupOffsetMap.addAll({_listDatas[i].indexLetter: _groupOffset});
        //保存完了再加_groupOffset
        _groupOffset += _cellHeight + _groupHeight;
      } else if (_listDatas[i].indexLetter == _listDatas[i - 1].indexLetter) {
        //相同分组,只需要加Cell的高度
        _groupOffset += _cellHeight;
      } else {
        _groupOffsetMap.addAll({_listDatas[i].indexLetter: _groupOffset});
        //保存完了再加_groupOffset
        _groupOffset += _cellHeight + _groupHeight;
      }
    }
  }
......
  • 获取索引条回调标识ListView滚动到指定偏移位置
// 悬浮索引条
IndexBar(
  indexBarCallBack: (String str){
    // 选中右侧索引,listView滚动到固定位置
    if (_groupOffsetMap[str] != null) {
      _scrollController.animateTo(_groupOffsetMap[str],
      duration: Duration(microseconds: 100),
      curve: Curves.easeIn);
    }
  },
)
运行效果

显示指示器

问题:上面指示器在滚动的时候,底部会有空白内容滑上去

空白内容滑动到屏幕

解决思路:判断当前索引字符是否在屏幕内,如果在屏幕内就不用添加,下面滚动的时候也就不会偏移

var _groupOffset = _cellHeight * _headerData.length;
    //进过循环计算,将每一个头的位置算出来。放入字典
    for (int i = 0; i < _listDatas.length; i++) {
      if (i < 1) {
        ......
      } else if (_listDatas[i].indexLetter == _listDatas[i - 1].indexLetter) {   
        ......
      } else {
        // 判断当前索引字符有没有超出屏幕,如果没有超出屏幕就不用添加,下面滚动的时候也就不会偏移
        _groupOffsetMap.addAll({_listDatas[i].indexLetter: _groupOffset});
        //保存完了再加_groupOffset
        _groupOffset += _cellHeight + _groupHeight;
      }
    }
......

// 上面判断索引字符在屏幕内,就不会添加到_groupOffsetMap,这里就不会滚动偏移
// 选中右侧索引,listView滚动到固定位置
if (_groupOffsetMap[str] != null) {
   _scrollController.animateTo(_groupOffsetMap[str],
     duration: Duration(microseconds: 100),
     curve: Curves.easeIn);
}

下面给IndexBar添加选中索引的图片指示器

  • IndexBar添加指示器Y值显示索引标识是否显示等属性
class _IndexBarState extends State<IndexBar> {

  // 索引条选中 背景色与文字颜色
  Color _bkColor = Color.fromRGBO(1, 1, 1, 0.0);
  Color _textColor = Colors.black;

  // 添加图片指示器Y值,显示索引标识,是否显示等属性
  double _indicatorY = 0.0;
  String _indicatorText = 'A';
  bool _indicatorHidden = true;
......
  • 修改getIndex方法返回类型为索引index
// 获取选中的item的字符!!
// 修改为返回索引下标
int getIndex(BuildContext context, Offset globalPosition) {
  // 获取当前小部件的盒子
  RenderBox? box = context.findRenderObject() as RenderBox?;
  // 获取y值 globalToLocald当前位置距离部件原点(左上角)的位置
  double y = box!.globalToLocal(globalPosition).dy;
  // 算出字符高度
  var itemHeight = screenHeigth(context) / 2 / INDEX_WORDS.length;
  // 算出第几个item, clamp约束index范围值
  int index = (y ~/ itemHeight).clamp(0, INDEX_WORDS.length - 1);
  return index;
}
  • IndexBar添加图片指示器,右侧索引条拖动时更新属性值
// TODO: implement build
    return Positioned(
      right: 0.0,
      top: screenHeigth(context) / 8,
      height: screenHeigth(context) / 2,
      width: 120,
      // 添加手势
      child: Row(
        children: [
          Container(
            alignment: Alignment(0, _indicatorY),
            width: 100,
            // color: Colors.red,
            child: _indicatorHidden
               ? null
               : Stack(
              // 让气泡居中
              alignment: Alignment(-0.2, 0),
              children: [
                Image(image: AssetImage('images/气泡.png'), width: 60,),
                Text(
                  _indicatorText,
                  style: TextStyle(fontSize: 35, color: Colors.white),
                )
              ],
            ),
          ), //指示器
          GestureDetector(
            // 拖拽手势
            onVerticalDragUpdate: (DragUpdateDetails details) {
              // String str = getIndex(context, details.globalPosition);
              // print('选中的是$str');
              int index = getIndex(context, details.globalPosition);
              widget.indexBarCallBack(INDEX_WORDS[index]);
              setState(() {
                _indicatorY = 2.2 / INDEX_WORDS.length * index - 1.1;
                _indicatorText = INDEX_WORDS[index];
                _indicatorHidden = false;
              });
            },
            // 点击索引
            onVerticalDragDown:(DragDownDetails details){
              int index = getIndex(context, details.globalPosition);
              widget.indexBarCallBack(INDEX_WORDS[index]);
              setState(() {
                _indicatorY = 2.2 / INDEX_WORDS.length * index - 1.1;
                _indicatorText = INDEX_WORDS[index];
                _indicatorHidden = false;
                _bkColor = Color.fromRGBO(1, 1, 1, 0.5);
                _textColor = Colors.white;
              });
            },
            // 点击结束,颜色值还原
            onVerticalDragEnd:(DragEndDetails details){
              setState(() {
                _indicatorHidden = true;
                _bkColor = Color.fromRGBO(1, 1, 1, 0.0);
                _textColor = Colors.black;
              });
            },
            child: Container(
              // 设置宽度,让右侧索引居中
              width: 20,
              color: _bkColor,
              child: Column(
                children: words,
              ),
            ),
          ), //索引条
        ],
      ),
    );
拖动索引条显示指示器

聊天页面导航条

AppBar里面有个属性actions,可以设置导航栏上面的一些按钮。我们现在要实现的是聊天界面,导航栏右边的按钮被点击,会弹出菜单列表。系统就提供了一个PopupMenuButton,我们可以拿来直接使用。

PopupMenuButton的一些属性

  • color: 下拉框的背景颜色
  • offset:下拉框距离导航栏的偏移量
  • child:是指导航栏上按钮,就是你要点击的那个按钮
  • temBuilder:这个是下拉框里面的内容了
  • PopupMenuItem:这个是下拉框里面的每个item,是一个Widget
class _ChatPageState extends State<ChatPage> {
  // PopupMenuItem自定义item
  Widget _buildPopupMenuItem(String imgAss, String title) {
    return Row(
      children: [
        Image(
          image: AssetImage(imgAss),
          width: 20,
        ),
        SizedBox(
          width: 20,
        ),
        Text(
          title,
          style: TextStyle(color: Colors.white),
        ),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: WeChatThemeColor,
        title: const Text('微信页面'),
        // 让AppBar的title居中显示
        centerTitle: true,
        actions: [
          Container(
            margin: EdgeInsets.only(right: 10),

            // PopupMenuButton自定义菜单栏
            child: PopupMenuButton(
              color: Color.fromRGBO(1, 1, 1, 0.65),
              // 让PopupMenuButton菜单栏向下偏移60
              offset: Offset(0, 60.0),
              child: Image(
                image: AssetImage('images/圆加.png'),
                width: 25,
              ),
              itemBuilder: (BuildContext context) {
                return <PopupMenuItem<String>>[
                  PopupMenuItem(
                      child: _buildPopupMenuItem('images/发起群聊.png', '发起群聊')),
                  PopupMenuItem(
                      child: _buildPopupMenuItem('images/添加朋友.png', '添加朋友')),
                  PopupMenuItem(
                      child: _buildPopupMenuItem('images/扫一扫1.png', '扫一扫')),
                  PopupMenuItem(
                      child: _buildPopupMenuItem('images/收付款.png', '收付款')),
                ];
              },
            ),
          )
        ],
      ),
      body: const Center(
        child: Text('微信页面'),
      ),
    );
  }
}
点击弹出菜单栏

准备网络数据

RAP网站搭建服务器,准备网络数据

  • 注册好账号,仓库 -> 新建仓库 -> 填写仓库信息
新建仓库
建好的仓库
  • 可以使用示例接口
示例接口
  • 点击删除模版删除示例接口,新建接口
新建接口
接口地址
  • 编辑接口 -> 保存
编辑接口

这时候浏览器查看接口地址,就能看到接口数据

编辑接口数据
编辑接口

Mock自定义接口数据规则 -> 示例
生成假用户信息 -> 我们这里只需要生成用户头像即可

编辑最终数据
头像地址

头像地址只需要更改数字即可改变不同头像,example:1.jpg 2.jpg

浏览器查看接口数据

发送网络请求

Dart三方库,网络请求库diohttp

网络请求库dio
官方网络请求库http
引入三方库
  • pubspec.yaml文件配置网络请求库 -> 点击Pub get载入三方库
配置http网络请求库
  • 导入网络请求头文件
import 'package:http/http.dart' as http;
  • 网络请求获取数据
class _ChatPageState extends State<ChatPage> {

  @override
  void initState() {
    super.initState();
    //获取网络数据
    getDatas();
  }

  // 异步方法,但是不一定切换线程
  getDatas() async {
    final url =
    Uri.parse('http://rap2api.taobao.org/app/mock/256798/api/chat/list');
    // 等待网络请求返回结果
    final response = await http.get(url);
    print(response.statusCode);
    print(response.body);
  }
......

多线程可以是异步的,但异步不代表多线程

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

推荐阅读更多精彩内容