You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
samples/web/dad_jokes/lib/auto_size_text.dart

405 lines
12 KiB

// Package auto_size_text:
// https://pub.dartlang.org/packages/auto_size_text
import 'package:flutter/widgets.dart';
bool checkTextFits(TextSpan text, Locale locale, double scale, int maxLines,
double maxWidth, double maxHeight) {
final tp = TextPainter(
text: text,
textAlign: TextAlign.left,
textDirection: TextDirection.ltr,
textScaleFactor: scale ?? 1,
maxLines: maxLines,
locale: locale,
)..layout(maxWidth: maxWidth);
return !(tp.didExceedMaxLines ||
tp.height > maxHeight ||
tp.width > maxWidth);
}
/// Flutter widget that automatically resizes text to fit perfectly within its bounds.
///
/// All size constraints as well as maxLines are taken into account. If the text
/// overflows anyway, you should check if the parent widget actually constraints
/// the size of this widget.
class AutoSizeText extends StatefulWidget {
/// Creates a [AutoSizeText] widget.
///
/// If the [style] argument is null, the text will use the style from the
/// closest enclosing [DefaultTextStyle].
const AutoSizeText(
this.data, {
Key key,
this.style,
this.minFontSize = 12.0,
this.maxFontSize,
this.stepGranularity = 1.0,
this.presetFontSizes,
this.group,
this.textAlign,
this.textDirection,
this.locale,
this.softWrap,
this.overflow,
this.textScaleFactor,
this.maxLines,
this.semanticsLabel,
}) : assert(data != null),
assert(stepGranularity >= 0.1),
textSpan = null,
super(key: key);
/// Creates a [AutoSizeText] widget with a [TextSpan].
const AutoSizeText.rich(
this.textSpan, {
Key key,
this.style,
this.minFontSize = 12.0,
this.maxFontSize,
this.stepGranularity = 1.0,
this.presetFontSizes,
this.group,
this.textAlign,
this.textDirection,
this.locale,
this.softWrap,
this.overflow,
this.textScaleFactor,
this.maxLines,
this.semanticsLabel,
}) : assert(textSpan != null),
assert(stepGranularity >= 0.1),
data = null,
super(key: key);
/// The text to display.
///
/// This will be null if a [textSpan] is provided instead.
final String data;
/// The text to display as a [TextSpan].
///
/// This will be null if [data] is provided instead.
final TextSpan textSpan;
/// If non-null, the style to use for this text.
///
/// If the style's "inherit" property is true, the style will be merged with
/// the closest enclosing [DefaultTextStyle]. Otherwise, the style will
/// replace the closest enclosing [DefaultTextStyle].
final TextStyle style;
/// The minimum text size constraint to be used when auto-sizing text.
///
/// Is being ignored if [presetFontSizes] is set.
final double minFontSize;
/// The maximum text size constraint to be used when auto-sizing text.
///
/// Is being ignored if [presetFontSizes] is set.
final double maxFontSize;
/// The steps in which the font size is being adapted to constraints.
///
/// The Text scales uniformly in a range between [minFontSize] and
/// [maxFontSize].
/// Each increment occurs as per the step size set in stepGranularity.
///
/// Most of the time you don't want a stepGranularity below 1.0.
///
/// Is being ignored if [presetFontSizes] is set.
final double stepGranularity;
/// Lets you specify all the possible font sizes.
///
/// **Important:** The presetFontSizes are used the order they are given in.
/// If the first fontSize matches, all others are being ignored.
final List<double> presetFontSizes;
/// Synchronizes the size of multiple [AutoSizeText]s.
///
/// 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;
/// How the text should be aligned horizontally.
final TextAlign textAlign;
/// The directionality of the text.
///
/// This decides how [textAlign] values like [TextAlign.start] and
/// [TextAlign.end] are interpreted.
///
/// This is also used to disambiguate how to render bidirectional text. For
/// example, if the [data] is an English phrase followed by a Hebrew phrase,
/// in a [TextDirection.ltr] context the English phrase will be on the left
/// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
/// context, the English phrase will be on the right and the Hebrew phrase on
/// its left.
///
/// Defaults to the ambient [Directionality], if any.
final TextDirection textDirection;
/// Used to select a font when the same Unicode character can
/// be rendered differently, depending on the locale.
///
/// It's rarely necessary to set this property. By default its value
/// is inherited from the enclosing app with `Localizations.localeOf(context)`.
final Locale locale;
/// Whether the text should break at soft line breaks.
///
/// If false, the glyphs in the text will be positioned as if there was
/// unlimited horizontal space.
final bool softWrap;
/// How visual overflow should be handled.
final TextOverflow overflow;
/// The number of font pixels for each logical pixel.
///
/// For example, if the text scale factor is 1.5, text will be 50% larger than
/// the specified font size.
///
/// 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
/// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope.
final double textScaleFactor;
/// An optional maximum number of lines for the text to span, wrapping if necessary.
/// If the text exceeds the given number of lines, it will be resized according
/// to the specified bounds and if necessary truncated according to [overflow].
///
/// If this is 1, text will not wrap. Otherwise, text will be wrapped at the
/// edge of the box.
///
/// If this is null, but there is an ambient [DefaultTextStyle] that specifies
/// an explicit number for its [DefaultTextStyle.maxLines], then the
/// [DefaultTextStyle] value will take precedence. You can use a [RichText]
/// widget directly to entirely override the [DefaultTextStyle].
final int maxLines;
/// An alternative semantics label for this text.
///
/// If present, the semantics of this widget will contain this value instead
/// of the actual text.
final String semanticsLabel;
@override
_AutoSizeTextState createState() => _AutoSizeTextState();
}
class _AutoSizeTextState extends State<AutoSizeText> {
double _previousFontSize;
Text _cachedText;
double _cachedFontSize;
@override
void initState() {
super.initState();
if (widget.group != null) {
widget.group._register(this);
}
}
@override
void didUpdateWidget(AutoSizeText oldWidget) {
_cachedText = null;
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, size) {
final defaultTextStyle = DefaultTextStyle.of(context);
var style = widget.style;
if (widget.style == null || widget.style.inherit) {
style = defaultTextStyle.style.merge(widget.style);
}
final fontSize = _calculateFontSize(size, style, defaultTextStyle);
Widget text;
if (widget.group != null) {
if (fontSize != _previousFontSize) {
widget.group._updateFontSize(this, fontSize);
}
text = _buildText(widget.group._fontSize, style);
} else {
text = _buildText(fontSize, style);
}
_previousFontSize = fontSize;
return text;
});
}
double _calculateFontSize(
BoxConstraints size, TextStyle style, DefaultTextStyle defaultStyle) {
final userScale =
widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context);
final minFontSize = widget.minFontSize ?? 0;
assert(
minFontSize >= 0, 'MinFontSize has to be greater than or equal to 0.');
final maxFontSize = widget.maxFontSize ?? double.infinity;
assert(maxFontSize > 0, 'MaxFontSize has to be greater than 0.');
assert(minFontSize <= maxFontSize,
'MinFontSize has to be smaller or equal than maxFontSize.');
final maxLines = widget.maxLines ?? defaultStyle.maxLines;
var presetIndex = 0;
if (widget.presetFontSizes != null) {
assert(widget.presetFontSizes.isNotEmpty, 'PresetFontSizes is empty.');
}
double initialFontSize;
if (widget.presetFontSizes == null) {
final current = style.fontSize;
initialFontSize = current.clamp(minFontSize, maxFontSize).toDouble();
} else {
initialFontSize = widget.presetFontSizes[presetIndex++];
}
var fontSize = initialFontSize * userScale;
final span = TextSpan(
style: widget.textSpan?.style ?? style,
text: widget.textSpan?.text ?? widget.data,
children: widget.textSpan?.children,
recognizer: widget.textSpan?.recognizer,
);
while (!checkTextFits(span, widget.locale, fontSize / style.fontSize,
maxLines, size.maxWidth, size.maxHeight)) {
if (widget.presetFontSizes == null) {
final newFontSize = fontSize - widget.stepGranularity;
if (newFontSize < (minFontSize * userScale)) break;
fontSize = newFontSize;
} else if (presetIndex < widget.presetFontSizes.length) {
fontSize = widget.presetFontSizes[presetIndex++] * userScale;
} else {
break;
}
}
return fontSize;
}
Widget _buildText(double fontSize, TextStyle style) {
if (_cachedText != null && _cachedFontSize == fontSize) {
return _cachedText;
}
Text text;
if (widget.data != null) {
text = Text(
widget.data,
style: style.copyWith(fontSize: fontSize),
textAlign: widget.textAlign,
textDirection: widget.textDirection,
locale: widget.locale,
softWrap: widget.softWrap,
overflow: widget.overflow,
textScaleFactor: 1,
maxLines: widget.maxLines,
semanticsLabel: widget.semanticsLabel,
);
} else {
text = Text.rich(
widget.textSpan,
style: style,
textAlign: widget.textAlign,
textDirection: widget.textDirection,
locale: widget.locale,
softWrap: widget.softWrap,
overflow: widget.overflow,
textScaleFactor: fontSize / style.fontSize,
maxLines: widget.maxLines,
semanticsLabel: widget.semanticsLabel,
);
}
_cachedText = text;
_cachedFontSize = fontSize;
return text;
}
void _notifySync() {
setState(() {});
}
@override
void dispose() {
if (widget.group != null) {
widget.group._remove(this);
}
super.dispose();
}
}
class AutoSizeGroup {
final _listeners = <_AutoSizeTextState, double>{};
var _widgetsNotified = false;
double _fontSize = double.infinity;
void _register(_AutoSizeTextState text) {
_listeners[text] = double.infinity;
}
void _updateFontSize(_AutoSizeTextState text, double maxFontSize) {
final oldFontSize = _fontSize;
if (maxFontSize <= _fontSize) {
_fontSize = maxFontSize;
_listeners[text] = maxFontSize;
} else if (_listeners[text] == _fontSize) {
_listeners[text] = maxFontSize;
_fontSize = double.infinity;
for (var size in _listeners.values) {
if (size < _fontSize) _fontSize = size;
}
} else {
_listeners[text] = maxFontSize;
}
if (oldFontSize != _fontSize) {
_widgetsNotified = false;
// Timer.run(_notifyListeners);
_notifyListeners();
}
}
void _notifyListeners() {
if (_widgetsNotified) {
return;
} else {
_widgetsNotified = true;
}
for (var text in _listeners.keys.toList()) {
if (text.mounted) {
text._notifySync();
} else {
_listeners.remove(text);
}
}
}
void _remove(_AutoSizeTextState text) {
_updateFontSize(text, double.infinity);
_listeners.remove(text);
}
}