Flutter chat interface - chat list pull down to load more historical messages

Flutter chat interface - chat list pull down to load more historical messages

Previously realized the rich text display content of the flutter chat interface, the realization of the custom emoticon keyboard, the plus sign [➕] to expand the camera, photo album and other operation panels, and the message bubble display to realize Flexible. Here, record the sliding list of the implemented chat interface and pull down to load more historical messages

The list of the chat interface uses ListView.

1. Rendering

insert image description here

Two, ListView

ListView is a scrolling component that can linearly arrange all subcomponents in one direction, and it also supports lazy loading of list items (created only when needed).

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>[],
})

The follow-up chat interface will use reverse, physics, controller, etc.

3. Chat interface message list

The chat interface list scrolling uses ListView.builder.
Need to set shrinkWrap

shrinkWrap: This attribute indicates whether to set the length of the ListView according to the total length of the subcomponents, and the default value is false. By default, ListView takes up as much space as possible in the scrolling direction. shrinkWrap must be true when the ListView is in a borderless (scroll direction) container.

reverse: Set reverse to true, the content will be displayed upside down.

3.1. Chat list

// 聊天列表
  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. When there are few items in the chat interface, the iOS scrolling range is small and cannot bounce back

For this problem, CustomScrollView is used here for nested ListView. The main function of CustomScrollView is to provide a common Scrollable and Viewport to combine multiple Slivers.

Concrete implementation code

// 嵌套的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. When the reverse of ListView is true, when there are too few items, it will be displayed from bottom to top, resulting in a large blank at the top

When there are too few items, it will be displayed from bottom to top, and the large blank area at the top is because the interface and the emoticon keyboard and input box below use the Column control. So Expanded is used to fill, the Expanded component forces the subcomponents to fill the available space, and the Expanded will force the remaining blank space to be filled.

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),
      ),
    );
  }

// The interface and the following emoticon keyboard, input box, etc. use the Column control

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. List sliding elastic effect

Need to customize ChatScrollPhysics, which inherits ScrollPhysics

Realize the elastic effect of sliding loading, and the elastic effect of shielding upward sliding. (BouncingScrollPhysics has elastic effects up and down)

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. Remove ListView sliding ripple - define ScrollBehavior

Implement 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;
  }
}

4. Pull down to load more messages and no more messages

Add ChatLoadingIndicator to listview when pulling down to load more messages

Make a judgment on the last item in the list. list

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

Load more message Indicator codes

// 刷新的动画
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,
      ),
    );
  }
}

When there is no more data, it is necessary to display no more messages at this time.

// 没有更多消息时候
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,
      ),
    );
  }
}

Listen to ScorllController to control loading and other messages

判断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();
        }
      }
    });
  }

So far, the flutter chat interface-chat list drop-down loading more historical messages is basically completed, and there are many encapsulated message classes here. Subsequent operations of sending messages will be sorted out.

V. Summary

Flutter chat interface - chat list pulls down to load more historical messages, mainly realizes the use of Expand nested ListView layout in Column, and sets reverse, physics, and ScrollBehavior. It can solve the problem of a large blank area at the top caused by reverse being true, and remove the sliding ripple of ListView. After the last message is set to load more messages indicator and no more messages prompt.

Learning records, keep improving every day.

Guess you like

Origin blog.csdn.net/gloryFlow/article/details/131609267
Recommended