diff --git a/analysis_options.yaml b/analysis_options.yaml index 7f38dd4..e8fd3e4 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -13,7 +13,6 @@ linter: rules: - avoid_function_literals_in_foreach_calls - avoid_renaming_method_parameters - - avoid_returning_null - avoid_unused_constructor_parameters - await_only_futures - camel_case_types @@ -25,13 +24,10 @@ linter: - empty_statements - implementation_imports - invariant_booleans - - iterable_contains_unrelated_type - - list_remove_unrelated_type - no_adjacent_strings_in_list - non_constant_identifier_names - only_throw_errors - overridden_fields - - package_api_docs - package_names - package_prefixed_library_names - prefer_final_locals diff --git a/demo/android/app/build.gradle b/demo/android/app/build.gradle index 73116aa..fee9f39 100644 --- a/demo/android/app/build.gradle +++ b/demo/android/app/build.gradle @@ -12,7 +12,8 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 27 + namespace 'com.github.leisim.auto_size_text.demo' + compileSdk 34 lintOptions { disable 'InvalidPackage' @@ -20,8 +21,8 @@ android { defaultConfig { applicationId "com.github.leisim.auto_size_text.demo" - minSdkVersion 16 - targetSdkVersion 27 + minSdkVersion 21 + targetSdkVersion 34 versionCode 1 versionName 'v1' } diff --git a/demo/android/app/src/main/AndroidManifest.xml b/demo/android/app/src/main/AndroidManifest.xml index 1c5a59b..7810dc8 100644 --- a/demo/android/app/src/main/AndroidManifest.xml +++ b/demo/android/app/src/main/AndroidManifest.xml @@ -10,6 +10,7 @@ android:icon="@drawable/ic_launcher"> { title: Text('preset'), activeColor: colors[4], ), - BottomNavyBarItem( - icon: Icon(MdiIcons.stackOverflow), - title: Text('replacement'), - activeColor: colors[5], - ), ], ), ); diff --git a/demo/pubspec.yaml b/demo/pubspec.yaml index 9ad504a..f928fbf 100644 --- a/demo/pubspec.yaml +++ b/demo/pubspec.yaml @@ -1,10 +1,10 @@ name: demo description: AutoSizeText Demo App -version: 1.0.0+2 +version: 1.0.0+3 environment: - sdk: '>=2.12.0-0 <3.0.0' + sdk: ">=3.2.0 <4.0.0" dependencies: flutter: @@ -13,14 +13,14 @@ dependencies: auto_size_text: path: ../ - bottom_navy_bar: ^4.2.0 - material_design_icons_flutter: ^4.0.5755 + bottom_navy_bar: ^6.1.0 + material_design_icons_flutter: ^7.0.7296 dev_dependencies: - flutter_test: + flutter_test: sdk: flutter flutter: uses-material-design: true -publish_to: none \ No newline at end of file +publish_to: none diff --git a/lib/auto_size_text.dart b/lib/auto_size_text.dart index 0ff37b6..78b7bc9 100644 --- a/lib/auto_size_text.dart +++ b/lib/auto_size_text.dart @@ -3,6 +3,7 @@ library auto_size_text; import 'dart:async'; +import 'dart:math'; import 'package:flutter/widgets.dart'; diff --git a/lib/src/auto_size_text.dart b/lib/src/auto_size_text.dart index ec43838..aa43f65 100644 --- a/lib/src/auto_size_text.dart +++ b/lib/src/auto_size_text.dart @@ -31,6 +31,7 @@ class AutoSizeText extends StatefulWidget { this.overflowReplacement, this.textScaleFactor, this.maxLines, + this.minLetterSpacing, this.semanticsLabel, }) : textSpan = null, super(key: key); @@ -56,6 +57,7 @@ class AutoSizeText extends StatefulWidget { this.overflowReplacement, this.textScaleFactor, this.maxLines, + this.minLetterSpacing, this.semanticsLabel, }) : data = null, super(key: key); @@ -184,7 +186,7 @@ class AutoSizeText extends StatefulWidget { /// This property also affects [minFontSize], [maxFontSize] and [presetFontSizes]. /// /// The value given to the constructor as textScaleFactor. If null, will - /// use the [MediaQueryData.textScaleFactor] obtained from the ambient + /// use [TextScaler] obtained from the ambient /// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope. final double? textScaleFactor; @@ -201,6 +203,17 @@ class AutoSizeText extends StatefulWidget { /// widget directly to entirely override the [DefaultTextStyle]. final int? maxLines; + /// The minimum letter spacing constraint to be used when auto-sizing text. + /// + /// When specified, if the minimum font size is achieved and the text still + /// doesn't fit the available area, the letter spacing will be decreased until + /// the text fits or the [minLetterSpacing] value is achieved. It is decreased + /// by [stepGranularity] on each iteration. + final double? minLetterSpacing; + + // The default letter spacing if none is specified. + static const double _defaultLetterSpacing = 0; + /// An alternative semantics label for this text. /// /// If present, the semantics of this widget will contain this value instead @@ -254,17 +267,20 @@ class _AutoSizeTextState extends State { _validateProperties(style, maxLines); - final result = _calculateFontSize(size, style, maxLines); + final result = + _calculateFontSize(size, style, maxLines, widget.minLetterSpacing); final fontSize = result[0] as double; final textFits = result[1] as bool; + final letterSpacing = result[2] as double?; Widget text; if (widget.group != null) { widget.group!._updateFontSize(this, fontSize); - text = _buildText(widget.group!._fontSize, style, maxLines); + text = + _buildText(widget.group!._fontSize, letterSpacing, style, maxLines); } else { - text = _buildText(fontSize, style, maxLines); + text = _buildText(fontSize, letterSpacing, style, maxLines); } if (widget.overflowReplacement != null && !textFits) { @@ -306,7 +322,11 @@ class _AutoSizeTextState extends State { } List _calculateFontSize( - BoxConstraints size, TextStyle? style, int? maxLines) { + BoxConstraints size, + TextStyle? style, + int? maxLines, + double? minLetterSpacing, + ) { final span = TextSpan( style: widget.textSpan?.style ?? style, text: widget.textSpan?.text ?? widget.data, @@ -314,8 +334,9 @@ class _AutoSizeTextState extends State { recognizer: widget.textSpan?.recognizer, ); - final userScale = - widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context); + final userScale = widget.textScaleFactor ?? 1.0; + + var letterSpacing = span.style!.letterSpacing; int left; int right; @@ -325,8 +346,9 @@ class _AutoSizeTextState extends State { final num defaultFontSize = style!.fontSize!.clamp(widget.minFontSize, widget.maxFontSize); final defaultScale = defaultFontSize * userScale / style.fontSize!; + if (_checkTextFits(span, defaultScale, maxLines, size)) { - return [defaultFontSize * userScale, true]; + return [defaultFontSize * userScale, true, letterSpacing]; } left = (widget.minFontSize / widget.stepGranularity).floor(); @@ -337,9 +359,9 @@ class _AutoSizeTextState extends State { } var lastValueFits = false; + var scale = userScale; while (left <= right) { final mid = (left + (right - left) / 2).floor(); - double scale; if (presetFontSizes == null) { scale = mid * userScale * widget.stepGranularity / style!.fontSize!; } else { @@ -355,6 +377,20 @@ class _AutoSizeTextState extends State { if (!lastValueFits) { right += 1; + if (minLetterSpacing != null) { + letterSpacing = letterSpacing ?? AutoSizeText._defaultLetterSpacing; + do { + letterSpacing = + max(minLetterSpacing, letterSpacing! - widget.stepGranularity); + final stepSpan = TextSpan( + style: span.style!.copyWith(letterSpacing: letterSpacing), + text: span.text, + children: span.children, + recognizer: span.recognizer, + ); + lastValueFits = _checkTextFits(stepSpan, scale, maxLines, size); + } while (!lastValueFits && letterSpacing > minLetterSpacing); + } } double fontSize; @@ -364,7 +400,7 @@ class _AutoSizeTextState extends State { fontSize = presetFontSizes[right] * userScale; } - return [fontSize, lastValueFits]; + return [fontSize, lastValueFits, letterSpacing]; } bool _checkTextFits( @@ -379,7 +415,7 @@ class _AutoSizeTextState extends State { ), textAlign: widget.textAlign ?? TextAlign.left, textDirection: widget.textDirection ?? TextDirection.ltr, - textScaleFactor: scale, + textScaler: TextScaler.linear(scale), maxLines: words.length, locale: widget.locale, strutStyle: widget.strutStyle, @@ -397,7 +433,7 @@ class _AutoSizeTextState extends State { text: text, textAlign: widget.textAlign ?? TextAlign.left, textDirection: widget.textDirection ?? TextDirection.ltr, - textScaleFactor: scale, + textScaler: TextScaler.linear(scale), maxLines: maxLines, locale: widget.locale, strutStyle: widget.strutStyle, @@ -410,19 +446,20 @@ class _AutoSizeTextState extends State { textPainter.width > constraints.maxWidth); } - Widget _buildText(double fontSize, TextStyle style, int? maxLines) { + Widget _buildText( + double fontSize, double? letterSpacing, TextStyle style, int? maxLines) { if (widget.data != null) { return Text( widget.data!, key: widget.textKey, - style: style.copyWith(fontSize: fontSize), + style: style.copyWith(fontSize: fontSize, letterSpacing: letterSpacing), strutStyle: widget.strutStyle, textAlign: widget.textAlign, textDirection: widget.textDirection, locale: widget.locale, softWrap: widget.softWrap, overflow: widget.overflow, - textScaleFactor: 1, + textScaler: TextScaler.noScaling, maxLines: maxLines, semanticsLabel: widget.semanticsLabel, ); @@ -430,14 +467,14 @@ class _AutoSizeTextState extends State { return Text.rich( widget.textSpan!, key: widget.textKey, - style: style, + style: style.copyWith(letterSpacing: letterSpacing), strutStyle: widget.strutStyle, textAlign: widget.textAlign, textDirection: widget.textDirection, locale: widget.locale, softWrap: widget.softWrap, overflow: widget.overflow, - textScaleFactor: fontSize / style.fontSize!, + textScaler: TextScaler.linear(fontSize / style.fontSize!), maxLines: maxLines, semanticsLabel: widget.semanticsLabel, ); diff --git a/pubspec.yaml b/pubspec.yaml index 758bbdb..6396a74 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,10 @@ name: auto_size_text description: Flutter widget that automatically resizes text to fit perfectly within its bounds. -version: 3.0.0 +version: 3.1.0 homepage: https://github.com/leisim/auto_size_text environment: - sdk: '>=2.12.0 <3.0.0' + sdk: ">=3.2.0 <4.0.0" dependencies: flutter: @@ -13,4 +13,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: '>=1.11.1 <3.0.0' \ No newline at end of file + lints: ^4.0.0 diff --git a/test/min_letter_spacing_test.dart b/test/min_letter_spacing_test.dart new file mode 100644 index 0000000..b667fc3 --- /dev/null +++ b/test/min_letter_spacing_test.dart @@ -0,0 +1,77 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'utils.dart'; + +void main() { + testWidgets('Does not change letterSpacing if no minLetterSpacing is passed', + (tester) async { + await pumpAndExpectLetterSpacing( + tester: tester, + expectedLetterSpacing: null, + widget: SizedBox( + width: 100, + child: AutoSizeText( + 'XXXXX', + style: TextStyle(fontSize: 60), + minFontSize: 60, + maxLines: 1, + ), + ), + ); + }); + + testWidgets('Does not change letterSpacing if the text fits with minFontSize', + (tester) async { + await pumpAndExpectLetterSpacing( + tester: tester, + expectedLetterSpacing: null, + widget: SizedBox( + width: 100, + child: AutoSizeText( + 'XXXXX', + style: TextStyle(fontSize: 60), + minFontSize: 20, + maxLines: 1, + minLetterSpacing: -60, + ), + ), + ); + }); + + testWidgets('Respects minLetterSpacing', (tester) async { + await pumpAndExpectLetterSpacing( + tester: tester, + expectedLetterSpacing: -20, + widget: SizedBox( + width: 100, + child: AutoSizeText( + 'XXXXX', + style: TextStyle(fontSize: 60), + minFontSize: 60, + maxLines: 1, + minLetterSpacing: -20, + ), + ), + ); + }); + + testWidgets('letterSpacing is larger than minLetterSpacing if enough space', + (tester) async { + await pumpAndExpectLetterSpacing( + tester: tester, + expectedLetterSpacing: -40, + widget: SizedBox( + width: 100, + child: AutoSizeText( + 'XXXXX', + style: TextStyle(fontSize: 60), + minFontSize: 60, + maxLines: 1, + minLetterSpacing: -60, + ), + ), + ); + }); +} diff --git a/test/min_max_font_size_test.dart b/test/min_max_font_size_test.dart index ad7cea3..d6bd9d9 100644 --- a/test/min_max_font_size_test.dart +++ b/test/min_max_font_size_test.dart @@ -1,6 +1,5 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'utils.dart'; diff --git a/test/utils.dart b/test/utils.dart index f45978b..ab5cb8e 100644 --- a/test/utils.dart +++ b/test/utils.dart @@ -1,12 +1,13 @@ import 'dart:io'; -import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; double effectiveFontSize(Text text) => - (text.textScaleFactor ?? 1) * text.style!.fontSize!; + text.textScaler?.scale(text.style!.fontSize!) ?? text.style!.fontSize!; + +double? effectiveLetterSpacing(Text text) => text.style!.letterSpacing; bool doesTextFit( Text text, [ @@ -25,7 +26,7 @@ bool doesTextFit( text: span, textAlign: text.textAlign ?? TextAlign.start, textDirection: text.textDirection, - textScaleFactor: text.textScaleFactor ?? 1, + textScaler: text.textScaler ?? TextScaler.linear(1), maxLines: text.maxLines, locale: text.locale, strutStyle: text.strutStyle, @@ -85,6 +86,15 @@ Future pumpAndExpectFontSize({ expect(effectiveFontSize(text), expectedFontSize); } +Future pumpAndExpectLetterSpacing({ + required WidgetTester tester, + required double? expectedLetterSpacing, + required Widget widget, +}) async { + final text = await pumpAndGetText(tester: tester, widget: widget); + expect(effectiveLetterSpacing(text), expectedLetterSpacing); +} + RichText getRichText(WidgetTester tester) => tester.widget(find.byType(RichText));