1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-11 23:39:14 +00:00

Add flutter_web samples (#75)

This commit is contained in:
Kevin Moore
2019-05-07 13:32:08 -07:00
committed by Andrew Brogdon
parent 42f2dce01b
commit 3fe927cb29
697 changed files with 241026 additions and 0 deletions

View File

@@ -0,0 +1,404 @@
// Package auto_size_text:
// https://pub.dartlang.org/packages/auto_size_text
import 'package:flutter_web/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);
}
}

View File

@@ -0,0 +1,17 @@
import 'package:flutter_web/material.dart';
import 'main_page.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Dad Jokes',
theme: ThemeData(
primarySwatch: Colors.deepOrange,
),
home: MainPage(title: 'Dad Jokes'),
);
}
}

View File

@@ -0,0 +1,136 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter_web/material.dart';
import 'package:http/http.dart' as http;
import 'auto_size_text.dart';
const _dadJokeApi = 'https://icanhazdadjoke.com/';
const _httpHeaders = {
'Accept': 'application/json',
};
const jokeTextStyle = TextStyle(
fontFamily: 'Patrick Hand',
fontSize: 36,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.normal);
class MainPage extends StatefulWidget {
MainPage({Key key, this.title}) : super(key: key);
final String title;
@override
MainPageState createState() => MainPageState();
}
class MainPageState extends State<MainPage> {
Future<String> _response;
String _displayedJoke = '';
@override
void initState() {
super.initState();
_refreshAction();
}
void _refreshAction() {
setState(() {
_response = http.read(_dadJokeApi, headers: _httpHeaders);
});
}
void _aboutAction() {
showDialog(
context: context,
builder: (BuildContext context) {
return const AlertDialog(
title: Text('About Dad Jokes'),
content: Text(
'Dad jokes is brought to you by Tim Sneath (@timsneath), '
'proud dad of Naomi, Esther, and Silas. May your children '
'groan like mine will.\n\nDad jokes come from '
'https://icanhazdadjoke.com with thanks.'));
});
}
FutureBuilder<String> _jokeBody() {
return FutureBuilder<String>(
future: _response,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return const ListTile(
leading: Icon(Icons.sync_problem),
title: Text('No connection'),
);
case ConnectionState.waiting:
return const Center(child: CircularProgressIndicator());
default:
if (snapshot.hasError) {
return const Center(
child: ListTile(
leading: Icon(Icons.error),
title: Text('Network error'),
subtitle: Text(
'Sorry - this isn\'t funny, we know, but something went '
'wrong when connecting to the Internet. Check your '
'network connection and try again.'),
),
);
} else {
final decoded = json.decode(snapshot.data);
if (decoded['status'] == 200) {
_displayedJoke = decoded['joke'] as String;
return Padding(
padding: const EdgeInsets.all(16),
child: Dismissible(
key: const Key('joke'),
direction: DismissDirection.horizontal,
onDismissed: (direction) {
_refreshAction();
},
child: AutoSizeText(_displayedJoke, style: jokeTextStyle),
));
} else {
return ListTile(
leading: const Icon(Icons.sync_problem),
title: Text('Unexpected error: ${snapshot.data}'),
);
}
}
}
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: Image.asset(
'icon.png',
fit: BoxFit.scaleDown,
),
title: Text(widget.title),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.info),
tooltip: 'About Dad Jokes',
onPressed: _aboutAction,
),
],
),
body: Center(
child: SafeArea(child: _jokeBody()),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _refreshAction,
icon: const Icon(Icons.mood),
label: const Text('NEW JOKE'),
),
);
}
}