Interfaz de chat Flutter: lista de chat desplegable para cargar más mensajes históricos

Interfaz de chat Flutter: lista de chat desplegable para cargar más mensajes históricos

Previamente se dio cuenta del contenido de visualización de texto enriquecido de la interfaz de chat flutter, la realización del teclado de emoticonos personalizado, el signo más [➕] para expandir la cámara, el álbum de fotos y otros paneles de operación, y la visualización de burbujas de mensajes para realizar Flexible. Aquí, registre la lista deslizante de la interfaz de chat implementada y despliegue para cargar más mensajes históricos

La lista de la interfaz de chat utiliza ListView.

1. Representación

inserte la descripción de la imagen aquí

Dos, vista de lista

ListView es un componente de desplazamiento que puede organizar linealmente todos los subcomponentes en una dirección, y también admite la carga diferida de los elementos de la lista (que se crea solo cuando es necesario).

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

La interfaz de chat de seguimiento utilizará reversa, física, controlador, etc.

3. Lista de mensajes de la interfaz de chat

El desplazamiento de la lista de la interfaz de chat utiliza ListView.builder.
Necesidad de configurar ShrinkWrap

ShrinkWrap: este atributo indica si se debe establecer la longitud de ListView de acuerdo con la longitud total de los subcomponentes, y el valor predeterminado es falso. De forma predeterminada, ListView ocupa la mayor cantidad de espacio posible en la dirección de desplazamiento. ShrinkWrap debe ser verdadero cuando ListView está en un contenedor sin bordes (dirección de desplazamiento).

reverso: Establezca el reverso en verdadero, el contenido se mostrará al revés.

3.1 Lista de chat

// 聊天列表
  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 Cuando hay pocos elementos en la interfaz de chat, el rango de desplazamiento de iOS es pequeño y no puede recuperarse

Para este problema, CustomScrollView se usa aquí para ListView anidado. La función principal de CustomScrollView es proporcionar un Scrollable y Viewport común para combinar múltiples Slivers.

Código de implementación concreto

// 嵌套的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. Cuando ocurre lo contrario de ListView, cuando hay muy pocos elementos, se mostrará de abajo hacia arriba, lo que dará como resultado un gran espacio en blanco en la parte superior.

Cuando hay muy pocos elementos, se mostrará de abajo hacia arriba, y el área grande en blanco en la parte superior se debe a que la interfaz y el teclado de emoticonos y el cuadro de entrada a continuación usan el control Columna. Entonces Expandido se usa para llenar, el componente Expandido obliga a los subcomponentes a llenar el espacio disponible, y Expandido obligará a llenar el espacio en blanco restante.

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

// La interfaz y el siguiente teclado de emoticonos, cuadro de entrada, etc. usan el control Columna

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 Lista efecto elástico deslizante

Necesita personalizar ChatScrollPhysics, que hereda ScrollPhysics

Realice el efecto elástico de la carga deslizante y el efecto elástico de proteger el deslizamiento hacia arriba. (BouncingScrollPhysics tiene efectos elásticos arriba y abajo)

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 Eliminar la ondulación deslizante de ListView - definir ScrollBehavior

Implementar 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. Tire hacia abajo para cargar más mensajes y no más mensajes

Agregue ChatLoadingIndicator a la vista de lista al desplegar para cargar más mensajes

Haga un juicio sobre el último elemento de la lista. lista

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

Cargar más mensajes Códigos indicadores

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

Cuando no hay más datos, es necesario no mostrar más mensajes en este momento.

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

Escuche ScorllController para controlar la carga y otros mensajes

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

Hasta ahora, el menú desplegable de la lista de chat de la interfaz de chat flutter que carga más mensajes históricos está básicamente completado, y hay muchas clases de mensajes encapsulados aquí. Las operaciones posteriores de envío de mensajes se resolverán.

V. Resumen

Interfaz de chat de Flutter: la lista de chat se despliega para cargar más mensajes históricos, principalmente se da cuenta del uso de Expandir el diseño ListView anidado en Columna y establece el reverso, la física y ScrollBehavior. Puede resolver el problema de una gran área en blanco en la parte superior causada por el reverso, y eliminar la onda deslizante de ListView. Después de configurar el último mensaje para cargar el indicador de más mensajes y no solicitar más mensajes.

Registros de aprendizaje, sigue mejorando cada día.

Supongo que te gusta

Origin blog.csdn.net/gloryFlow/article/details/131609267
Recomendado
Clasificación