diff --git a/lib/flutter_auto_size_text.dart b/lib/flutter_auto_size_text.dart index bf429ce..591e4e8 100644 --- a/lib/flutter_auto_size_text.dart +++ b/lib/flutter_auto_size_text.dart @@ -1 +1,22 @@ -export 'src/auto_size_text.dart'; +library; + +import 'dart:async'; +import 'dart:math'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +part 'src/auto_size_group.dart'; + +part 'src/auto_size_group_builder.dart'; + +part 'src/auto_size_text.dart'; + +part 'src/text_fitter.dart'; + +part 'src/auto_size_builder/auto_size.dart'; + +part 'src/auto_size_builder/auto_size_builder.dart'; + +part 'src/auto_size_builder/auto_size_element.dart'; + +part 'src/auto_size_builder/render_auto_size.dart'; diff --git a/lib/src/auto_size_builder/auto_size.dart b/lib/src/auto_size_builder/auto_size.dart index 08cd144..6273e5b 100644 --- a/lib/src/auto_size_builder/auto_size.dart +++ b/lib/src/auto_size_builder/auto_size.dart @@ -1,4 +1,4 @@ -part of 'auto_size_builder.dart'; +part of '../../flutter_auto_size_text.dart'; class _AutoSize extends RenderObjectWidget { _AutoSize({ @@ -17,13 +17,15 @@ class _AutoSize extends RenderObjectWidget { required this.textScaleFactor, required this.minFontSize, required this.maxFontSize, + this.groupMaxFontSize, required this.stepGranularity, required this.presetFontSizes, + this.onMaxPossibleFontSizeChanged, }) { _validateProperties(); } - final AutoSizeTextBuilder builder; + final _AutoSizeTextBuilder builder; final Widget? overflowReplacement; final TextSpan text; @@ -41,6 +43,8 @@ class _AutoSize extends RenderObjectWidget { final double maxFontSize; final double stepGranularity; final List? presetFontSizes; + final double? groupMaxFontSize; + final void Function(double)? onMaxPossibleFontSizeChanged; TextFitter _buildFitter() { return TextFitter( @@ -64,13 +68,17 @@ class _AutoSize extends RenderObjectWidget { @override _RenderAutoSize createRenderObject(BuildContext context) { - return _RenderAutoSize(fitter: _buildFitter()); + return _RenderAutoSize( + fitter: _buildFitter(), + onMaxPossibleFontSizeChanged: onMaxPossibleFontSizeChanged, + groupMaxFontSize: groupMaxFontSize); } @override void updateRenderObject( BuildContext context, covariant _RenderAutoSize renderObject) { renderObject.updateTextFitter(_buildFitter()); + renderObject.updateGroupMaxFontSize(groupMaxFontSize); } @override diff --git a/lib/src/auto_size_builder/auto_size_builder.dart b/lib/src/auto_size_builder/auto_size_builder.dart index 6ccdf56..eb605ee 100644 --- a/lib/src/auto_size_builder/auto_size_builder.dart +++ b/lib/src/auto_size_builder/auto_size_builder.dart @@ -1,16 +1,10 @@ -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_auto_size_text/src/text_fitter.dart'; +part of '../../flutter_auto_size_text.dart'; -part 'auto_size.dart'; -part 'auto_size_element.dart'; -part 'render_auto_size.dart'; +typedef _AutoSizeTextBuilder = Widget Function(BuildContext context, + double textScaleFactor, double? groupMaxFontSize, bool overflow); -typedef AutoSizeTextBuilder = Widget Function( - BuildContext context, double textScaleFactor, bool overflow); - -class AutoSizeBuilder extends StatefulWidget { - const AutoSizeBuilder({ +class _AutoSizeBuilder extends StatefulWidget { + const _AutoSizeBuilder({ super.key, required this.builder, this.overflowReplacement, @@ -30,9 +24,10 @@ class AutoSizeBuilder extends StatefulWidget { this.maxFontSize, this.stepGranularity, this.presetFontSizes, + this.group, }); - final AutoSizeTextBuilder builder; + final _AutoSizeTextBuilder builder; /// {@macro auto_size_text.overflowReplacement} final Widget? overflowReplacement; @@ -83,11 +78,29 @@ class AutoSizeBuilder extends StatefulWidget { /// {@macro auto_size_text.presetFontSizes} final List? presetFontSizes; + /// {@macro auto_size_text.group} + final AutoSizeGroup? group; + @override - State createState() => _AutoSizeBuilderState(); + State<_AutoSizeBuilder> createState() => _AutoSizeBuilderState(); } -class _AutoSizeBuilderState extends State { +class _AutoSizeBuilderState extends State<_AutoSizeBuilder> { + @override + void initState() { + super.initState(); + widget.group?._register(this); + } + + @override + void didUpdateWidget(covariant _AutoSizeBuilder oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.group != widget.group) { + oldWidget.group?._remove(this); + widget.group?._register(this); + } + } + @override Widget build(BuildContext context) { final defaultTextStyle = DefaultTextStyle.of(context); @@ -127,8 +140,24 @@ class _AutoSizeBuilderState extends State { textScaleFactor: widget.textScaleFactor ?? 1, minFontSize: widget.minFontSize ?? 12.0, maxFontSize: widget.maxFontSize ?? double.infinity, + groupMaxFontSize: widget.group?._effectiveMaxPossibleFontSize, stepGranularity: widget.stepGranularity ?? 1.0, presetFontSizes: widget.presetFontSizes, + onMaxPossibleFontSizeChanged: widget.group != null + ? (maxPossibleFontSize) { + widget.group?._updateFontSize(this, maxPossibleFontSize); + } + : null, ); } + + void _notifySync() { + setState(() {}); + } + + @override + void dispose() { + widget.group?._remove(this); + super.dispose(); + } } diff --git a/lib/src/auto_size_builder/auto_size_element.dart b/lib/src/auto_size_builder/auto_size_element.dart index e5e9868..e7b9145 100644 --- a/lib/src/auto_size_builder/auto_size_element.dart +++ b/lib/src/auto_size_builder/auto_size_element.dart @@ -1,4 +1,4 @@ -part of 'auto_size_builder.dart'; +part of '../../flutter_auto_size_text.dart'; class _AutoSizeElement extends RenderObjectElement { _AutoSizeElement(_AutoSize super.widget); @@ -40,12 +40,12 @@ class _AutoSizeElement extends RenderObjectElement { super.unmount(); } - void _updateText(double textScaleFactor, bool overflow) { + void _updateText(double textScaleFactor, double? groupMaxFontSize, bool overflow) { owner!.buildScope(this, () { _overflow = overflow; Widget built; try { - built = widget.builder(this, textScaleFactor, overflow); + built = widget.builder(this, textScaleFactor, groupMaxFontSize, overflow); debugWidgetBuilderValue(widget, built); } catch (e) { built = ErrorWidget(e); diff --git a/lib/src/auto_size_builder/render_auto_size.dart b/lib/src/auto_size_builder/render_auto_size.dart index 6c72190..1f6f087 100644 --- a/lib/src/auto_size_builder/render_auto_size.dart +++ b/lib/src/auto_size_builder/render_auto_size.dart @@ -1,4 +1,4 @@ -part of 'auto_size_builder.dart'; +part of '../../flutter_auto_size_text.dart'; class _AutoSizeParentData extends ParentData with ContainerParentDataMixin {} @@ -7,7 +7,11 @@ class _RenderAutoSize extends RenderBox with ContainerRenderObjectMixin> { - _RenderAutoSize({required TextFitter fitter}) : _fitter = fitter; + _RenderAutoSize( + {required TextFitter fitter, + this.onMaxPossibleFontSizeChanged, + required this.groupMaxFontSize}) + : _fitter = fitter; var _overflow = false; var _needsBuild = true; @@ -15,8 +19,11 @@ class _RenderAutoSize extends RenderBox bool? _previousOverflow; double? _longestWordWidth; - Function(double, bool)? _buildCallback; - void updateBuildCallback(Function(double, bool)? buildCallback) { + Function(double, double?, bool)? _buildCallback; + double? groupMaxFontSize; + final void Function(double)? onMaxPossibleFontSizeChanged; + + void updateBuildCallback(Function(double, double?, bool)? buildCallback) { if (_buildCallback == buildCallback) return; _previousTextScaleFactor = null; _buildCallback = buildCallback; @@ -24,6 +31,7 @@ class _RenderAutoSize extends RenderBox } TextFitter _fitter; + void updateTextFitter(TextFitter fitter) { if (_fitter == fitter) return; if (_fitter.text != fitter.text) { @@ -34,6 +42,12 @@ class _RenderAutoSize extends RenderBox markNeedsLayout(); } + void updateGroupMaxFontSize(double? newGroupMaxFontSize) { + if (groupMaxFontSize == newGroupMaxFontSize) return; + groupMaxFontSize = newGroupMaxFontSize; + markNeedsLayout(); + } + RenderBox get child => _overflow ? lastChild! : firstChild!; bool get hasReplacement => !identical(firstChild, lastChild); @@ -115,8 +129,11 @@ class _RenderAutoSize extends RenderBox _previousTextScaleFactor = result.scale; _previousOverflow = result.overflow; _needsBuild = false; - invokeLayoutCallback( - (_) => _buildCallback!(result.scale, result.overflow)); + final fontSize = _fitter.text.style?.fontSize ?? _kDefaultFontSize; + final maxPossibleFontSize = fontSize * result.scale; + onMaxPossibleFontSizeChanged?.call(maxPossibleFontSize); + invokeLayoutCallback((_) => + _buildCallback!(result.scale, groupMaxFontSize, result.overflow)); } _overflow = result.overflow; diff --git a/lib/src/auto_size_group.dart b/lib/src/auto_size_group.dart new file mode 100644 index 0000000..8f286c8 --- /dev/null +++ b/lib/src/auto_size_group.dart @@ -0,0 +1,49 @@ +part of '../flutter_auto_size_text.dart'; + +/// Controller to synchronize the fontSize of multiple AutoSizeTexts. +class AutoSizeGroup { + final _listeners = <_AutoSizeBuilderState, double>{}; + var _widgetsNotified = false; + + void _register(_AutoSizeBuilderState text) { + _listeners[text] = double.infinity; + } + + double get _effectiveMaxPossibleFontSize { + final minMaxPossibleFontSize = _listeners.values.fold( + double.infinity, + (previousValue, element) => + element < previousValue ? element : previousValue); + return minMaxPossibleFontSize; + } + + void _updateFontSize(_AutoSizeBuilderState text, double maxPossibleFontSize) { + final oldEffectiveMaxPossibleFontSize = _effectiveMaxPossibleFontSize; + _listeners[text] = maxPossibleFontSize; + final newEffectiveMaxPossibleFontSize = _effectiveMaxPossibleFontSize; + + if (oldEffectiveMaxPossibleFontSize != newEffectiveMaxPossibleFontSize) { + _widgetsNotified = false; + scheduleMicrotask(_notifyListeners); + } + } + + void _notifyListeners() { + if (_widgetsNotified) { + return; + } else { + _widgetsNotified = true; + } + + for (final textState in _listeners.keys) { + if (textState.mounted) { + textState._notifySync(); + } + } + } + + void _remove(_AutoSizeBuilderState text) { + _updateFontSize(text, double.infinity); + _listeners.remove(text); + } +} diff --git a/lib/src/auto_size_group_builder.dart b/lib/src/auto_size_group_builder.dart new file mode 100644 index 0000000..49d274a --- /dev/null +++ b/lib/src/auto_size_group_builder.dart @@ -0,0 +1,22 @@ +part of '../flutter_auto_size_text.dart'; + +/// A Flutter widget that provides an [AutoSizeGroup] to its builder function. +class AutoSizeGroupBuilder extends StatefulWidget { + final Widget Function(BuildContext context, AutoSizeGroup autoSizeGroup) + builder; + + /// Creates an [AutoSizeGroupBuilder] widget. + const AutoSizeGroupBuilder({super.key, required this.builder}); + + @override + _AutoSizeGroupBuilderState createState() => _AutoSizeGroupBuilderState(); +} + +class _AutoSizeGroupBuilderState extends State { + final _group = AutoSizeGroup(); + + @override + Widget build(BuildContext context) { + return widget.builder(context, _group); + } +} diff --git a/lib/src/auto_size_text.dart b/lib/src/auto_size_text.dart index f98263e..de3443b 100644 --- a/lib/src/auto_size_text.dart +++ b/lib/src/auto_size_text.dart @@ -1,5 +1,4 @@ -import 'package:flutter/widgets.dart'; -import 'package:flutter_auto_size_text/src/auto_size_builder/auto_size_builder.dart'; +part of '../flutter_auto_size_text.dart'; /// Flutter widget that automatically resizes text to fit perfectly within its /// bounds. @@ -140,7 +139,7 @@ class AutoSizeText extends StatelessWidget { /// If you want multiple [AutoSizeText]s to have the same text size, give all /// of them the same [AutoSizeGroup] instance. All of them will have the /// size of the smallest [AutoSizeText] - //final AutoSizeGroup? group; + final AutoSizeGroup? group; /// {@template auto_size_text.wrapWords} /// Whether words which don't fit in one line should be wrapped. @@ -185,11 +184,11 @@ class AutoSizeText extends StatelessWidget { this.maxFontSize, this.stepGranularity, this.presetFontSizes, - //this.group, + this.group, this.wrapWords, this.overflowReplacement, this.overflowCallback, - }) : textSpan = null; + }) : textSpan = null; /// Creates a [AutoSizeText] widget with a [TextSpan]. const AutoSizeText.rich( @@ -212,20 +211,27 @@ class AutoSizeText extends StatelessWidget { this.maxFontSize, this.stepGranularity, this.presetFontSizes, - //this.group, + this.group, this.wrapWords, this.overflowReplacement, this.overflowCallback, - }) : data = null; + }) : data = null; @override Widget build(BuildContext context) { final span = textSpan ?? TextSpan(text: data); - return AutoSizeBuilder( + return _AutoSizeBuilder( text: span, style: style, - builder: (context, scale, overflow) { + builder: (context, scale, groupMaxFontSize, overflow) { overflowCallback?.call(overflow); + final fontSize = + span.style?.fontSize ?? style?.fontSize ?? _kDefaultFontSize; + final scaledFontSize = fontSize * scale; + final adjustedScale = + groupMaxFontSize != null && scaledFontSize > groupMaxFontSize + ? groupMaxFontSize / fontSize + : scale; return Text.rich( span, key: textKey, @@ -236,7 +242,7 @@ class AutoSizeText extends StatelessWidget { locale: locale, softWrap: softWrap, overflow: this.overflow, - textScaler: TextScaler.linear(scale), + textScaler: TextScaler.linear(adjustedScale), maxLines: maxLines, semanticsLabel: semanticsLabel, ); @@ -246,6 +252,7 @@ class AutoSizeText extends StatelessWidget { maxFontSize: maxFontSize, stepGranularity: stepGranularity, presetFontSizes: presetFontSizes, + group: group, textAlign: textAlign, textDirection: textDirection, locale: locale, diff --git a/lib/src/text_fitter.dart b/lib/src/text_fitter.dart index 31339df..878e0ac 100644 --- a/lib/src/text_fitter.dart +++ b/lib/src/text_fitter.dart @@ -1,6 +1,4 @@ -import 'dart:math'; - -import 'package:flutter/widgets.dart'; +part of '../flutter_auto_size_text.dart'; const _kDefaultFontSize = 14; diff --git a/test/group_builder_test.dart b/test/group_builder_test.dart new file mode 100644 index 0000000..b391438 --- /dev/null +++ b/test/group_builder_test.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_auto_size_text/flutter_auto_size_text.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'utils.dart'; + +Widget testWidget({required double width1, required double width2}) { + return MaterialApp( + home: AutoSizeGroupBuilder( + builder: (_, group) => Column( + children: [ + SizedBox( + width: width1, + height: 100, + child: AutoSizeText( + 'XXXXXX', + style: const TextStyle(fontSize: 60), + minFontSize: 1, + maxLines: 1, + group: group, + ), + ), + SizedBox( + width: width2, + height: 100.0, + child: AutoSizeText( + 'XXXXXX', + style: const TextStyle(fontSize: 60), + minFontSize: 1, + maxLines: 1, + group: group, + ), + ), + ], + ), + ), + ); +} + +void _expectFontSizes(WidgetTester tester, double fontSize) { + final texts = tester.widgetList(find.byType(RichText)); + for (final text in texts) { + expect(effectiveFontSize(text as RichText), fontSize); + } +} + +void main() { + testWidgets('Group sync', (tester) async { + await tester.pumpWidget(testWidget(width1: 300, width2: 300)); + + _expectFontSizes(tester, 50); + + await tester.pumpWidget(testWidget(width1: 200, width2: 300)); + await tester.pump(Duration.zero); + await tester.pump(Duration.zero); + + _expectFontSizes(tester, 33); + + await tester.pumpWidget(testWidget(width1: 200, width2: 150)); + await tester.pump(Duration.zero); + await tester.pump(Duration.zero); + + _expectFontSizes(tester, 25); + + await tester.pumpWidget(testWidget(width1: 200, width2: 100)); + await tester.pump(Duration.zero); + await tester.pump(Duration.zero); + + _expectFontSizes(tester, 16); + + await tester.pumpWidget(testWidget(width1: 60, width2: 60)); + await tester.pump(Duration.zero); + await tester.pump(Duration.zero); + + _expectFontSizes(tester, 10); + + await tester.pumpWidget(testWidget(width1: 200, width2: 60)); + await tester.pump(Duration.zero); + await tester.pump(Duration.zero); + + _expectFontSizes(tester, 10); + + await tester.pumpWidget(testWidget(width1: 200, width2: 250)); + await tester.pump(Duration.zero); + await tester.pump(Duration.zero); + + _expectFontSizes(tester, 33); + + await tester.pumpWidget(testWidget(width1: 250, width2: 250)); + await tester.pump(Duration.zero); + await tester.pump(Duration.zero); + + _expectFontSizes(tester, 41); + + await tester.pumpWidget(testWidget(width1: 300, width2: 300)); + await tester.pump(Duration.zero); + await tester.pump(Duration.zero); + // Upsizing both requires an extra frame to settle on the new size. + await tester.pump(); + + _expectFontSizes(tester, 50); + }); +} diff --git a/test/group_test.dart b/test/group_test.dart new file mode 100644 index 0000000..0a158ea --- /dev/null +++ b/test/group_test.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_auto_size_text/flutter_auto_size_text.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'utils.dart'; + +class GroupTest extends StatefulWidget { + @override + GroupTestState createState() => GroupTestState(); +} + +class GroupTestState extends State { + AutoSizeGroup group = AutoSizeGroup(); + double width1 = 300.0; + double width2 = 300.0; + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Column( + children: [ + SizedBox( + width: width1, + height: 100, + child: AutoSizeText( + 'XXXXXX', + style: const TextStyle(fontSize: 60), + minFontSize: 1, + maxLines: 1, + group: group, + ), + ), + SizedBox( + width: width2, + height: 100.0, + child: AutoSizeText( + 'XXXXXX', + style: const TextStyle(fontSize: 60), + minFontSize: 1, + maxLines: 1, + group: group, + ), + ), + ], + ), + ); + } + + void refresh() { + setState(() {}); + } +} + +void _expectFontSizes(WidgetTester tester, double fontSize) { + final texts = tester.widgetList(find.byType(RichText)); + for (final text in texts) { + expect(effectiveFontSize(text as RichText), fontSize); + } +} + +void main() { + testWidgets('Group sync', (tester) async { + await tester.pumpWidget(GroupTest()); + + _expectFontSizes(tester, 50); + + final state = tester.state(find.byType(GroupTest)) as GroupTestState; + + state.width1 = 200; + state.refresh(); + await tester.pump(Duration.zero); + await tester.pump(Duration.zero); + _expectFontSizes(tester, 33); + + state.width2 = 150; + state.refresh(); + await tester.pump(Duration.zero); + await tester.pump(Duration.zero); + _expectFontSizes(tester, 25); + + state.width2 = 100; + state.refresh(); + await tester.pump(Duration.zero); + await tester.pump(Duration.zero); + _expectFontSizes(tester, 16); + + state.width1 = 60; + state.width2 = 60; + state.refresh(); + await tester.pump(Duration.zero); + await tester.pump(Duration.zero); + _expectFontSizes(tester, 10); + + state.width1 = 200; + state.refresh(); + await tester.pump(Duration.zero); + await tester.pump(Duration.zero); + _expectFontSizes(tester, 10); + + state.width2 = 250; + state.refresh(); + await tester.pump(Duration.zero); + await tester.pump(Duration.zero); + _expectFontSizes(tester, 33); + + state.width1 = 250; + state.refresh(); + await tester.pump(Duration.zero); + await tester.pump(Duration.zero); + _expectFontSizes(tester, 41); + + state.width1 = 300; + state.width2 = 300; + state.refresh(); + await tester.pump(Duration.zero); + await tester.pump(Duration.zero); + _expectFontSizes(tester, 50); + + await tester.pump(Duration.zero); + }); +}