滚动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);
  }
......

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

成功获取接口数据
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容