我不知道怎么和Flutter对齐Whatsapp或Telegram上的右转换是左对齐的,但日期在右边。如果有可用的空间,则日期位于同一行的末尾。
第一行和第三行聊天可以用Wrap()小部件完成。但是第二行不能用Wrap(),因为聊天文本是一个单独的小部件,填充了整个宽度,不允许日期小部件适合。你会如何用Flutter做到这一点?
Wrap()
x8goxv8g1#
这里有一个例子,你可以在DartPad中运行,它可能足以让你开始。它使用SingleChildRenderObjectWidget来布局孩子,并绘制ChatBubble的聊天消息以及消息时间和一个虚拟的复选标记图标。要了解有关RenderObject类的更多信息,我可以推荐这个video。它非常深入地描述了所有相关的类和方法,并帮助我创建了第一个自定义RenderObject。
SingleChildRenderObjectWidget
ChatBubble
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, ), ); }, ), ); } }
shyt4zoc2#
@hnnngwdlch感谢您的回答它帮助了我,有了这个你就可以完全控制画家了。我稍微修改了你的代码,也许对某人有用。PD:我不知道在RenderObject中声明TextPainter是否有明显的性能缺点,如果有人知道请在评论中写。
class TextMessageWidget extends SingleChildRenderObjectWidget { final String text; final TextStyle? textStyle; final double? spacing; const TextMessageWidget({ Key? key, required this.text, this.textStyle, this.spacing, required Widget child, }) : super(key: key, child: child); @override RenderObject createRenderObject(BuildContext context) { return RenderTextMessageWidget(text, textStyle, spacing); } @override void updateRenderObject(BuildContext context, RenderTextMessageWidget renderObject) { renderObject ..text = text ..textStyle = textStyle ..spacing = spacing; } } class RenderTextMessageWidget extends RenderBox with RenderObjectWithChildMixin<RenderBox> { String _text; TextStyle? _textStyle; double? _spacing; // With this constants you can modify the final result static const double _kOffset = 1.5; static const double _kFactor = 0.8; RenderTextMessageWidget( String text, TextStyle? textStyle, double? spacing ) : _text = text, _textStyle = textStyle, _spacing = spacing; String get text => _text; set text(String value) { if (_text == value) return; _text = value; markNeedsLayout(); } TextStyle? get textStyle => _textStyle; set textStyle(TextStyle? value) { if (_textStyle == value) return; _textStyle = value; markNeedsLayout(); } double? get spacing => _spacing; set spacing(double? value) { if (_spacing == value) return; _spacing = value; markNeedsLayout(); } TextPainter textPainter = TextPainter(); @override void performLayout() { size = _performLayout(constraints: constraints, dry: false); final BoxParentData childParentData = child!.parentData as BoxParentData; childParentData.offset = Offset( size.width - child!.size.width, size.height - child!.size.height / _kOffset ); } @override Size computeDryLayout(BoxConstraints constraints) { return _performLayout(constraints: constraints, dry: true); } Size _performLayout({required BoxConstraints constraints, required bool dry}) { textPainter = TextPainter( text: TextSpan(text: _text, style: _textStyle), textDirection: TextDirection.ltr ); late final double spacing; if(_spacing == null){ spacing = constraints.maxWidth * 0.03; } else { spacing = _spacing!; } textPainter.layout(minWidth: 0, maxWidth: constraints.maxWidth); double height = textPainter.height; double width = textPainter.width; // Compute the LineMetrics of our textPainter final List<LineMetrics> lines = textPainter.computeLineMetrics(); // We are only interested in the last line's width final lastLineWidth = lines.last.width; 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)); } if(lastLineWidth + spacing > constraints.maxWidth - child!.size.width) { height += (childSize.height * _kFactor); } else if(lines.length == 1){ width += childSize.width + spacing; } } return Size(width, height); } @override void paint(PaintingContext context, Offset offset) { textPainter.paint(context.canvas, offset); final parentData = child!.parentData as BoxParentData; context.paintChild(child!, offset + parentData.offset); } }
2条答案
按热度按时间x8goxv8g1#
这里有一个例子,你可以在DartPad中运行,它可能足以让你开始。它使用
SingleChildRenderObjectWidget
来布局孩子,并绘制ChatBubble
的聊天消息以及消息时间和一个虚拟的复选标记图标。要了解有关
RenderObject
类的更多信息,我可以推荐这个video。它非常深入地描述了所有相关的类和方法,并帮助我创建了第一个自定义RenderObject
。shyt4zoc2#
@hnnngwdlch感谢您的回答它帮助了我,有了这个你就可以完全控制画家了。我稍微修改了你的代码,也许对某人有用。
PD:我不知道在RenderObject中声明TextPainter是否有明显的性能缺点,如果有人知道请在评论中写。