import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; /// {@template text_measure} /// TextMeasure widget. /// {@endtemplate} class TextMeasure extends StatefulWidget { /// {@macro text_measure} const TextMeasure({ super.key, required this.text, required this.width, this.style = const TextStyle( fontSize: 12, fontWeight: FontWeight.normal, ), required this.builder, this.onSizeChanged, }); /// Text to measure final String text; /// Max width of text final double width; /// Text style final TextStyle style; /// Callback with size final void Function(Size)? onSizeChanged; /// Builder final Widget Function(BuildContext context, Size size) builder; @override State createState() => _TextMeasureState(); } /// State for widget TextMeasure. class _TextMeasureState extends State { Size textSize = Size.zero; /* #region Lifecycle */ @override void initState() { super.initState(); rebuild(widget.text, widget.width, widget.style); } @override void didUpdateWidget(covariant TextMeasure oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.text == widget.text && oldWidget.width == widget.width && oldWidget.style == widget.style) { return; } rebuild(widget.text, widget.width, widget.style); } /* #endregion */ void rebuild(String text, double width, TextStyle style, {bool async = false}) => runZonedGuarded( () async { Size size; if (async) { // ignore: deprecated_member_use_from_same_package size = await measureTextAsync(text, width, style); } else { size = measureText(text, width, style); } if (!mounted || textSize == size) return; widget.onSizeChanged?.call(size); setState(() => textSize = size); }, (error, stackTrace) { if (!mounted) return; ScaffoldMessenger.maybeOf(context)?.showSnackBar( SnackBar( content: Text('$error'), duration: const Duration(seconds: 5), backgroundColor: Colors.red, action: SnackBarAction( label: 'Retry', onPressed: () => rebuild(text, width, style, async: true), ), ), ); setState(() => textSize = Size.zero); }, ); @Deprecated('Does not work in isolate') static FutureOr measureTextAsync( String text, double width, TextStyle style) => compute( _measureTextAsync$Compute, ( text: text, width: width, style: style, token: null /* RootIsolateToken.instance */, ), ); @Deprecated('Does not work in isolate') static FutureOr _measureTextAsync$Compute( ({ String text, double width, TextStyle style, RootIsolateToken? token }) args, ) { if (args.token case RootIsolateToken token) { // Initialize Flutter services and allow to use method channels BackgroundIsolateBinaryMessenger.ensureInitialized(token); } return measureText(args.text, args.width, args.style); } /// Измерение текста без рендеринга static Size measureText(String text, double width, TextStyle style) { // Создаем объект TextSpan, который содержит текст и его стиль final textSpan = TextSpan(text: text, style: style); // Создаем объект TextPainter, который может выполнять измерение текста без его реального рендеринга final textPainter = TextPainter( text: textSpan, // это важно, так как текст должен иметь направление для корректного измерения textDirection: TextDirection.ltr, // опционально, устанавливает максимальное количество строк //maxLines: 1, ) // Вызываем layout(), чтобы TextPainter мог произвести расчеты ..layout(maxWidth: width); // Теперь мы можем получить размеры текста return textPainter.size; } @override Widget build(BuildContext context) => widget.builder(context, textSize); }