我的编程空间,编程开发者的网络收藏夹
学习永远不晚

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

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

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

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滑动波纹。之后在消息的最后一条设置为加载更多消息指示器与没有更多消息提示。

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

来源地址:https://blog.csdn.net/gloryFlow/article/details/131609267

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

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

下载Word文档到电脑,方便收藏和打印~

下载Word文档

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录