Flutter チャット インターフェイス - チャット リストをプルダウンして、より多くの履歴メッセージをロードします

Flutter チャット インターフェイス - チャット リストをプルダウンして、より多くの履歴メッセージをロードします

これまでに、フラッターチャットインターフェースのリッチテキスト表示内容、カスタム絵文字キーボードの実現、カメラやフォトアルバムなどの操作パネルを展開するプラス記号[➕]、メッセージバブル表示などを実現し、フレキシブルを実現しました。ここで、実装されたチャット インターフェイスのスライド リストを記録し、プルダウンしてさらに履歴メッセージをロードします。

チャットインターフェースのリストはListViewを使用します。

1. レンダリング

ここに画像の説明を挿入

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

フォローアップ チャット インターフェイスでは、リバース、物理、コントローラーなどが使用されます。

3. チャットインターフェースメッセージリスト

チャット インターフェイスのリストのスクロールには ListView.builder を使用します。
シュリンクラップを設定する必要があります

ShrinWrap: この属性は、サブコンポーネントの合計の長さに応じて ListView の長さを設定するかどうかを示します。デフォルト値は false です。デフォルトでは、ListView はスクロール方向に可能な限り多くのスペースを占有します。ListView がボーダーレス (スクロール方向) コンテナー内にある場合は、shrinkWrap が true である必要があります。

reverse: reverse を true に設定すると、コンテンツが上下逆に表示されます。

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 のスクロール範囲が小さく、元に戻すことができません

この問題に対して、ここではネストされた ListView に CustomScrollView が使用されています。CustomScrollView の主な機能は、複数の Sliver を組み合わせるために共通の Scrollable と Viewport を提供することです。

具体的な実装コード

// 嵌套的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 の逆が true の場合、項目数が少なすぎると下から上に表示され、上部に大きな空白が表示されます。

項目が少なすぎる場合は、下から上に表示されます。上部に大きな空白領域があるのは、その下のインターフェイスと絵文字キーボードと入力ボックスが列コントロールを使用しているためです。そのため、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. リストスライド弾性効果

ScrollPhysicsを継承するChatScrollPhysicsをカスタマイズする必要がある

滑り荷重の弾性効果と上向き滑りをシールドする弾性効果を実現。(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;
  }
}

4. プルダウンしてさらにメッセージをロードし、メッセージをロードしないようにします

さらにメッセージを読み込むためにプルダウンするときにリストビューに ChatLoadingIndicator を追加します

リストの最後の項目で判断します。リスト

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

さらにメッセージ インジケーター コードをロードする

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

これまでのところ、フラッター チャット インターフェイス - チャット リスト ドロップダウンでより多くの履歴メッセージを読み込むことは基本的に完了しており、ここには多くのカプセル化されたメッセージ クラスがあります。その後のメッセージ送信操作が整理されます。

V. まとめ

Flutter チャット インターフェイス - チャット リストは、より多くの履歴メッセージを読み込むためにプルダウンされ、主に Column での Expand ネストされた ListView レイアウトの使用を実現し、リバース、物理、および ScrollBehavior を設定します。reverse が true の場合に上部に大きな空白領域が発生する問題を解決し、ListView のスライドの波紋を除去します。最後のメッセージが追加メッセージ インジケーターをロードするように設定されると、メッセージ プロンプトは表示されなくなります。

学習記録、日々改善を続けてください。

おすすめ

転載: blog.csdn.net/gloryFlow/article/details/131609267