【问题标题】:Flutter chat text align like Whatsapp or TelegramFlutter 聊天文本对齐,如 Whatsapp 或 Telegram
【发布时间】:2022-01-23 16:55:01
【问题描述】:

我无法确定这种与 Flutter 的一致性。

Whatsapp 或 Telegram 上的右侧转换为左对齐,但日期在右侧。如果日期有空位,则在同一行的末尾。

第一条和第三条聊天线可以使用Wrap() 小部件完成。但是Wrap() 无法使用第二行,因为聊天文本是一个单独的小部件并填充了整个宽度并且不允许日期小部件适合。你会如何使用 Flutter 来做到这一点?

【问题讨论】:

  • 您熟悉RenderBoxes 吗?这是您应该遵循的正确方法(当然您可以尝试一些变通方法,例如使用TextPainter 测量文本但是...)
  • 我不是。我去查一下,谢谢。
  • 一个警告:这不是一个简单的方法......所以也许首先尝试TextPainter - 我知道这是一种解决方法,但它会更容易......
  • 感谢您的提醒。无论如何,您不提供示例代码有点放弃了;)
  • 我有一个写得很好的代码..明天会和你分享

标签: flutter alignment chat


【解决方案1】:

这是一个可以在 DartPad 中运行的示例,它可能足以让您入门。它使用SingleChildRenderObjectWidget 来布置孩子并绘制ChatBubble 的聊天消息以及消息时间和一个虚拟复选标记图标。

要了解有关RenderObject 课程的更多信息,我可以推荐此video。它深入描述了所有相关的类和方法,并帮助我创建了我的第一个自定义RenderObject

import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:intl/intl.dart';

const Color darkBlue = Color.fromARGB(255, 18, 32, 47);

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark().copyWith(
        scaffoldBackgroundColor: darkBlue,
      ),
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: ExampleChatBubbles(),
        ),
      ),
    );
  }
}

class ChatBubble extends StatelessWidget {
  final String message;
  final DateTime messageTime;
  final Alignment alignment;
  final Icon icon;
  final TextStyle textStyleMessage;
  final TextStyle textStyleMessageTime;
  // The available max width for the chat bubble in percent of the incoming constraints
  final int maxChatBubbleWidthPercentage;

  const ChatBubble({
    Key? key,
    required this.message,
    required this.icon,
    required this.alignment,
    required this.messageTime,
    this.maxChatBubbleWidthPercentage = 80,
    this.textStyleMessage = const TextStyle(
      fontSize: 11,
      color: Colors.black,
    ),
    this.textStyleMessageTime = const TextStyle(
      fontSize: 11,
      color: Colors.black,
    ),
  })  : assert(
          maxChatBubbleWidthPercentage <= 100 &&
              maxChatBubbleWidthPercentage >= 50,
          'maxChatBubbleWidthPercentage width must lie between 50 and 100%',
        ),
        super(key: key);

  @override
  Widget build(BuildContext context) {
    final textSpan = TextSpan(text: message, style: textStyleMessage);
    final textPainter = TextPainter(
      text: textSpan,
      textDirection: ui.TextDirection.ltr,
    );

    return Align(
      alignment: alignment,
      child: Container(
        padding: const EdgeInsets.symmetric(
          horizontal: 5,
          vertical: 5,
        ),
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(5),
          color: Colors.green.shade200,
        ),
        child: InnerChatBubble(
          maxChatBubbleWidthPercentage: maxChatBubbleWidthPercentage,
          textPainter: textPainter,
          child: Padding(
            padding: const EdgeInsets.only(
              left: 15,
            ),
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                Text(
                  DateFormat('hh:mm').format(messageTime),
                  style: textStyleMessageTime,
                ),
                const SizedBox(
                  width: 5,
                ),
                icon
              ],
            ),
          ),
        ),
      ),
    );
  }
}

// By using a SingleChildRenderObjectWidget we have full control about the whole
// layout and painting process.
class InnerChatBubble extends SingleChildRenderObjectWidget {
  final TextPainter textPainter;
  final int maxChatBubbleWidthPercentage;
  const InnerChatBubble({
    Key? key,
    required this.textPainter,
    required this.maxChatBubbleWidthPercentage,
    Widget? child,
  }) : super(key: key, child: child);

  @override
  RenderObject createRenderObject(BuildContext context) {
    return RenderInnerChatBubble(textPainter, maxChatBubbleWidthPercentage);
  }

  @override
  void updateRenderObject(
      BuildContext context, RenderInnerChatBubble renderObject) {
    renderObject
      ..textPainter = textPainter
      ..maxChatBubbleWidthPercentage = maxChatBubbleWidthPercentage;
  }
}

class RenderInnerChatBubble extends RenderBox
    with RenderObjectWithChildMixin<RenderBox> {
  TextPainter _textPainter;
  int _maxChatBubbleWidthPercentage;
  double _lastLineHeight = 0;

  RenderInnerChatBubble(
      TextPainter textPainter, int maxChatBubbleWidthPercentage)
      : _textPainter = textPainter,
        _maxChatBubbleWidthPercentage = maxChatBubbleWidthPercentage;

  TextPainter get textPainter => _textPainter;
  set textPainter(TextPainter value) {
    if (_textPainter == value) return;
    _textPainter = value;
    markNeedsLayout();
  }

  int get maxChatBubbleWidthPercentage => _maxChatBubbleWidthPercentage;
  set maxChatBubbleWidthPercentage(int value) {
    if (_maxChatBubbleWidthPercentage == value) return;
    _maxChatBubbleWidthPercentage = value;
    markNeedsLayout();
  }

  @override
  void performLayout() {
    // Layout child and calculate size
    size = _performLayout(
      constraints: constraints,
      dry: false,
    );

    // Position child
    final BoxParentData childParentData = child!.parentData as BoxParentData;
    childParentData.offset = Offset(
        size.width - child!.size.width, textPainter.height - _lastLineHeight);
  }

  @override
  Size computeDryLayout(BoxConstraints constraints) {
    return _performLayout(constraints: constraints, dry: true);
  }

  Size _performLayout({
    required BoxConstraints constraints,
    required bool dry,
  }) {
    final BoxConstraints constraints =
        this.constraints * (_maxChatBubbleWidthPercentage / 100);

    textPainter.layout(minWidth: 0, maxWidth: constraints.maxWidth);
    double height = textPainter.height;
    double width = textPainter.width;
    // Compute the LineMetrics of our textPainter
    final List<ui.LineMetrics> lines = textPainter.computeLineMetrics();
    // We are only interested in the last line's width
    final lastLineWidth = lines.last.width;
    _lastLineHeight = lines.last.height;

    // Layout child and assign size of RenderBox
    if (child != null) {
      late final Size childSize;
      if (!dry) {
        child!.layout(BoxConstraints(maxWidth: constraints.maxWidth),
            parentUsesSize: true);
        childSize = child!.size;
      } else {
        childSize =
            child!.getDryLayout(BoxConstraints(maxWidth: constraints.maxWidth));
      }

      final horizontalSpaceExceeded =
          lastLineWidth + childSize.width > constraints.maxWidth;

      if (horizontalSpaceExceeded) {
        height += childSize.height;
        _lastLineHeight = 0;
      } else {
        height += childSize.height - _lastLineHeight;
      }
      if (lines.length == 1 && !horizontalSpaceExceeded) {
        width += childSize.width;
      }
    }
    return Size(width, height);
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    // Paint the chat message
    textPainter.paint(context.canvas, offset);
    if (child != null) {
      final parentData = child!.parentData as BoxParentData;
      // Paint the child (i.e. the row with the messageTime and Icon)
      context.paintChild(child!, offset + parentData.offset);
    }
  }
}

class ExampleChatBubbles extends StatelessWidget {
  // Some chat dummy data
  final chatData = [
    [
      'Hi',
      Alignment.centerRight,
      DateTime.now().add(const Duration(minutes: -100)),
    ],
    [
      'Helloooo?',
      Alignment.centerRight,
      DateTime.now().add(const Duration(minutes: -60)),
    ],
    [
      'Hi James',
      Alignment.centerLeft,
      DateTime.now().add(const Duration(minutes: -58)),
    ],
    [
      'Do you want to watch the basketball game tonight? We could order some chinese food :)',
      Alignment.centerRight,
      DateTime.now().add(const Duration(minutes: -57)),
    ],
    [
      'Sounds great! Let us meet at 7 PM, okay?',
      Alignment.centerLeft,
      DateTime.now().add(const Duration(minutes: -57)),
    ],
    [
      'See you later!',
      Alignment.centerRight,
      DateTime.now().add(const Duration(minutes: -55)),
    ],
  ];

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: ListView.builder(
        itemCount: chatData.length,
        itemBuilder: (context, index) {
          return Padding(
            padding: const EdgeInsets.symmetric(
              vertical: 5,
            ),
            child: ChatBubble(
              icon: Icon(
                Icons.check,
                size: 15,
                color: Colors.grey.shade700,
              ),
              alignment: chatData[index][1] as Alignment,
              message: chatData[index][0] as String,
              messageTime: chatData[index][2] as DateTime,
              // How much of the available width may be consumed by the ChatBubble
              maxChatBubbleWidthPercentage: 75,
            ),
          );
        },
      ),
    );
  }
}

【讨论】:

  • 聊天气泡的宽度都是一样的,但我想这就是你计算剩余空间的方式。不管怎样,你给我的已经够多了。谢谢你:)
  • 这是RenderObject 的一大特色。您设置RenderBox 的高度和宽度。我相应地更新了我的答案。您现在可以设置maxChatBubbleWidthPercentage,它将定义ChatBubble 的最大宽度为传入约束的可用maxWidth 的百分比。 ChatBubble 现在只会占用消息和图标所需的空间。
  • 已经开始学习RenderObject了。非常感谢您的帮助。谢谢!
猜你喜欢
  • 2022-06-13
  • 1970-01-01
  • 2021-04-18
  • 2021-12-10
  • 2020-03-14
  • 2017-01-21
  • 2019-09-14
  • 1970-01-01
  • 2022-11-17
相关资源
最近更新 更多