1
part of auto_size_text;
2

3
/// Flutter widget that automatically resizes text to fit perfectly within its
4
/// bounds.
5
///
6
/// All size constraints as well as maxLines are taken into account. If the text
7
/// overflows anyway, you should check if the parent widget actually constraints
8
/// the size of this widget.
9
class AutoSizeText extends StatefulWidget {
10
  /// Creates a [AutoSizeText] widget.
11
  ///
12
  /// If the [style] argument is null, the text will use the style from the
13
  /// closest enclosing [DefaultTextStyle].
14 1
  const AutoSizeText(
15
    this.data, {
16
    Key key,
17
    this.textKey,
18
    this.style,
19
    this.strutStyle,
20
    this.minFontSize = 12,
21
    this.maxFontSize = double.infinity,
22
    this.stepGranularity = 1,
23
    this.presetFontSizes,
24
    this.group,
25
    this.textAlign,
26
    this.textDirection,
27
    this.locale,
28
    this.softWrap,
29
    this.wrapWords = true,
30
    this.overflow,
31
    this.overflowReplacement,
32
    this.textScaleFactor,
33
    this.maxLines,
34
    this.semanticsLabel,
35 1
  })  : assert(data != null,
36
            'A non-null String must be provided to a AutoSizeText widget.'),
37
        textSpan = null,
38 1
        super(key: key);
39

40
  /// Creates a [AutoSizeText] widget with a [TextSpan].
41 1
  const AutoSizeText.rich(
42
    this.textSpan, {
43
    Key key,
44
    this.textKey,
45
    this.style,
46
    this.strutStyle,
47
    this.minFontSize = 12,
48
    this.maxFontSize = double.infinity,
49
    this.stepGranularity = 1,
50
    this.presetFontSizes,
51
    this.group,
52
    this.textAlign,
53
    this.textDirection,
54
    this.locale,
55
    this.softWrap,
56
    this.wrapWords = true,
57
    this.overflow,
58
    this.overflowReplacement,
59
    this.textScaleFactor,
60
    this.maxLines,
61
    this.semanticsLabel,
62 1
  })  : assert(textSpan != null,
63
            'A non-null TextSpan must be provided to a AutoSizeText.rich widget.'),
64
        data = null,
65 1
        super(key: key);
66

67
  /// Sets the key for the resulting [Text] widget.
68
  ///
69
  /// This allows you to find the actual `Text` widget built by `AutoSizeText`.
70
  final Key textKey;
71

72
  /// The text to display.
73
  ///
74
  /// This will be null if a [textSpan] is provided instead.
75
  final String data;
76

77
  /// The text to display as a [TextSpan].
78
  ///
79
  /// This will be null if [data] is provided instead.
80
  final TextSpan textSpan;
81

82
  /// If non-null, the style to use for this text.
83
  ///
84
  /// If the style's "inherit" property is true, the style will be merged with
85
  /// the closest enclosing [DefaultTextStyle]. Otherwise, the style will
86
  /// replace the closest enclosing [DefaultTextStyle].
87
  final TextStyle style;
88

89
  // The default font size if none is specified.
90
  static const double _defaultFontSize = 14;
91

92
  /// The strut style to use. Strut style defines the strut, which sets minimum
93
  /// vertical layout metrics.
94
  ///
95
  /// Omitting or providing null will disable strut.
96
  ///
97
  /// Omitting or providing null for any properties of [StrutStyle] will result
98
  /// in default values being used. It is highly recommended to at least specify
99
  /// a font size.
100
  ///
101
  /// See [StrutStyle] for details.
102
  final StrutStyle strutStyle;
103

104
  /// The minimum text size constraint to be used when auto-sizing text.
105
  ///
106
  /// Is being ignored if [presetFontSizes] is set.
107
  final double minFontSize;
108

109
  /// The maximum text size constraint to be used when auto-sizing text.
110
  ///
111
  /// Is being ignored if [presetFontSizes] is set.
112
  final double maxFontSize;
113

114
  /// The step size in which the font size is being adapted to constraints.
115
  ///
116
  /// The Text scales uniformly in a range between [minFontSize] and
117
  /// [maxFontSize].
118
  /// Each increment occurs as per the step size set in stepGranularity.
119
  ///
120
  /// Most of the time you don't want a stepGranularity below 1.0.
121
  ///
122
  /// Is being ignored if [presetFontSizes] is set.
123
  final double stepGranularity;
124

125
  /// Predefines all the possible font sizes.
126
  ///
127
  /// **Important:** PresetFontSizes have to be in descending order.
128
  final List<double> presetFontSizes;
129

130
  /// Synchronizes the size of multiple [AutoSizeText]s.
131
  ///
132
  /// If you want multiple [AutoSizeText]s to have the same text size, give all
133
  /// of them the same [AutoSizeGroup] instance. All of them will have the
134
  /// size of the smallest [AutoSizeText]
135
  final AutoSizeGroup group;
136

137
  /// How the text should be aligned horizontally.
138
  final TextAlign textAlign;
139

140
  /// The directionality of the text.
141
  ///
142
  /// This decides how [textAlign] values like [TextAlign.start] and
143
  /// [TextAlign.end] are interpreted.
144
  ///
145
  /// This is also used to disambiguate how to render bidirectional text. For
146
  /// example, if the [data] is an English phrase followed by a Hebrew phrase,
147
  /// in a [TextDirection.ltr] context the English phrase will be on the left
148
  /// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
149
  /// context, the English phrase will be on the right and the Hebrew phrase on
150
  /// its left.
151
  ///
152
  /// Defaults to the ambient [Directionality], if any.
153
  final TextDirection textDirection;
154

155
  /// Used to select a font when the same Unicode character can
156
  /// be rendered differently, depending on the locale.
157
  ///
158
  /// It's rarely necessary to set this property. By default its value
159
  /// is inherited from the enclosing app with `Localizations.localeOf(context)`.
160
  final Locale locale;
161

162
  /// Whether the text should break at soft line breaks.
163
  ///
164
  /// If false, the glyphs in the text will be positioned as if there was
165
  /// unlimited horizontal space.
166
  final bool softWrap;
167

168
  /// Whether words which don't fit in one line should be wrapped.
169
  ///
170
  /// If false, the fontSize is lowered as far as possible until all words fit
171
  /// into a single line.
172
  final bool wrapWords;
173

174
  /// How visual overflow should be handled.
175
  ///
176
  /// Defaults to retrieving the value from the nearest [DefaultTextStyle] ancestor.
177
  final TextOverflow overflow;
178

179
  /// If the text is overflowing and does not fit its bounds, this widget is
180
  /// displayed instead.
181
  final Widget overflowReplacement;
182

183
  /// The number of font pixels for each logical pixel.
184
  ///
185
  /// For example, if the text scale factor is 1.5, text will be 50% larger than
186
  /// the specified font size.
187
  ///
188
  /// This property also affects [minFontSize], [maxFontSize] and [presetFontSizes].
189
  ///
190
  /// The value given to the constructor as textScaleFactor. If null, will
191
  /// use the [MediaQueryData.textScaleFactor] obtained from the ambient
192
  /// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope.
193
  final double textScaleFactor;
194

195
  /// An optional maximum number of lines for the text to span, wrapping if necessary.
196
  /// If the text exceeds the given number of lines, it will be resized according
197
  /// to the specified bounds and if necessary truncated according to [overflow].
198
  ///
199
  /// If this is 1, text will not wrap. Otherwise, text will be wrapped at the
200
  /// edge of the box.
201
  ///
202
  /// If this is null, but there is an ambient [DefaultTextStyle] that specifies
203
  /// an explicit number for its [DefaultTextStyle.maxLines], then the
204
  /// [DefaultTextStyle] value will take precedence. You can use a [RichText]
205
  /// widget directly to entirely override the [DefaultTextStyle].
206
  final int maxLines;
207

208
  /// An alternative semantics label for this text.
209
  ///
210
  /// If present, the semantics of this widget will contain this value instead
211
  /// of the actual text. This will overwrite any of the semantics labels applied
212
  /// directly to the [TextSpan]s.
213
  ///
214
  /// This is useful for replacing abbreviations or shorthands with the full
215
  /// text value:
216
  ///
217
  /// ```dart
218
  /// AutoSizeText(r'$$', semanticsLabel: 'Double dollars')
219
  /// ```
220
  final String semanticsLabel;
221

222 1
  @override
223 1
  _AutoSizeTextState createState() => _AutoSizeTextState();
224
}
225

226
class _AutoSizeTextState extends State<AutoSizeText> {
227 1
  @override
228
  void initState() {
229 1
    super.initState();
230

231 1
    if (widget.group != null) {
232 1
      widget.group._register(this);
233
    }
234
  }
235

236 1
  @override
237
  void didUpdateWidget(AutoSizeText oldWidget) {
238 1
    super.didUpdateWidget(oldWidget);
239

240 1
    if (oldWidget.group != widget.group) {
241 0
      oldWidget.group?._remove(this);
242 0
      widget.group?._register(this);
243
    }
244
  }
245

246 1
  @override
247
  Widget build(BuildContext context) {
248 1
    return LayoutBuilder(builder: (context, size) {
249 1
      final defaultTextStyle = DefaultTextStyle.of(context);
250

251 1
      var style = widget.style;
252 1
      if (widget.style == null || widget.style.inherit) {
253 1
        style = defaultTextStyle.style.merge(widget.style);
254
      }
255 1
      if (style.fontSize == null) {
256 1
        style = style.copyWith(fontSize: AutoSizeText._defaultFontSize);
257
      }
258

259 1
      final maxLines = widget.maxLines ?? defaultTextStyle.maxLines;
260

261 1
      _validateProperties(style, maxLines);
262

263 1
      final result = _calculateFontSize(size, style, maxLines);
264 1
      final fontSize = result[0] as double;
265 1
      final textFits = result[1] as bool;
266

267
      Widget text;
268

269 1
      if (widget.group != null) {
270 1
        widget.group._updateFontSize(this, fontSize);
271 1
        text = _buildText(widget.group._fontSize, style, maxLines);
272
      } else {
273 1
        text = _buildText(fontSize, style, maxLines);
274
      }
275

276 1
      if (widget.overflowReplacement != null && !textFits) {
277 1
        return widget.overflowReplacement;
278
      } else {
279
        return text;
280
      }
281
    });
282
  }
283

284 1
  void _validateProperties(TextStyle style, int maxLines) {
285 1
    assert(widget.overflow == null || widget.overflowReplacement == null,
286
        'Either overflow or overflowReplacement must be null.');
287 1
    assert(maxLines == null || maxLines > 0,
288
        'MaxLines must be greater than or equal to 1.');
289 1
    assert(widget.key == null || widget.key != widget.textKey,
290
        'Key and textKey must not be equal.');
291

292 1
    if (widget.presetFontSizes == null) {
293
      assert(
294 1
          widget.stepGranularity >= 0.1,
295
          'StepGranularity must be greater than or equal to 0.1. It is not a '
296
          'good idea to resize the font with a higher accuracy.');
297 1
      assert(widget.minFontSize >= 0,
298
          'MinFontSize must be greater than or equal to 0.');
299 1
      assert(widget.maxFontSize > 0, 'MaxFontSize has to be greater than 0.');
300 1
      assert(widget.minFontSize <= widget.maxFontSize,
301
          'MinFontSize must be smaller or equal than maxFontSize.');
302 1
      assert(widget.minFontSize / widget.stepGranularity % 1 == 0,
303
          'MinFontSize must be a multiple of stepGranularity.');
304 1
      if (widget.maxFontSize != double.infinity) {
305 1
        assert(widget.maxFontSize / widget.stepGranularity % 1 == 0,
306
            'MaxFontSize must be a multiple of stepGranularity.');
307
      }
308
    } else {
309 1
      assert(widget.presetFontSizes.isNotEmpty,
310
          'PresetFontSizes must not be empty.');
311
    }
312
  }
313

314 1
  List _calculateFontSize(BoxConstraints size, TextStyle style, int maxLines) {
315 1
    final span = TextSpan(
316 1
      style: widget.textSpan?.style ?? style,
317 1
      text: widget.textSpan?.text ?? widget.data,
318 1
      children: widget.textSpan?.children,
319 1
      recognizer: widget.textSpan?.recognizer,
320
    );
321

322
    final userScale =
323 1
        widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context);
324

325
    int left;
326
    int right;
327

328 1
    final presetFontSizes = widget.presetFontSizes?.reversed?.toList();
329
    if (presetFontSizes == null) {
330
      final defaultFontSize =
331 1
          style.fontSize.clamp(widget.minFontSize, widget.maxFontSize);
332 1
      final defaultScale = defaultFontSize * userScale / style.fontSize;
333 1
      if (_checkTextFits(span, defaultScale, maxLines, size)) {
334 1
        return <Object>[defaultFontSize * userScale, true];
335
      }
336

337 1
      left = (widget.minFontSize / widget.stepGranularity).floor();
338 1
      right = (defaultFontSize / widget.stepGranularity).ceil();
339
    } else {
340
      left = 0;
341 1
      right = presetFontSizes.length - 1;
342
    }
343

344
    var lastValueFits = false;
345 1
    while (left <= right) {
346 1
      final mid = (left + (right - left) / 2).floor();
347
      double scale;
348
      if (presetFontSizes == null) {
349 1
        scale = mid * userScale * widget.stepGranularity / style.fontSize;
350
      } else {
351 1
        scale = presetFontSizes[mid] * userScale / style.fontSize;
352
      }
353 1
      if (_checkTextFits(span, scale, maxLines, size)) {
354 1
        left = mid + 1;
355
        lastValueFits = true;
356
      } else {
357 1
        right = mid - 1;
358
      }
359
    }
360

361
    if (!lastValueFits) {
362 1
      right += 1;
363
    }
364

365
    double fontSize;
366
    if (presetFontSizes == null) {
367 1
      fontSize = right * userScale * widget.stepGranularity;
368
    } else {
369 1
      fontSize = presetFontSizes[right] * userScale;
370
    }
371

372 1
    return <Object>[fontSize, lastValueFits];
373
  }
374

375 1
  bool _checkTextFits(
376
      TextSpan text, double scale, int maxLines, BoxConstraints constraints) {
377 1
    if (!widget.wrapWords) {
378 1
      final words = text.toPlainText().split(RegExp('\\s+'));
379

380 1
      final wordWrapTextPainter = TextPainter(
381 1
        text: TextSpan(
382 1
          style: text.style,
383 1
          text: words.join('\n'),
384
        ),
385 1
        textAlign: widget.textAlign ?? TextAlign.left,
386 1
        textDirection: widget.textDirection ?? TextDirection.ltr,
387
        textScaleFactor: scale ?? 1,
388 1
        maxLines: words.length,
389 1
        locale: widget.locale,
390 1
        strutStyle: widget.strutStyle,
391
      );
392

393 1
      wordWrapTextPainter.layout(maxWidth: constraints.maxWidth);
394

395 1
      if (wordWrapTextPainter.didExceedMaxLines ||
396 1
          wordWrapTextPainter.width > constraints.maxWidth) {
397
        return false;
398
      }
399
    }
400

401 1
    final textPainter = TextPainter(
402
      text: text,
403 1
      textAlign: widget.textAlign ?? TextAlign.left,
404 1
      textDirection: widget.textDirection ?? TextDirection.ltr,
405
      textScaleFactor: scale ?? 1,
406
      maxLines: maxLines,
407 1
      locale: widget.locale,
408 1
      strutStyle: widget.strutStyle,
409
    );
410

411 1
    textPainter.layout(maxWidth: constraints.maxWidth);
412

413 1
    return !(textPainter.didExceedMaxLines ||
414 1
        textPainter.height > constraints.maxHeight ||
415 1
        textPainter.width > constraints.maxWidth);
416
  }
417

418 1
  Widget _buildText(double fontSize, TextStyle style, int maxLines) {
419 1
    if (widget.data != null) {
420 1
      return Text(
421 1
        widget.data,
422 1
        key: widget.textKey,
423 1
        style: style.copyWith(fontSize: fontSize),
424 1
        strutStyle: widget.strutStyle,
425 1
        textAlign: widget.textAlign,
426 1
        textDirection: widget.textDirection,
427 1
        locale: widget.locale,
428 1
        softWrap: widget.softWrap,
429 1
        overflow: widget.overflow,
430
        textScaleFactor: 1,
431
        maxLines: maxLines,
432 1
        semanticsLabel: widget.semanticsLabel,
433
      );
434
    } else {
435 1
      return Text.rich(
436 1
        widget.textSpan,
437 1
        key: widget.textKey,
438
        style: style,
439 1
        strutStyle: widget.strutStyle,
440 1
        textAlign: widget.textAlign,
441 1
        textDirection: widget.textDirection,
442 1
        locale: widget.locale,
443 1
        softWrap: widget.softWrap,
444 1
        overflow: widget.overflow,
445 1
        textScaleFactor: fontSize / style.fontSize,
446
        maxLines: maxLines,
447 1
        semanticsLabel: widget.semanticsLabel,
448
      );
449
    }
450
  }
451

452 1
  void _notifySync() {
453 1
    setState(() {});
454
  }
455

456 1
  @override
457
  void dispose() {
458 1
    if (widget.group != null) {
459 1
      widget.group._remove(this);
460
    }
461 1
    super.dispose();
462
  }
463
}

Read our documentation on viewing source code .

Loading