flutter聊天界面-聊天列表 下拉加载更多历史消息

news2024/9/28 23:27:18

flutter聊天界面-聊天列表 下拉加载更多历史消息

在之前实现了flutter聊天界面的富文本展示内容、自定义表情键盘实现、加号【➕】更多展开相机、相册等操作Panel、消息气泡展示实现Flexible。这里把实现的聊天界面的滑动列表及下拉加载更多历史消息记录一下

聊天界面的列表使用ListView。

一、效果图

在这里插入图片描述

二、ListView

ListView是滚动组件,它可以沿一个方向线性排布所有子组件,并且它也支持列表项懒加载(在需要时才会创建)。

ListView({
  ...  
  //可滚动widget公共参数
  Axis scrollDirection = Axis.vertical,
  bool reverse = false,
  ScrollController? controller,
  bool? primary,
  ScrollPhysics? physics,
  EdgeInsetsGeometry? padding,
  
  //ListView各个构造函数的共同参数  
  double? itemExtent,
  Widget? prototypeItem, //列表项原型,后面解释
  bool shrinkWrap = false,
  bool addAutomaticKeepAlives = true,
  bool addRepaintBoundaries = true,
  double? cacheExtent, // 预渲染区域长度
    
  //子widget列表
  List<Widget> children = const <Widget>[],
})

后续聊天界面会用到reverse、physics、controller等

三、聊天界面消息列表

聊天界面列表滚动使用的是ListView.builder。
需要设置shrinkWrap

shrinkWrap:该属性表示是否根据子组件的总长度来设置ListView的长度,默认值为false 。默认情况下,ListView会在滚动方向尽可能多的占用空间。当ListView在一个无边界(滚动方向上)的容器中时,shrinkWrap必须为true。

reverse:设置reverse为ture,内容会倒过来显示。

3.1、聊天列表

// 聊天列表
  Widget buildScrollConfiguration(
      ChatContainerModel model, BuildContext context) {
    return ListView.builder(
      physics: AlwaysScrollableScrollPhysics(),
      key: chatListViewKey,
      shrinkWrap: true,
      addRepaintBoundaries: false,
      controller: scrollController,
      padding:
          const EdgeInsets.only(left: 0.0, right: 0.0, bottom: 0.0, top: 0.0),
      itemCount: messageList.length + 1,
      reverse: true,
      clipBehavior: Clip.none,
      itemBuilder: (BuildContext context, int index) {
        if (index == messageList.length) {
          if (historyMessageList != null && historyMessageList!.isEmpty) {
            return const ChatNoMoreIndicator();
          }
          return const ChatLoadingIndicator();
        } else {
          CommonChatMessage chatMessage = messageList[index];
          return ChatCellElem(
            childElem: MessageElemHelper.layoutCellElem(chatMessage),
            chatMessage: chatMessage,
            onSendFailedIndicatorPressed: (CommonChatMessage chatMessage) {
              onSendFailedIndicatorPressed(context, chatMessage);
            },
            onBubbleTapPressed: (CommonChatMessage chatMessage) {
              onBubbleTapPressed(context, chatMessage);
            },
            onBubbleDoubleTapPressed: (CommonChatMessage chatMessage) {
              onBubbleDoubleTapPressed(context, chatMessage);
            },
            onBubbleLongPressed: (CommonChatMessage chatMessage,
                LongPressStartDetails details,
                ChatBubbleFrame? chatBubbleFrame) {
              onBubbleLongPressed(
                  context, chatMessage, details, chatBubbleFrame);
            },
          );
        }
      },
    );
  }

3.2、聊天界面条目较少时候,iOS滚动范围较小、无法回弹问题

这个问题,这里使用的是CustomScrollView来进行嵌套ListView。CustomScrollView 的主要功能是提供一个公共的 Scrollable 和 Viewport,来组合多个 Sliver。

具体实现代码

// 嵌套的customScrollView
  Widget buildCustomScrollView(ChatContainerModel model, BuildContext context) {
    return LayoutBuilder(
        builder: (BuildContext lbContext, BoxConstraints constraints) {
      double layoutHeight = constraints.biggest.height;
      return CustomScrollView(
        slivers: <Widget>[
          SliverPadding(
            padding: EdgeInsets.all(0.0),
            sliver: SliverToBoxAdapter(
              child: Container(
                alignment: Alignment.topCenter,
                height: layoutHeight,
                child: buildScrollConfiguration(model, context),
              ),
            ),
          ),
        ],
      );
    });
  }

3.3、使用ListView的reverse为true时候,导致条目太少的时候会从下往上显示,导致顶部大片空白

导致条目太少的时候会从下往上显示,导致顶部大片空白的情况是由于界面及下面的表情键盘、输入框等使用的是Column控件。所以要用到Expanded来填充,Expanded组件强制子组件填充可用空间,Expanded会强制填充剩余留白空间。

Widget buildListContainer(ChatContainerModel model, BuildContext context) {
    return Expanded(
      child: Container(
        decoration: BoxDecoration(
          color: ColorUtil.hexColor(0xf7f7f7),
        ),
        clipBehavior: Clip.hardEdge,
        alignment: Alignment.topCenter,
        child: isNeedDismissPanelGesture
            ? GestureDetector(
                onPanDown: handlerGestureTapDown,
                child: buildCustomScrollView(model, context),
              )
            : buildCustomScrollView(model, context),
      ),
    );
  }

// 界面及下面的表情键盘、输入框等使用的是Column控件

return Container(
          key: chatContainerKey,
          width: double.infinity,
          height: double.infinity,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            children: [
              buildChatStatisticsBar(model),
              ChatAnnouncementBar(
                announcementNotice: model.announcementNotice,
                onAnnouncementPressed: () {
                  onAnnouncementPressed(model.announcementNotice);
                },
              ),
              buildListContainer(model, context),
              ChatNavigatorBar(
                onNavigatorItemPressed: (CommNavigatorEntry navigatorEntry) {
                  onNavigatorItemPressed(navigatorEntry, model);
                },
                navigatorEntries: model.navigatorEntries,
              ),
              ChatInputBar(
                chatInputBarController: chatInputBarController,
                moreOptionEntries: model.moreOptionEntries,
                showPostEnterButton: checkShowPostAndStatistics(model),
              ),
            ],
          ),
        );

3.4、列表滑动弹性效果

需要自定义ChatScrollPhysics,该类继承ScrollPhysics

实现下滑加载带弹性效果,上滑屏蔽弹性效果。(BouncingScrollPhysics是上下都有弹性效果)

class ChatScrollPhysics extends ScrollPhysics {
  /// Creates scroll physics that bounce back from the edge.
  const ChatScrollPhysics({ScrollPhysics? parent}) : super(parent: parent);

  
  ChatScrollPhysics applyTo(ScrollPhysics? ancestor) {
    return ChatScrollPhysics(parent: buildParent(ancestor));
  }

  /// The multiple applied to overscroll to make it appear that scrolling past
  /// the edge of the scrollable contents is harder than scrolling the list.
  /// This is done by reducing the ratio of the scroll effect output vs the
  /// scroll gesture input.
  ///
  /// This factor starts at 0.52 and progressively becomes harder to overscroll
  /// as more of the area past the edge is dragged in (represented by an increasing
  /// `overscrollFraction` which starts at 0 when there is no overscroll).
  double frictionFactor(double overscrollFraction) =>
      0.52 * math.pow(1 - overscrollFraction, 2);

  
  double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
    print("applyPhysicsToUserOffset position:${position}, offset:${offset}");
    assert(offset != 0.0);
    assert(position.minScrollExtent <= position.maxScrollExtent);

    if (!position.outOfRange) return offset;

    final double overscrollPastStart =
        math.max(position.minScrollExtent - position.pixels, 0.0);
    final double overscrollPastEnd =
        math.max(position.pixels - position.maxScrollExtent, 0.0);
    final double overscrollPast =
        math.max(overscrollPastStart, overscrollPastEnd);
    final bool easing = (overscrollPastStart > 0.0 && offset < 0.0) ||
        (overscrollPastEnd > 0.0 && offset > 0.0);

    final double friction = easing
        // Apply less resistance when easing the overscroll vs tensioning.
        ? frictionFactor(
            (overscrollPast - offset.abs()) / position.viewportDimension)
        : frictionFactor(overscrollPast / position.viewportDimension);
    final double direction = offset.sign;

    double applyPhysicsToUserOffset =
        direction * _applyFriction(overscrollPast, offset.abs(), friction);
    print("applyPhysicsToUserOffset:${applyPhysicsToUserOffset}");
    return applyPhysicsToUserOffset;
  }

  static double _applyFriction(
      double extentOutside, double absDelta, double gamma) {
    assert(absDelta > 0);
    double total = 0.0;
    if (extentOutside > 0) {
      final double deltaToLimit = extentOutside / gamma;
      if (absDelta < deltaToLimit) return absDelta * gamma;
      total += extentOutside;
      absDelta -= deltaToLimit;
    }
    return total + absDelta;
  }

  
  double applyBoundaryConditions(ScrollMetrics position, double value) {
    print("applyBoundaryConditions:${position},value:${value}");
    return 0.0;
  }

  
  Simulation? createBallisticSimulation(
      ScrollMetrics position, double velocity) {
    final Tolerance tolerance = this.tolerance;
    print(
        "createBallisticSimulation:${position},velocity:${velocity},tolerance.velocity:${tolerance.velocity}");
    if (velocity.abs() >= tolerance.velocity || position.outOfRange) {
      return BouncingScrollSimulation(
        spring: spring,
        position: position.pixels,
        velocity: velocity,
        leadingExtent: position.minScrollExtent,
        trailingExtent: position.maxScrollExtent,
        tolerance: tolerance,
      );
    }
    return null;
  }

  // The ballistic simulation here decelerates more slowly than the one for
  // ClampingScrollPhysics so we require a more deliberate input gesture
  // to trigger a fling.
  
  double get minFlingVelocity {
    double aMinFlingVelocity = kMinFlingVelocity * 2.0;
    print("minFlingVelocity:${aMinFlingVelocity}");
    return aMinFlingVelocity;
  }

  // Methodology:
  // 1- Use https://github.com/flutter/platform_tests/tree/master/scroll_overlay to test with
  //    Flutter and platform scroll views superimposed.
  // 3- If the scrollables stopped overlapping at any moment, adjust the desired
  //    output value of this function at that input speed.
  // 4- Feed new input/output set into a power curve fitter. Change function
  //    and repeat from 2.
  // 5- Repeat from 2 with medium and slow flings.
  /// Momentum build-up function that mimics iOS's scroll speed increase with repeated flings.
  ///
  /// The velocity of the last fling is not an important factor. Existing speed
  /// and (related) time since last fling are factors for the velocity transfer
  /// calculations.
  
  double carriedMomentum(double existingVelocity) {
    double aCarriedMomentum = existingVelocity.sign *
        math.min(0.000816 * math.pow(existingVelocity.abs(), 1.967).toDouble(),
            40000.0);
    print(
        "carriedMomentum:${aCarriedMomentum},existingVelocity:${existingVelocity}");
    return aCarriedMomentum;
  }

  // Eyeballed from observation to counter the effect of an unintended scroll
  // from the natural motion of lifting the finger after a scroll.
  
  double get dragStartDistanceMotionThreshold {
    print("dragStartDistanceMotionThreshold");
    return 3.5;
  }
}

3.5、去除ListView滑动波纹 - 定义ScrollBehavior

实现ScrollBehavior

class ChatScrollBehavior extends ScrollBehavior {
  final bool showLeading;
  final bool showTrailing;

  ChatScrollBehavior({
    this.showLeading: false,	//不显示头部水波纹
    this.showTrailing: false,	//不显示尾部水波纹
  });

  
  Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {
    switch (getPlatform(context)) {
      case TargetPlatform.iOS:
        return child;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        return GlowingOverscrollIndicator(
          child: child,
          showLeading: showLeading,
          showTrailing: showTrailing,
          axisDirection: axisDirection,
          color: Theme.of(context).accentColor,
        );
    }
    return null;
  }
}

四、下拉加载更多消息与没有更多消息

在下拉加载更多消息时,在listview上加ChatLoadingIndicator

在列表的最后一条进行判断。列表的

itemCount: messageList.length + 1,
if (index == messageList.length) {
          if (historyMessageList != null && historyMessageList!.isEmpty) {
            return const ChatNoMoreIndicator();
          }
          return const ChatLoadingIndicator();
        }

加载更多消息Indicator代码

// 刷新的动画
class ChatLoadingIndicator extends StatelessWidget {
  const ChatLoadingIndicator({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Container(
      height: 60.0,
      width: double.infinity,
      alignment: Alignment.center,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          CupertinoActivityIndicator(
            color: ColorUtil.hexColor(0x333333),
          ),
          const SizedBox(
            width: 10,
          ),
          buildIndicatorTitle(context),
        ],
      ),
    );
  }

  Widget buildIndicatorTitle(BuildContext context) {
    return Text(
      "加载中",
      textAlign: TextAlign.left,
      maxLines: 1000,
      overflow: TextOverflow.ellipsis,
      softWrap: true,
      style: TextStyle(
        fontSize: 14,
        fontWeight: FontWeight.w500,
        fontStyle: FontStyle.normal,
        color: ColorUtil.hexColor(0x555555),
        decoration: TextDecoration.none,
      ),
    );
  }
}

当没有更多数据的时候,这时候需要显示没有更多消息了。

// 没有更多消息时候
class ChatNoMoreIndicator extends StatelessWidget {
  const ChatNoMoreIndicator({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Container(
      height: 40.0,
      width: double.infinity,
      alignment: Alignment.center,
      // 不显示提示文本
      child: buildIndicatorTitle(context),
    );
  }

  Widget buildIndicatorTitle(BuildContext context) {
    return Text(
      "没有更多消息",
      textAlign: TextAlign.left,
      maxLines: 1,
      overflow: TextOverflow.ellipsis,
      softWrap: true,
      style: TextStyle(
        fontSize: 14,
        fontWeight: FontWeight.w500,
        fontStyle: FontStyle.normal,
        color: ColorUtil.hexColor(0x555555),
        decoration: TextDecoration.none,
      ),
    );
  }
}

监听ScorllController来控制加载等多消息

判断scrollController.position.pixels与scrollController.position.maxScrollExtent

// 滚动控制器Controller
  void addScrollListener() {
    scrollController.addListener(() {
      LoggerManager()
          .debug("addScrollListener pixels:${scrollController.position.pixels},"
              "maxScrollExtent:${scrollController.position.maxScrollExtent}"
              "isLoading:${isLoading}");
      if (scrollController.position.pixels >=
          scrollController.position.maxScrollExtent) {
        if (isLoading == false) {
          loadHistoryMore();
        }
      }
    });
  }

至此flutter聊天界面-聊天列表 下拉加载更多历史消息基本完成,这里有很多封装的消息类。后续的发送消息的操作等再整理。

五、小结

flutter聊天界面-聊天列表 下拉加载更多历史消息,主要实现Column中使用Expand嵌套ListView布局,设置reverse、physics、ScrollBehavior。可以解决reverse为true首导致顶部大片空白问题,去除ListView滑动波纹。之后在消息的最后一条设置为加载更多消息指示器与没有更多消息提示。

学习记录,每天不停进步。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/730604.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL索引优化原则和失效情况

目录 1. 全值匹配2. 最佳左前缀法则3. 不要在索引列上做任何计算4. 范围之后全失效5. 尽量使用覆盖索引6. 使用不等于&#xff08;!或<>&#xff09;会使索引失效7. is null 或 is not null也无法使用索引8. like通配符以%开头会使索引失效9. 字符串不加单引号导致索引失…

程序员的悲哀是什么?

点击下方“JavaEdge”&#xff0c;选择“设为星标” 第一时间关注技术干货&#xff01; 免责声明~ 切记&#xff0c;任何文章不要过度深思&#xff08;任何东西都无法经得起审视&#xff0c;因为这世上没有同样的成长环境&#xff0c;也没有同样的认知水平同时也「没有适用于所…

大模型高效训练基础知识:梯度累积(Gradient Accumulationn)

梯度累积 梯度累积&#xff08;Gradient Accumulation&#xff09;的基本思想是将一次性的整批参数更新的梯度计算变为以一小步一小步的方式进行&#xff08;如下图&#xff09;&#xff0c;具体而言该方法以小批次的方式进行模型前向传播和反向传播&#xff0c;过程中迭代计算…

变革管理中的几个不错的模型小结

其他的变革模型&#xff1a; 变革管理流程&#xff1a;

二、Java的变量

二、变量 2.1、关键字与保留字 关键字 定义&#xff1a;赋予特殊含义&#xff0c;用做专门用途的字符串 特点&#xff1a;关键字的所有字母都为小写 保留字&#xff08;reserved word&#xff09; 现有Java版本尚未使用&#xff0c;但以后版本可能会作为关键字使用。自己命名…

使用PYQT制作人才交流管理系统

利用使用PYQT制作一个人才交流管理系统&#xff0c;先使用QT designer.exe设计好人才交流管理系统的各个UI界面&#xff0c;然后利用pyuic把生成的ui界面编译为py文件&#xff0c;在主函数文件内创建一个类继承ui.py界面的类&#xff0c;即可完成数据库逻辑与 GUI 代码分离&…

小米不再忍耐,裁员三分之一强势反击印度,印度制造或因此梦破

就在小米公司表态继续投资印度之后不久&#xff0c;印度媒体报道指小米印度公司的员工数量从1500人减少到1000人左右&#xff0c;这显示出小米开始强势反击印度&#xff0c;业界人士指出此举的影响远不止于此&#xff0c;结果很可能是导致印度制造的梦想破灭。 1.小米为印度手机…

软件工程——第9章面向对象方法学引论知识点整理

本专栏是博主个人笔记&#xff0c;主要目的是利用碎片化的时间来记忆软工知识点&#xff0c;特此声明&#xff01; 文章目录 1.当前最好的软件开发技术是&#xff1f; 2.面向对象的原则是什么&#xff1f; 3.人们把客观世界中的实体抽象为什么&#xff1f; 4.软件系统本质上…

主诉病程时长提取

编写Python函数 import re def structured_pro(original_text,keyword,out_unitNone):pattern_split_str[。&#xff0c;]pattern_splitre.compile(pattern_split_str,re.I|re.M)original_listpattern_split.split(original_text)pattern_keywordre.compile(keyword,re.I|re.M…

目前最好的MicrosoftProject替代方案

Microsoft Project是一个功能强大的项目管理工具。然而它可能是昂贵的&#xff0c;并且可能不适合所有的项目管理需求。幸运的是&#xff0c;有几个Microsoft Project的替代方案提供了类似的特性和功能。在本文中我们将探索目前可用的一些最好的Microsoft Project替代方案。 1、…

12.4 ARM异常处理

目录 ARM异常处理&#xff08;一&#xff09; 异常 概念 异常处理机制 ARM异常源 概念 ARM异常源 ARM异常模式 ARM异常处理&#xff08;二&#xff09; ARM异常响应 异常向量表 异常返回 IRQ异常举例​编辑 ARM异常处理&#xff08;三 &#xff09; 异常优先级 …

自我介绍这样写?

已经帮小伙伴改了 500 多份简历了&#xff0c;也发现了一些大家写简历时的共性问题。其中让我印象比较深刻的一个点就是 自我介绍 &#xff0c;基本上所有同学的自我介绍都是这么写的&#xff1a; 读这篇文章的朋友们&#xff0c;你是不是也是这么写自我介绍的呢&#xff1f; 这…

MySQL索引优化整合案例实现

目录 1 JOIN优化1.1 JOIN算法原理1.2 in和exists函数 2 order by优化2.1 索引排序2.2 额外排序2.3 排序优化 3 索引单表优化案例3.1. 建表3.2. 单表索引分析3.1.1 需求3.1.2 优化 4 索引多表优化案例 1 JOIN优化 1.1 JOIN算法原理 1) JOIN回顾 JOIN 是 MySQL 用来进行联表操作…

在 Jetpack Compose 中创建 BottomAppBar

Jetpack Compose 是 Android 的现代 UI 工具包&#xff0c;它使得构建美观且功能强大的应用变得更加简单。在本文中&#xff0c;我们将探讨如何使用 Jetpack Compose 创建一个 BottomAppBar。 开始之前 确保你的开发环境已经设置好了 Jetpack Compose。你需要使用 Android Stu…

矩阵Matrices

目录 矩阵的变换 向量和矩阵 矩阵的变换 房子图形的矩阵变换&#xff08;wiggle动态变换&#xff09; uicontrol的‘style’类型&#xff1a; X house dot2dot(X) theta1 wiggle(X) function dot2dot(X) % DOT2DOT Connect the points from a 2-by-n matrix. X(:,end1) X…

5 类数据,洞察游戏的秘密

数据犹如一扇扇窗户&#xff0c;透过它&#xff0c;我们可以洞察到游戏世界内部的诸多秘密。这些秘密&#xff0c;就像是隐藏在房间深处的宝藏&#xff0c;只有真正理解并善用它们&#xff0c;我们才能创造出更吸引人的游戏&#xff0c;形成更成功的商业模式。 一个关键的问题是…

【Linux】—— 浅谈进程优先级

本期&#xff0c;我们将来聊聊的是关于进程优先级的相关知识&#xff01;&#xff01;&#xff01; 目录 序言 &#xff08;一&#xff09;基本概念 &#xff08;二&#xff09;查看系统进程 1、PRI and NI 2、PRI vs NI &#xff08;三&#xff09;设置优先级 序言 首先…

关于antdesign-vue的layout组件样式失效问题(#components-layout-demo-custom-trigger)

1.错误描述 使用antdesign vue 中的layout作为主要布局&#xff0c;https://www.antdv.com/components/layout-cn 正常引入后&#xff0c;跟期望的样子不一样 期望中的样子 实际的样子 logo没有了&#xff0c;而且此时也无法更改样式。 此时的样式是这样的 无论怎么修改都…

Grafana 使用Rest API 作为数据源的实践

本文使用最新版本的Grafana 10 进行操作。 如果要使用Rest API 作为grafana 的数据源&#xff0c;可以选择安装一个Infinity的数据源插件。 如果创建数据源时&#xff0c;搜不到infinity&#xff0c;点击find more 查找安装该数据源插件 1. 安装 Infinity 数据源插件&#xf…

IP 协议的相关特性

目录 IP协议有三大特点&#xff1a;无连接、无状态、不可靠。 四位版本号 四位头部长度 八位服务类型: 十六位总长度 16 位标识, 3 位标志, 13 位片偏移 八位生存时间 八位协议 十六位首部校验和 关于IP v4地址不够的问题 ip地址动态分配: ip地址转换(NAT) 数据传输…