Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow auto adjusting letterSpacing #135

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/auto_size_text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
library auto_size_text;

import 'dart:async';
import 'dart:math';

import 'package:flutter/widgets.dart';

Expand Down
57 changes: 47 additions & 10 deletions lib/src/auto_size_text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class AutoSizeText extends StatefulWidget {
this.overflowReplacement,
this.textScaleFactor,
this.maxLines,
this.minLetterSpacing,
this.semanticsLabel,
}) : textSpan = null,
super(key: key);
Expand All @@ -56,6 +57,7 @@ class AutoSizeText extends StatefulWidget {
this.overflowReplacement,
this.textScaleFactor,
this.maxLines,
this.minLetterSpacing,
this.semanticsLabel,
}) : data = null,
super(key: key);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -254,17 +267,20 @@ class _AutoSizeTextState extends State<AutoSizeText> {

_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) {
Expand Down Expand Up @@ -306,7 +322,11 @@ class _AutoSizeTextState extends State<AutoSizeText> {
}

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,
Expand All @@ -317,6 +337,8 @@ class _AutoSizeTextState extends State<AutoSizeText> {
final userScale =
widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context);

var letterSpacing = span.style!.letterSpacing;

int left;
int right;

Expand All @@ -326,7 +348,7 @@ class _AutoSizeTextState extends State<AutoSizeText> {
style!.fontSize!.clamp(widget.minFontSize, widget.maxFontSize);
final defaultScale = defaultFontSize * userScale / style.fontSize!;
if (_checkTextFits(span, defaultScale, maxLines, size)) {
return <Object>[defaultFontSize * userScale, true];
return <Object?>[defaultFontSize * userScale, true, letterSpacing];
}

left = (widget.minFontSize / widget.stepGranularity).floor();
Expand All @@ -337,9 +359,9 @@ class _AutoSizeTextState extends State<AutoSizeText> {
}

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 {
Expand All @@ -355,6 +377,20 @@ class _AutoSizeTextState extends State<AutoSizeText> {

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;
Expand All @@ -364,7 +400,7 @@ class _AutoSizeTextState extends State<AutoSizeText> {
fontSize = presetFontSizes[right] * userScale;
}

return <Object>[fontSize, lastValueFits];
return <Object?>[fontSize, lastValueFits, letterSpacing];
}

bool _checkTextFits(
Expand Down Expand Up @@ -410,12 +446,13 @@ class _AutoSizeTextState extends State<AutoSizeText> {
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,
Expand All @@ -430,7 +467,7 @@ class _AutoSizeTextState extends State<AutoSizeText> {
return Text.rich(
widget.textSpan!,
key: widget.textKey,
style: style,
style: style.copyWith(letterSpacing: letterSpacing),
strutStyle: widget.strutStyle,
textAlign: widget.textAlign,
textDirection: widget.textDirection,
Expand Down
77 changes: 77 additions & 0 deletions test/min_letter_spacing_test.dart
Original file line number Diff line number Diff line change
@@ -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,
),
),
);
});
}
11 changes: 11 additions & 0 deletions test/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import 'package:flutter_test/flutter_test.dart';
double effectiveFontSize(Text text) =>
(text.textScaleFactor ?? 1) * text.style!.fontSize!;

double? effectiveLetterSpacing(Text text) => text.style!.letterSpacing;

bool doesTextFit(
Text text, [
double maxWidth = double.infinity,
Expand Down Expand Up @@ -85,6 +87,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));

Expand Down