1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-11 15:28:44 +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,411 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter_web/material.dart';
// This demo displays one Category at a time. The backdrop show a list
// of all of the categories and the selected category is displayed
// (CategoryView) on top of the backdrop.
class Category {
const Category({this.title, this.assets});
final String title;
final List<String> assets;
@override
String toString() => '$runtimeType("$title")';
}
const List<Category> allCategories = <Category>[
Category(
title: 'Accessories',
assets: <String>[
'products/belt.png',
'products/earrings.png',
'products/backpack.png',
'products/hat.png',
'products/scarf.png',
'products/sunnies.png',
],
),
Category(
title: 'Blue',
assets: <String>[
'products/backpack.png',
'products/cup.png',
'products/napkins.png',
'products/top.png',
],
),
Category(
title: 'Cold Weather',
assets: <String>[
'products/jacket.png',
'products/jumper.png',
'products/scarf.png',
'products/sweater.png',
'products/sweats.png',
],
),
Category(
title: 'Home',
assets: <String>[
'products/cup.png',
'products/napkins.png',
'products/planters.png',
'products/table.png',
'products/teaset.png',
],
),
Category(
title: 'Tops',
assets: <String>[
'products/jumper.png',
'products/shirt.png',
'products/sweater.png',
'products/top.png',
],
),
Category(
title: 'Everything',
assets: <String>[
'products/backpack.png',
'products/belt.png',
'products/cup.png',
'products/dress.png',
'products/earrings.png',
'products/flatwear.png',
'products/hat.png',
'products/jacket.png',
'products/jumper.png',
'products/napkins.png',
'products/planters.png',
'products/scarf.png',
'products/shirt.png',
'products/sunnies.png',
'products/sweater.png',
'products/sweats.png',
'products/table.png',
'products/teaset.png',
'products/top.png',
],
),
];
class CategoryView extends StatelessWidget {
const CategoryView({Key key, this.category}) : super(key: key);
final Category category;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return ListView(
key: PageStorageKey<Category>(category),
padding: const EdgeInsets.symmetric(
vertical: 16.0,
horizontal: 64.0,
),
children: category.assets.map<Widget>((String asset) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Card(
child: Container(
width: 144.0,
alignment: Alignment.center,
child: Column(
children: <Widget>[
Image.asset(
'$asset',
fit: BoxFit.contain,
),
Container(
padding: const EdgeInsets.only(bottom: 16.0),
alignment: AlignmentDirectional.center,
child: Text(
asset,
style: theme.textTheme.caption,
),
),
],
),
),
),
const SizedBox(height: 24.0),
],
);
}).toList(),
);
}
}
// One BackdropPanel is visible at a time. It's stacked on top of the
// the BackdropDemo.
class BackdropPanel extends StatelessWidget {
const BackdropPanel({
Key key,
this.onTap,
this.onVerticalDragUpdate,
this.onVerticalDragEnd,
this.title,
this.child,
}) : super(key: key);
final VoidCallback onTap;
final GestureDragUpdateCallback onVerticalDragUpdate;
final GestureDragEndCallback onVerticalDragEnd;
final Widget title;
final Widget child;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return Material(
elevation: 2.0,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
GestureDetector(
behavior: HitTestBehavior.opaque,
onVerticalDragUpdate: onVerticalDragUpdate,
onVerticalDragEnd: onVerticalDragEnd,
onTap: onTap,
child: Container(
height: 48.0,
padding: const EdgeInsetsDirectional.only(start: 16.0),
alignment: AlignmentDirectional.centerStart,
child: DefaultTextStyle(
style: theme.textTheme.subhead,
child: Tooltip(
message: 'Tap to dismiss',
child: title,
),
),
),
),
const Divider(height: 1.0),
Expanded(child: child),
],
),
);
}
}
// Cross fades between 'Select a Category' and 'Asset Viewer'.
class BackdropTitle extends AnimatedWidget {
const BackdropTitle({
Key key,
Listenable listenable,
}) : super(key: key, listenable: listenable);
@override
Widget build(BuildContext context) {
final Animation<double> animation = listenable;
return DefaultTextStyle(
style: Theme.of(context).primaryTextTheme.title,
softWrap: false,
overflow: TextOverflow.ellipsis,
child: Stack(
children: <Widget>[
Opacity(
opacity: CurvedAnimation(
parent: ReverseAnimation(animation),
curve: const Interval(0.5, 1.0),
).value,
child: const Text('Select a Category'),
),
Opacity(
opacity: CurvedAnimation(
parent: animation,
curve: const Interval(0.5, 1.0),
).value,
child: const Text('Asset Viewer'),
),
],
),
);
}
}
// This widget is essentially the backdrop itself.
class BackdropDemo extends StatefulWidget {
static const String routeName = '/material/backdrop';
@override
_BackdropDemoState createState() => _BackdropDemoState();
}
class _BackdropDemoState extends State<BackdropDemo>
with SingleTickerProviderStateMixin {
final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop');
AnimationController _controller;
Category _category = allCategories[0];
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
value: 1.0,
vsync: this,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _changeCategory(Category category) {
setState(() {
_category = category;
_controller.fling(velocity: 2.0);
});
}
bool get _backdropPanelVisible {
final AnimationStatus status = _controller.status;
return status == AnimationStatus.completed ||
status == AnimationStatus.forward;
}
void _toggleBackdropPanelVisibility() {
_controller.fling(velocity: _backdropPanelVisible ? -2.0 : 2.0);
}
double get _backdropHeight {
final RenderBox renderBox = _backdropKey.currentContext.findRenderObject();
return renderBox.size.height;
}
// By design: the panel can only be opened with a swipe. To close the panel
// the user must either tap its heading or the backdrop's menu icon.
void _handleDragUpdate(DragUpdateDetails details) {
if (_controller.isAnimating ||
_controller.status == AnimationStatus.completed) return;
_controller.value -=
details.primaryDelta / (_backdropHeight ?? details.primaryDelta);
}
void _handleDragEnd(DragEndDetails details) {
if (_controller.isAnimating ||
_controller.status == AnimationStatus.completed) return;
final double flingVelocity =
details.velocity.pixelsPerSecond.dy / _backdropHeight;
if (flingVelocity < 0.0)
_controller.fling(velocity: math.max(2.0, -flingVelocity));
else if (flingVelocity > 0.0)
_controller.fling(velocity: math.min(-2.0, -flingVelocity));
else
_controller.fling(velocity: _controller.value < 0.5 ? -2.0 : 2.0);
}
// Stacks a BackdropPanel, which displays the selected category, on top
// of the backdrop. The categories are displayed with ListTiles. Just one
// can be selected at a time. This is a LayoutWidgetBuild function because
// we need to know how big the BackdropPanel will be to set up its
// animation.
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
const double panelTitleHeight = 48.0;
final Size panelSize = constraints.biggest;
final double panelTop = panelSize.height - panelTitleHeight;
final Animation<RelativeRect> panelAnimation = _controller.drive(
RelativeRectTween(
begin: RelativeRect.fromLTRB(
0.0,
panelTop - MediaQuery.of(context).padding.bottom,
0.0,
panelTop - panelSize.height,
),
end: const RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
),
);
final ThemeData theme = Theme.of(context);
final List<Widget> backdropItems =
allCategories.map<Widget>((Category category) {
final bool selected = category == _category;
return Material(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(4.0)),
),
color: selected ? Colors.white.withOpacity(0.25) : Colors.transparent,
child: ListTile(
title: Text(category.title),
selected: selected,
onTap: () {
_changeCategory(category);
},
),
);
}).toList();
return Container(
key: _backdropKey,
color: theme.primaryColor,
child: Stack(
children: <Widget>[
ListTileTheme(
iconColor: theme.primaryIconTheme.color,
textColor: theme.primaryTextTheme.title.color.withOpacity(0.6),
selectedColor: theme.primaryTextTheme.title.color,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: backdropItems,
),
),
),
PositionedTransition(
rect: panelAnimation,
child: BackdropPanel(
onTap: _toggleBackdropPanelVisibility,
onVerticalDragUpdate: _handleDragUpdate,
onVerticalDragEnd: _handleDragEnd,
title: Text(_category.title),
child: CategoryView(category: _category),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 0.0,
title: BackdropTitle(
listenable: _controller.view,
),
actions: <Widget>[
IconButton(
onPressed: _toggleBackdropPanelVisibility,
icon: AnimatedIcon(
icon: AnimatedIcons.close_menu,
semanticLabel: 'close',
progress: _controller.view,
),
),
],
),
body: LayoutBuilder(
builder: _buildStack,
),
);
}
}

View File

@@ -0,0 +1,524 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
class BottomAppBarDemo extends StatefulWidget {
static const String routeName = '/material/bottom_app_bar';
@override
State createState() => _BottomAppBarDemoState();
}
// Flutter generally frowns upon abbrevation however this class uses two
// abbrevations extensively: "fab" for floating action button, and "bab"
// for bottom application bar.
class _BottomAppBarDemoState extends State<BottomAppBarDemo> {
static final GlobalKey<ScaffoldState> _scaffoldKey =
GlobalKey<ScaffoldState>();
// FAB shape
static const _ChoiceValue<Widget> kNoFab = _ChoiceValue<Widget>(
title: 'None',
label: 'do not show a floating action button',
value: null,
);
static const _ChoiceValue<Widget> kCircularFab = _ChoiceValue<Widget>(
title: 'Circular',
label: 'circular floating action button',
value: FloatingActionButton(
onPressed: _showSnackbar,
child: Icon(Icons.add, semanticLabel: 'Action'),
backgroundColor: Colors.orange,
),
);
static const _ChoiceValue<Widget> kDiamondFab = _ChoiceValue<Widget>(
title: 'Diamond',
label: 'diamond shape floating action button',
value: _DiamondFab(
onPressed: _showSnackbar,
child: Icon(Icons.add, semanticLabel: 'Action'),
),
);
// Notch
static const _ChoiceValue<bool> kShowNotchTrue = _ChoiceValue<bool>(
title: 'On',
label: 'show bottom appbar notch',
value: true,
);
static const _ChoiceValue<bool> kShowNotchFalse = _ChoiceValue<bool>(
title: 'Off',
label: 'do not show bottom appbar notch',
value: false,
);
// FAB Position
static const _ChoiceValue<FloatingActionButtonLocation> kFabEndDocked =
_ChoiceValue<FloatingActionButtonLocation>(
title: 'Attached - End',
label: 'floating action button is docked at the end of the bottom app bar',
value: FloatingActionButtonLocation.endDocked,
);
static const _ChoiceValue<FloatingActionButtonLocation> kFabCenterDocked =
_ChoiceValue<FloatingActionButtonLocation>(
title: 'Attached - Center',
label:
'floating action button is docked at the center of the bottom app bar',
value: FloatingActionButtonLocation.centerDocked,
);
static const _ChoiceValue<FloatingActionButtonLocation> kFabEndFloat =
_ChoiceValue<FloatingActionButtonLocation>(
title: 'Free - End',
label: 'floating action button floats above the end of the bottom app bar',
value: FloatingActionButtonLocation.endFloat,
);
static const _ChoiceValue<FloatingActionButtonLocation> kFabCenterFloat =
_ChoiceValue<FloatingActionButtonLocation>(
title: 'Free - Center',
label:
'floating action button is floats above the center of the bottom app bar',
value: FloatingActionButtonLocation.centerFloat,
);
static void _showSnackbar() {
const String text =
"When the Scaffold's floating action button location changes, "
'the floating action button animates to its new position.'
'The BottomAppBar adapts its shape appropriately.';
_scaffoldKey.currentState.showSnackBar(
const SnackBar(content: Text(text)),
);
}
// App bar color
static const List<_NamedColor> kBabColors = <_NamedColor>[
_NamedColor(null, 'Clear'),
_NamedColor(Color(0xFFFFC100), 'Orange'),
_NamedColor(Color(0xFF91FAFF), 'Light Blue'),
_NamedColor(Color(0xFF00D1FF), 'Cyan'),
_NamedColor(Color(0xFF00BCFF), 'Cerulean'),
_NamedColor(Color(0xFF009BEE), 'Blue'),
];
_ChoiceValue<Widget> _fabShape = kCircularFab;
_ChoiceValue<bool> _showNotch = kShowNotchTrue;
_ChoiceValue<FloatingActionButtonLocation> _fabLocation = kFabEndDocked;
Color _babColor = kBabColors.first.color;
void _onShowNotchChanged(_ChoiceValue<bool> value) {
setState(() {
_showNotch = value;
});
}
void _onFabShapeChanged(_ChoiceValue<Widget> value) {
setState(() {
_fabShape = value;
});
}
void _onFabLocationChanged(_ChoiceValue<FloatingActionButtonLocation> value) {
setState(() {
_fabLocation = value;
});
}
void _onBabColorChanged(Color value) {
setState(() {
_babColor = value;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: const Text('Bottom app bar'),
elevation: 0.0,
actions: <Widget>[
MaterialDemoDocumentationButton(BottomAppBarDemo.routeName),
IconButton(
icon: const Icon(Icons.sentiment_very_satisfied,
semanticLabel: 'Update shape'),
onPressed: () {
setState(() {
_fabShape =
_fabShape == kCircularFab ? kDiamondFab : kCircularFab;
});
},
),
],
),
body: ListView(
padding: const EdgeInsets.only(bottom: 88.0),
children: <Widget>[
const _Heading('FAB Shape'),
_RadioItem<Widget>(kCircularFab, _fabShape, _onFabShapeChanged),
_RadioItem<Widget>(kDiamondFab, _fabShape, _onFabShapeChanged),
_RadioItem<Widget>(kNoFab, _fabShape, _onFabShapeChanged),
const Divider(),
const _Heading('Notch'),
_RadioItem<bool>(kShowNotchTrue, _showNotch, _onShowNotchChanged),
_RadioItem<bool>(kShowNotchFalse, _showNotch, _onShowNotchChanged),
const Divider(),
const _Heading('FAB Position'),
_RadioItem<FloatingActionButtonLocation>(
kFabEndDocked, _fabLocation, _onFabLocationChanged),
_RadioItem<FloatingActionButtonLocation>(
kFabCenterDocked, _fabLocation, _onFabLocationChanged),
_RadioItem<FloatingActionButtonLocation>(
kFabEndFloat, _fabLocation, _onFabLocationChanged),
_RadioItem<FloatingActionButtonLocation>(
kFabCenterFloat, _fabLocation, _onFabLocationChanged),
const Divider(),
const _Heading('App bar color'),
_ColorsItem(kBabColors, _babColor, _onBabColorChanged),
],
),
floatingActionButton: _fabShape.value,
floatingActionButtonLocation: _fabLocation.value,
bottomNavigationBar: _DemoBottomAppBar(
color: _babColor,
fabLocation: _fabLocation.value,
shape: _selectNotch(),
),
);
}
NotchedShape _selectNotch() {
if (!_showNotch.value) return null;
if (_fabShape == kCircularFab) return const CircularNotchedRectangle();
if (_fabShape == kDiamondFab) return const _DiamondNotchedRectangle();
return null;
}
}
class _ChoiceValue<T> {
const _ChoiceValue({this.value, this.title, this.label});
final T value;
final String title;
final String label; // For the Semantics widget that contains title
@override
String toString() => '$runtimeType("$title")';
}
class _RadioItem<T> extends StatelessWidget {
const _RadioItem(this.value, this.groupValue, this.onChanged);
final _ChoiceValue<T> value;
final _ChoiceValue<T> groupValue;
final ValueChanged<_ChoiceValue<T>> onChanged;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return Container(
height: 56.0,
padding: const EdgeInsetsDirectional.only(start: 16.0),
alignment: AlignmentDirectional.centerStart,
child: MergeSemantics(
child: Row(children: <Widget>[
Radio<_ChoiceValue<T>>(
value: value,
groupValue: groupValue,
onChanged: onChanged,
),
Expanded(
child: Semantics(
container: true,
button: true,
label: value.label,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
onChanged(value);
},
child: Text(
value.title,
style: theme.textTheme.subhead,
),
),
),
),
]),
),
);
}
}
class _NamedColor {
const _NamedColor(this.color, this.name);
final Color color;
final String name;
}
class _ColorsItem extends StatelessWidget {
const _ColorsItem(this.colors, this.selectedColor, this.onChanged);
final List<_NamedColor> colors;
final Color selectedColor;
final ValueChanged<Color> onChanged;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: colors.map<Widget>((_NamedColor namedColor) {
return RawMaterialButton(
onPressed: () {
onChanged(namedColor.color);
},
constraints: const BoxConstraints.tightFor(
width: 32.0,
height: 32.0,
),
fillColor: namedColor.color,
shape: CircleBorder(
side: BorderSide(
color: namedColor.color == selectedColor
? Colors.black
: const Color(0xFFD5D7DA),
width: 2.0,
),
),
child: Semantics(
value: namedColor.name,
selected: namedColor.color == selectedColor,
),
);
}).toList(),
);
}
}
class _Heading extends StatelessWidget {
const _Heading(this.text);
final String text;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return Container(
height: 48.0,
padding: const EdgeInsetsDirectional.only(start: 56.0),
alignment: AlignmentDirectional.centerStart,
child: Text(
text,
style: theme.textTheme.body1.copyWith(
color: theme.primaryColor,
),
),
);
}
}
class _DemoBottomAppBar extends StatelessWidget {
const _DemoBottomAppBar({this.color, this.fabLocation, this.shape});
final Color color;
final FloatingActionButtonLocation fabLocation;
final NotchedShape shape;
static final List<FloatingActionButtonLocation> kCenterLocations =
<FloatingActionButtonLocation>[
FloatingActionButtonLocation.centerDocked,
FloatingActionButtonLocation.centerFloat,
];
@override
Widget build(BuildContext context) {
final List<Widget> rowContents = <Widget>[
IconButton(
icon: const Icon(Icons.menu, semanticLabel: 'Show bottom sheet'),
onPressed: () {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) => const _DemoDrawer(),
);
},
),
];
if (kCenterLocations.contains(fabLocation)) {
rowContents.add(
const Expanded(child: SizedBox()),
);
}
rowContents.addAll(<Widget>[
IconButton(
icon: const Icon(
Icons.search,
semanticLabel: 'show search action',
),
onPressed: () {
Scaffold.of(context).showSnackBar(
const SnackBar(content: Text('This is a dummy search action.')),
);
},
),
IconButton(
icon: Icon(
Theme.of(context).platform == TargetPlatform.iOS
? Icons.more_horiz
: Icons.more_vert,
semanticLabel: 'Show menu actions',
),
onPressed: () {
Scaffold.of(context).showSnackBar(
const SnackBar(content: Text('This is a dummy menu action.')),
);
},
),
]);
return BottomAppBar(
color: color,
child: Row(children: rowContents),
shape: shape,
);
}
}
// A drawer that pops up from the bottom of the screen.
class _DemoDrawer extends StatelessWidget {
const _DemoDrawer();
@override
Widget build(BuildContext context) {
return Drawer(
child: Column(
children: const <Widget>[
ListTile(
leading: Icon(Icons.search),
title: Text('Search'),
),
ListTile(
leading: Icon(Icons.threed_rotation),
title: Text('3D'),
),
],
),
);
}
}
// A diamond-shaped floating action button.
class _DiamondFab extends StatelessWidget {
const _DiamondFab({
this.child,
this.onPressed,
});
final Widget child;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return Material(
shape: const _DiamondBorder(),
color: Colors.orange,
child: InkWell(
onTap: onPressed,
child: Container(
width: 56.0,
height: 56.0,
child: IconTheme.merge(
data: IconThemeData(color: Theme.of(context).accentIconTheme.color),
child: child,
),
),
),
elevation: 6.0,
);
}
}
class _DiamondNotchedRectangle implements NotchedShape {
const _DiamondNotchedRectangle();
@override
Path getOuterPath(Rect host, Rect guest) {
if (!host.overlaps(guest)) return Path()..addRect(host);
assert(guest.width > 0.0);
final Rect intersection = guest.intersect(host);
// We are computing a "V" shaped notch, as in this diagram:
// -----\**** /-----
// \ /
// \ /
// \ /
//
// "-" marks the top edge of the bottom app bar.
// "\" and "/" marks the notch outline
//
// notchToCenter is the horizontal distance between the guest's center and
// the host's top edge where the notch starts (marked with "*").
// We compute notchToCenter by similar triangles:
final double notchToCenter =
intersection.height * (guest.height / 2.0) / (guest.width / 2.0);
return Path()
..moveTo(host.left, host.top)
..lineTo(guest.center.dx - notchToCenter, host.top)
..lineTo(guest.left + guest.width / 2.0, guest.bottom)
..lineTo(guest.center.dx + notchToCenter, host.top)
..lineTo(host.right, host.top)
..lineTo(host.right, host.bottom)
..lineTo(host.left, host.bottom)
..close();
}
}
class _DiamondBorder extends ShapeBorder {
const _DiamondBorder();
@override
EdgeInsetsGeometry get dimensions {
return const EdgeInsets.only();
}
@override
Path getInnerPath(Rect rect, {TextDirection textDirection}) {
return getOuterPath(rect, textDirection: textDirection);
}
@override
Path getOuterPath(Rect rect, {TextDirection textDirection}) {
return Path()
..moveTo(rect.left + rect.width / 2.0, rect.top)
..lineTo(rect.right, rect.top + rect.height / 2.0)
..lineTo(rect.left + rect.width / 2.0, rect.bottom)
..lineTo(rect.left, rect.top + rect.height / 2.0)
..close();
}
@override
void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) {}
// This border doesn't support scaling.
@override
ShapeBorder scale(double t) {
return null;
}
}

View File

@@ -0,0 +1,239 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
class NavigationIconView {
NavigationIconView({
Widget icon,
Widget activeIcon,
String title,
Color color,
TickerProvider vsync,
}) : _icon = icon,
_color = color,
_title = title,
item = BottomNavigationBarItem(
icon: icon,
activeIcon: activeIcon,
title: Text(title),
backgroundColor: color,
),
controller = AnimationController(
duration: kThemeAnimationDuration,
vsync: vsync,
) {
_animation = controller.drive(CurveTween(
curve: const Interval(0.5, 1.0, curve: Curves.fastOutSlowIn),
));
}
final Widget _icon;
final Color _color;
final String _title;
final BottomNavigationBarItem item;
final AnimationController controller;
Animation<double> _animation;
FadeTransition transition(
BottomNavigationBarType type, BuildContext context) {
Color iconColor;
if (type == BottomNavigationBarType.shifting) {
iconColor = _color;
} else {
final ThemeData themeData = Theme.of(context);
iconColor = themeData.brightness == Brightness.light
? themeData.primaryColor
: themeData.accentColor;
}
return FadeTransition(
opacity: _animation,
child: SlideTransition(
position: _animation.drive(
Tween<Offset>(
begin: const Offset(0.0, 0.02), // Slightly down.
end: Offset.zero,
),
),
child: IconTheme(
data: IconThemeData(
color: iconColor,
size: 120.0,
),
child: Semantics(
label: 'Placeholder for $_title tab',
child: _icon,
),
),
),
);
}
}
class CustomIcon extends StatelessWidget {
@override
Widget build(BuildContext context) {
final IconThemeData iconTheme = IconTheme.of(context);
return Container(
margin: const EdgeInsets.all(4.0),
width: iconTheme.size - 8.0,
height: iconTheme.size - 8.0,
color: iconTheme.color,
);
}
}
class CustomInactiveIcon extends StatelessWidget {
@override
Widget build(BuildContext context) {
final IconThemeData iconTheme = IconTheme.of(context);
return Container(
margin: const EdgeInsets.all(4.0),
width: iconTheme.size - 8.0,
height: iconTheme.size - 8.0,
decoration: BoxDecoration(
border: Border.all(color: iconTheme.color, width: 2.0),
));
}
}
class BottomNavigationDemo extends StatefulWidget {
static const String routeName = '/material/bottom_navigation';
@override
_BottomNavigationDemoState createState() => _BottomNavigationDemoState();
}
class _BottomNavigationDemoState extends State<BottomNavigationDemo>
with TickerProviderStateMixin {
int _currentIndex = 0;
BottomNavigationBarType _type = BottomNavigationBarType.shifting;
List<NavigationIconView> _navigationViews;
@override
void initState() {
super.initState();
_navigationViews = <NavigationIconView>[
NavigationIconView(
icon: const Icon(Icons.access_alarm),
title: 'Alarm',
color: Colors.deepPurple,
vsync: this,
),
NavigationIconView(
activeIcon: CustomIcon(),
icon: CustomInactiveIcon(),
title: 'Box',
color: Colors.deepOrange,
vsync: this,
),
NavigationIconView(
activeIcon: const Icon(Icons.cloud),
icon: const Icon(Icons.cloud_queue),
title: 'Cloud',
color: Colors.teal,
vsync: this,
),
NavigationIconView(
activeIcon: const Icon(Icons.favorite),
icon: const Icon(Icons.favorite_border),
title: 'Favorites',
color: Colors.indigo,
vsync: this,
),
NavigationIconView(
icon: const Icon(Icons.event_available),
title: 'Event',
color: Colors.pink,
vsync: this,
)
];
for (NavigationIconView view in _navigationViews)
view.controller.addListener(_rebuild);
_navigationViews[_currentIndex].controller.value = 1.0;
}
@override
void dispose() {
for (NavigationIconView view in _navigationViews) view.controller.dispose();
super.dispose();
}
void _rebuild() {
setState(() {
// Rebuild in order to animate views.
});
}
Widget _buildTransitionsStack() {
final List<FadeTransition> transitions = <FadeTransition>[];
for (NavigationIconView view in _navigationViews)
transitions.add(view.transition(_type, context));
// We want to have the newly animating (fading in) views on top.
transitions.sort((FadeTransition a, FadeTransition b) {
final Animation<double> aAnimation = a.opacity;
final Animation<double> bAnimation = b.opacity;
final double aValue = aAnimation.value;
final double bValue = bAnimation.value;
return aValue.compareTo(bValue);
});
return Stack(children: transitions);
}
@override
Widget build(BuildContext context) {
final BottomNavigationBar botNavBar = BottomNavigationBar(
items: _navigationViews
.map<BottomNavigationBarItem>(
(NavigationIconView navigationView) => navigationView.item)
.toList(),
currentIndex: _currentIndex,
type: _type,
onTap: (int index) {
setState(() {
_navigationViews[_currentIndex].controller.reverse();
_currentIndex = index;
_navigationViews[_currentIndex].controller.forward();
});
},
);
return Scaffold(
appBar: AppBar(
title: const Text('Bottom navigation'),
actions: <Widget>[
MaterialDemoDocumentationButton(BottomNavigationDemo.routeName),
PopupMenuButton<BottomNavigationBarType>(
onSelected: (BottomNavigationBarType value) {
setState(() {
_type = value;
});
},
itemBuilder: (BuildContext context) =>
<PopupMenuItem<BottomNavigationBarType>>[
const PopupMenuItem<BottomNavigationBarType>(
value: BottomNavigationBarType.fixed,
child: Text('Fixed'),
),
const PopupMenuItem<BottomNavigationBarType>(
value: BottomNavigationBarType.shifting,
child: Text('Shifting'),
)
],
)
],
),
body: Center(child: _buildTransitionsStack()),
bottomNavigationBar: botNavBar,
);
}
}

View File

@@ -0,0 +1,181 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/foundation.dart';
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
class TravelDestination {
const TravelDestination({
this.assetName,
this.assetPackage,
this.title,
this.description,
});
final String assetName;
final String assetPackage;
final String title;
final List<String> description;
bool get isValid =>
assetName != null && title != null && description?.length == 3;
}
final List<TravelDestination> destinations = <TravelDestination>[
const TravelDestination(
assetName: 'places/india_thanjavur_market.png',
assetPackage: _kGalleryAssetsPackage,
title: 'Top 10 Cities to Visit in Tamil Nadu',
description: <String>[
'Number 10',
'Thanjavur',
'Thanjavur, Tamil Nadu',
],
),
const TravelDestination(
assetName: 'places/india_chettinad_silk_maker.png',
assetPackage: _kGalleryAssetsPackage,
title: 'Artisans of Southern India',
description: <String>[
'Silk Spinners',
'Chettinad',
'Sivaganga, Tamil Nadu',
],
)
];
class TravelDestinationItem extends StatelessWidget {
TravelDestinationItem({Key key, @required this.destination, this.shape})
: assert(destination != null && destination.isValid),
super(key: key);
static const double height = 366.0;
final TravelDestination destination;
final ShapeBorder shape;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final TextStyle titleStyle =
theme.textTheme.headline.copyWith(color: Colors.white);
final TextStyle descriptionStyle = theme.textTheme.subhead;
return Container(
padding: const EdgeInsets.all(8.0),
height: height,
child: Card(
shape: shape,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// photo and title
SizedBox(
height: 184.0,
child: Stack(
children: <Widget>[
Positioned(
bottom: 16.0,
left: 16.0,
right: 16.0,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
destination.title,
style: titleStyle,
),
),
),
],
),
),
// description and share/explore buttons
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0.0),
child: DefaultTextStyle(
softWrap: false,
overflow: TextOverflow.ellipsis,
style: descriptionStyle,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// three line description
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
destination.description[0],
style:
descriptionStyle.copyWith(color: Colors.black54),
),
),
Text(destination.description[1]),
Text(destination.description[2]),
],
),
),
),
),
// share, explore buttons
ButtonTheme.bar(
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
FlatButton(
child: const Text('SHARE'),
textColor: Colors.amber.shade500,
onPressed: () {/* do nothing */},
),
FlatButton(
child: const Text('EXPLORE'),
textColor: Colors.amber.shade500,
onPressed: () {/* do nothing */},
),
],
),
),
],
),
),
);
}
}
class CardsDemo extends StatefulWidget {
static const String routeName = '/material/cards';
@override
_CardsDemoState createState() => _CardsDemoState();
}
class _CardsDemoState extends State<CardsDemo> {
ShapeBorder _shape;
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
@override
Widget build(BuildContext context) {
return wrapScaffold('Cards Demo', context, _scaffoldKey,
_buildContents(context), CardsDemo.routeName);
}
Widget _buildContents(BuildContext context) {
return ListView(
itemExtent: TravelDestinationItem.height,
padding: const EdgeInsets.only(top: 8.0, left: 8.0, right: 8.0),
children: destinations.map((TravelDestination destination) {
return Container(
margin: const EdgeInsets.only(bottom: 8.0),
child: TravelDestinationItem(
destination: destination,
shape: _shape,
),
);
}).toList());
}
}

View File

@@ -0,0 +1,79 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
class ChipDemo extends StatefulWidget {
static const routeName = '/material/chip';
@override
State<StatefulWidget> createState() => _ChipDemoState();
}
class _ChipDemoState extends State<ChipDemo> {
bool _filterChipSelected = false;
bool _hasAvatar = true;
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
@override
Widget build(BuildContext context) {
return wrapScaffold('Chip Demo', context, _scaffoldKey, _buildContents(),
ChipDemo.routeName);
}
Widget _buildContents() {
return Material(
child: Column(
children: [
addPadding(Chip(
label: Text('Chip'),
)),
addPadding(InputChip(
label: Text('InputChip'),
)),
addPadding(ChoiceChip(
label: Text('Selected ChoiceChip'),
selected: true,
)),
addPadding(ChoiceChip(
label: Text('Deselected ChoiceChip'),
selected: false,
)),
addPadding(FilterChip(
label: Text('FilterChip'),
selected: _filterChipSelected,
onSelected: (bool newValue) {
setState(() {
_filterChipSelected = newValue;
});
},
)),
addPadding(ActionChip(
label: Text('ActionChip'),
onPressed: () {},
)),
addPadding(ActionChip(
label: Text('Chip with avatar'),
avatar: _hasAvatar
? CircleAvatar(
backgroundColor: Colors.amber,
child: Text('Z'),
)
: null,
onPressed: () {
setState(() {
_hasAvatar = !_hasAvatar;
});
},
)),
],
));
}
}
Padding addPadding(Widget widget) => Padding(
padding: EdgeInsets.all(10.0),
child: widget,
);

View File

@@ -0,0 +1,231 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import 'package:flutter_web/rendering.dart';
import '../../gallery/demo.dart';
class Dessert {
Dessert(this.name, this.calories, this.fat, this.carbs, this.protein,
this.sodium, this.calcium, this.iron);
final String name;
final int calories;
final double fat;
final int carbs;
final double protein;
final int sodium;
final int calcium;
final int iron;
bool selected = false;
}
class DessertDataSource extends DataTableSource {
final List<Dessert> _desserts = <Dessert>[
Dessert('Frozen yogurt', 159, 6.0, 24, 4.0, 87, 14, 1),
Dessert('Ice cream sandwich', 237, 9.0, 37, 4.3, 129, 8, 1),
Dessert('Eclair', 262, 16.0, 24, 6.0, 337, 6, 7),
Dessert('Cupcake', 305, 3.7, 67, 4.3, 413, 3, 8),
Dessert('Gingerbread', 356, 16.0, 49, 3.9, 327, 7, 16),
Dessert('Jelly bean', 375, 0.0, 94, 0.0, 50, 0, 0),
Dessert('Lollipop', 392, 0.2, 98, 0.0, 38, 0, 2),
Dessert('Honeycomb', 408, 3.2, 87, 6.5, 562, 0, 45),
Dessert('Donut', 452, 25.0, 51, 4.9, 326, 2, 22),
Dessert('KitKat', 518, 26.0, 65, 7.0, 54, 12, 6),
Dessert('Frozen yogurt with sugar', 168, 6.0, 26, 4.0, 87, 14, 1),
Dessert('Ice cream sandwich with sugar', 246, 9.0, 39, 4.3, 129, 8, 1),
Dessert('Eclair with sugar', 271, 16.0, 26, 6.0, 337, 6, 7),
Dessert('Cupcake with sugar', 314, 3.7, 69, 4.3, 413, 3, 8),
Dessert('Gingerbread with sugar', 345, 16.0, 51, 3.9, 327, 7, 16),
Dessert('Jelly bean with sugar', 364, 0.0, 96, 0.0, 50, 0, 0),
Dessert('Lollipop with sugar', 401, 0.2, 100, 0.0, 38, 0, 2),
Dessert('Honeycomb with sugar', 417, 3.2, 89, 6.5, 562, 0, 45),
Dessert('Donut with sugar', 461, 25.0, 53, 4.9, 326, 2, 22),
Dessert('KitKat with sugar', 527, 26.0, 67, 7.0, 54, 12, 6),
Dessert('Frozen yogurt with honey', 223, 6.0, 36, 4.0, 87, 14, 1),
Dessert('Ice cream sandwich with honey', 301, 9.0, 49, 4.3, 129, 8, 1),
Dessert('Eclair with honey', 326, 16.0, 36, 6.0, 337, 6, 7),
Dessert('Cupcake with honey', 369, 3.7, 79, 4.3, 413, 3, 8),
Dessert('Gingerbread with honey', 420, 16.0, 61, 3.9, 327, 7, 16),
Dessert('Jelly bean with honey', 439, 0.0, 106, 0.0, 50, 0, 0),
Dessert('Lollipop with honey', 456, 0.2, 110, 0.0, 38, 0, 2),
Dessert('Honeycomb with honey', 472, 3.2, 99, 6.5, 562, 0, 45),
Dessert('Donut with honey', 516, 25.0, 63, 4.9, 326, 2, 22),
Dessert('KitKat with honey', 582, 26.0, 77, 7.0, 54, 12, 6),
Dessert('Frozen yogurt with milk', 262, 8.4, 36, 12.0, 194, 44, 1),
Dessert('Ice cream sandwich with milk', 339, 11.4, 49, 12.3, 236, 38, 1),
Dessert('Eclair with milk', 365, 18.4, 36, 14.0, 444, 36, 7),
Dessert('Cupcake with milk', 408, 6.1, 79, 12.3, 520, 33, 8),
Dessert('Gingerbread with milk', 459, 18.4, 61, 11.9, 434, 37, 16),
Dessert('Jelly bean with milk', 478, 2.4, 106, 8.0, 157, 30, 0),
Dessert('Lollipop with milk', 495, 2.6, 110, 8.0, 145, 30, 2),
Dessert('Honeycomb with milk', 511, 5.6, 99, 14.5, 669, 30, 45),
Dessert('Donut with milk', 555, 27.4, 63, 12.9, 433, 32, 22),
Dessert('KitKat with milk', 621, 28.4, 77, 15.0, 161, 42, 6),
Dessert('Coconut slice and frozen yogurt', 318, 21.0, 31, 5.5, 96, 14, 7),
Dessert(
'Coconut slice and ice cream sandwich', 396, 24.0, 44, 5.8, 138, 8, 7),
Dessert('Coconut slice and eclair', 421, 31.0, 31, 7.5, 346, 6, 13),
Dessert('Coconut slice and cupcake', 464, 18.7, 74, 5.8, 422, 3, 14),
Dessert('Coconut slice and gingerbread', 515, 31.0, 56, 5.4, 316, 7, 22),
Dessert('Coconut slice and jelly bean', 534, 15.0, 101, 1.5, 59, 0, 6),
Dessert('Coconut slice and lollipop', 551, 15.2, 105, 1.5, 47, 0, 8),
Dessert('Coconut slice and honeycomb', 567, 18.2, 94, 8.0, 571, 0, 51),
Dessert('Coconut slice and donut', 611, 40.0, 58, 6.4, 335, 2, 28),
Dessert('Coconut slice and KitKat', 677, 41.0, 72, 8.5, 63, 12, 12),
];
void _sort<T>(Comparable<T> getField(Dessert d), bool ascending) {
_desserts.sort((Dessert a, Dessert b) {
if (!ascending) {
final Dessert c = a;
a = b;
b = c;
}
final Comparable<T> aValue = getField(a);
final Comparable<T> bValue = getField(b);
return Comparable.compare(aValue, bValue);
});
notifyListeners();
}
int _selectedCount = 0;
@override
DataRow getRow(int index) {
assert(index >= 0);
if (index >= _desserts.length) return null;
final Dessert dessert = _desserts[index];
return DataRow.byIndex(
index: index,
selected: dessert.selected,
onSelectChanged: (bool value) {
if (dessert.selected != value) {
_selectedCount += value ? 1 : -1;
assert(_selectedCount >= 0);
dessert.selected = value;
notifyListeners();
}
},
cells: <DataCell>[
DataCell(Text('${dessert.name}')),
DataCell(Text('${dessert.calories}')),
DataCell(Text('${dessert.fat.toStringAsFixed(1)}')),
DataCell(Text('${dessert.carbs}')),
DataCell(Text('${dessert.protein.toStringAsFixed(1)}')),
DataCell(Text('${dessert.sodium}')),
DataCell(Text('${dessert.calcium}%')),
DataCell(Text('${dessert.iron}%')),
]);
}
@override
int get rowCount => _desserts.length;
@override
bool get isRowCountApproximate => false;
@override
int get selectedRowCount => _selectedCount;
void _selectAll(bool checked) {
for (Dessert dessert in _desserts) dessert.selected = checked;
_selectedCount = checked ? _desserts.length : 0;
notifyListeners();
}
}
class DataTableDemo extends StatefulWidget {
static const String routeName = '/material/data-table';
@override
_DataTableDemoState createState() => _DataTableDemoState();
}
class _DataTableDemoState extends State<DataTableDemo> {
int _rowsPerPage = PaginatedDataTable.defaultRowsPerPage;
int _sortColumnIndex;
bool _sortAscending = true;
final DessertDataSource _dessertsDataSource = DessertDataSource();
void _sort<T>(
Comparable<T> getField(Dessert d), int columnIndex, bool ascending) {
_dessertsDataSource._sort<T>(getField, ascending);
setState(() {
_sortColumnIndex = columnIndex;
_sortAscending = ascending;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Data tables'),
actions: <Widget>[
MaterialDemoDocumentationButton(DataTableDemo.routeName),
],
),
body: ListView(padding: const EdgeInsets.all(20.0), children: <Widget>[
PaginatedDataTable(
header: const Text('Nutrition'),
rowsPerPage: _rowsPerPage,
onRowsPerPageChanged: (int value) {
setState(() {
_rowsPerPage = value;
});
},
sortColumnIndex: _sortColumnIndex,
sortAscending: _sortAscending,
onSelectAll: _dessertsDataSource._selectAll,
columns: <DataColumn>[
DataColumn(
label: const Text('Dessert (100g serving)'),
onSort: (int columnIndex, bool ascending) => _sort<String>(
(Dessert d) => d.name, columnIndex, ascending)),
DataColumn(
label: const Text('Calories'),
tooltip:
'The total amount of food energy in the given serving size.',
numeric: true,
onSort: (int columnIndex, bool ascending) => _sort<num>(
(Dessert d) => d.calories, columnIndex, ascending)),
DataColumn(
label: const Text('Fat (g)'),
numeric: true,
onSort: (int columnIndex, bool ascending) => _sort<num>(
(Dessert d) => d.fat, columnIndex, ascending)),
DataColumn(
label: const Text('Carbs (g)'),
numeric: true,
onSort: (int columnIndex, bool ascending) => _sort<num>(
(Dessert d) => d.carbs, columnIndex, ascending)),
DataColumn(
label: const Text('Protein (g)'),
numeric: true,
onSort: (int columnIndex, bool ascending) => _sort<num>(
(Dessert d) => d.protein, columnIndex, ascending)),
DataColumn(
label: const Text('Sodium (mg)'),
numeric: true,
onSort: (int columnIndex, bool ascending) => _sort<num>(
(Dessert d) => d.sodium, columnIndex, ascending)),
DataColumn(
label: const Text('Calcium (%)'),
tooltip:
'The amount of calcium as a percentage of the recommended daily amount.',
numeric: true,
onSort: (int columnIndex, bool ascending) => _sort<num>(
(Dessert d) => d.calcium, columnIndex, ascending)),
DataColumn(
label: const Text('Iron (%)'),
numeric: true,
onSort: (int columnIndex, bool ascending) => _sort<num>(
(Dessert d) => d.iron, columnIndex, ascending)),
],
source: _dessertsDataSource)
]));
}
}

View File

@@ -0,0 +1,230 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter_web/material.dart';
import 'package:intl/intl.dart';
import '../../gallery/demo.dart';
class _InputDropdown extends StatelessWidget {
const _InputDropdown(
{Key key,
this.child,
this.labelText,
this.valueText,
this.valueStyle,
this.onPressed})
: super(key: key);
final String labelText;
final String valueText;
final TextStyle valueStyle;
final VoidCallback onPressed;
final Widget child;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onPressed,
child: InputDecorator(
decoration: InputDecoration(
labelText: labelText,
),
baseStyle: valueStyle,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(valueText, style: valueStyle),
Icon(Icons.arrow_drop_down,
color: Theme.of(context).brightness == Brightness.light
? Colors.grey.shade700
: Colors.white70),
],
),
),
);
}
}
class _DateTimePicker extends StatelessWidget {
const _DateTimePicker(
{Key key,
this.labelText,
this.selectedDate,
this.selectedTime,
this.selectDate,
this.selectTime})
: super(key: key);
final String labelText;
final DateTime selectedDate;
final TimeOfDay selectedTime;
final ValueChanged<DateTime> selectDate;
final ValueChanged<TimeOfDay> selectTime;
Future<void> _selectDate(BuildContext context) async {
final DateTime picked = await showDatePicker(
context: context,
initialDate: selectedDate,
firstDate: DateTime(2015, 8),
lastDate: DateTime(2101));
if (picked != null && picked != selectedDate) selectDate(picked);
}
Future<void> _selectTime(BuildContext context) async {
final TimeOfDay picked =
await showTimePicker(context: context, initialTime: selectedTime);
if (picked != null && picked != selectedTime) selectTime(picked);
}
@override
Widget build(BuildContext context) {
final TextStyle valueStyle = Theme.of(context).textTheme.title;
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Expanded(
flex: 4,
child: _InputDropdown(
labelText: labelText,
valueText: DateFormat.yMMMd().format(selectedDate),
valueStyle: valueStyle,
onPressed: () {
_selectDate(context);
},
),
),
const SizedBox(width: 12.0),
Expanded(
flex: 3,
child: _InputDropdown(
valueText: selectedTime.format(context),
valueStyle: valueStyle,
onPressed: () {
_selectTime(context);
},
),
),
],
);
}
}
class DateAndTimePickerDemo extends StatefulWidget {
static const String routeName = '/material/date-and-time-pickers';
@override
_DateAndTimePickerDemoState createState() => _DateAndTimePickerDemoState();
}
class _DateAndTimePickerDemoState extends State<DateAndTimePickerDemo> {
DateTime _fromDate = DateTime.now();
TimeOfDay _fromTime = const TimeOfDay(hour: 7, minute: 28);
DateTime _toDate = DateTime.now();
TimeOfDay _toTime = const TimeOfDay(hour: 7, minute: 28);
final List<String> _allActivities = <String>[
'hiking',
'swimming',
'boating',
'fishing'
];
String _activity = 'fishing';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Date and time pickers'),
actions: <Widget>[
MaterialDemoDocumentationButton(DateAndTimePickerDemo.routeName)
],
),
body: DropdownButtonHideUnderline(
child: SafeArea(
top: false,
bottom: false,
child: ListView(
padding: const EdgeInsets.all(16.0),
children: <Widget>[
TextField(
enabled: true,
decoration: const InputDecoration(
labelText: 'Event name',
border: OutlineInputBorder(),
),
style: Theme.of(context).textTheme.display1,
),
TextField(
decoration: const InputDecoration(
labelText: 'Location',
),
style: Theme.of(context)
.textTheme
.display1
.copyWith(fontSize: 20.0),
),
_DateTimePicker(
labelText: 'From',
selectedDate: _fromDate,
selectedTime: _fromTime,
selectDate: (DateTime date) {
setState(() {
_fromDate = date;
});
},
selectTime: (TimeOfDay time) {
setState(() {
_fromTime = time;
});
},
),
_DateTimePicker(
labelText: 'To',
selectedDate: _toDate,
selectedTime: _toTime,
selectDate: (DateTime date) {
setState(() {
_toDate = date;
});
},
selectTime: (TimeOfDay time) {
setState(() {
_toTime = time;
});
},
),
const SizedBox(height: 8.0),
InputDecorator(
decoration: const InputDecoration(
labelText: 'Activity',
hintText: 'Choose an activity',
contentPadding: EdgeInsets.zero,
),
isEmpty: _activity == null,
child: DropdownButton<String>(
value: _activity,
onChanged: (String newValue) {
setState(() {
_activity = newValue;
});
},
items: _allActivities
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,211 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
import 'full_screen_dialog_demo.dart';
enum DialogDemoAction {
cancel,
discard,
disagree,
agree,
}
const String _alertWithoutTitleText = 'Discard draft?';
const String _alertWithTitleText =
'Let Google help apps determine location. This means sending anonymous location '
'data to Google, even when no apps are running.';
class DialogDemoItem extends StatelessWidget {
const DialogDemoItem(
{Key key, this.icon, this.color, this.text, this.onPressed})
: super(key: key);
final IconData icon;
final Color color;
final String text;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return SimpleDialogOption(
onPressed: onPressed,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Icon(icon, size: 36.0, color: color),
Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Text(text),
),
],
),
);
}
}
class DialogDemo extends StatefulWidget {
static const String routeName = '/material/dialog';
@override
DialogDemoState createState() => DialogDemoState();
}
class DialogDemoState extends State<DialogDemo> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
TimeOfDay _selectedTime;
@override
void initState() {
super.initState();
final DateTime now = DateTime.now();
_selectedTime = TimeOfDay(hour: now.hour, minute: now.minute);
}
void showDemoDialog<T>({BuildContext context, Widget child}) {
showDialog<T>(
context: context,
builder: (BuildContext context) => child,
).then<void>((T value) {
// The value passed to Navigator.pop() or null.
if (value != null) {
_scaffoldKey.currentState
.showSnackBar(SnackBar(content: Text('You selected: $value')));
}
});
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final TextStyle dialogTextStyle =
theme.textTheme.subhead.copyWith(color: theme.textTheme.caption.color);
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: const Text('Dialogs'),
actions: <Widget>[
MaterialDemoDocumentationButton(DialogDemo.routeName)
],
),
body: ListView(
padding:
const EdgeInsets.symmetric(vertical: 24.0, horizontal: 72.0),
children: <Widget>[
RaisedButton(
child: const Text('ALERT'),
onPressed: () {
showDemoDialog<DialogDemoAction>(
context: context,
child: AlertDialog(
content: Text(_alertWithoutTitleText,
style: dialogTextStyle),
actions: <Widget>[
FlatButton(
child: const Text('CANCEL'),
onPressed: () {
Navigator.pop(
context, DialogDemoAction.cancel);
}),
FlatButton(
child: const Text('DISCARD'),
onPressed: () {
Navigator.pop(
context, DialogDemoAction.discard);
})
]));
}),
RaisedButton(
child: const Text('ALERT WITH TITLE'),
onPressed: () {
showDemoDialog<DialogDemoAction>(
context: context,
child: AlertDialog(
title:
const Text('Use Google\'s location service?'),
content: Text(_alertWithTitleText,
style: dialogTextStyle),
actions: <Widget>[
FlatButton(
child: const Text('DISAGREE'),
onPressed: () {
Navigator.pop(
context, DialogDemoAction.disagree);
}),
FlatButton(
child: const Text('AGREE'),
onPressed: () {
Navigator.pop(
context, DialogDemoAction.agree);
})
]));
}),
RaisedButton(
child: const Text('SIMPLE'),
onPressed: () {
showDemoDialog<String>(
context: context,
child: SimpleDialog(
title: const Text('Set backup account'),
children: <Widget>[
DialogDemoItem(
icon: Icons.account_circle,
color: theme.primaryColor,
text: 'username@gmail.com',
onPressed: () {
Navigator.pop(
context, 'username@gmail.com');
}),
DialogDemoItem(
icon: Icons.account_circle,
color: theme.primaryColor,
text: 'user02@gmail.com',
onPressed: () {
Navigator.pop(context, 'user02@gmail.com');
}),
DialogDemoItem(
icon: Icons.add_circle,
text: 'add account',
color: theme.disabledColor)
]));
}),
RaisedButton(
child: const Text('CONFIRMATION'),
onPressed: () {
showTimePicker(context: context, initialTime: _selectedTime)
.then<void>((TimeOfDay value) {
if (value != null && value != _selectedTime) {
_selectedTime = value;
_scaffoldKey.currentState.showSnackBar(SnackBar(
content: Text(
'You selected: ${value.format(context)}')));
}
});
}),
RaisedButton(
child: const Text('FULLSCREEN'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute<DismissDialogAction>(
builder: (BuildContext context) =>
FullScreenDialogDemo(),
fullscreenDialog: true,
));
}),
]
// Add a little space between the buttons
.map<Widget>((Widget button) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: button);
}).toList()));
}
}

View File

@@ -0,0 +1,197 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
class DrawerDemo extends StatefulWidget {
static const String routeName = '/material/drawer';
@override
_DrawerDemoState createState() => _DrawerDemoState();
}
class _DrawerDemoState extends State<DrawerDemo> with TickerProviderStateMixin {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
static const List<String> _drawerContents = <String>[
'A',
'B',
'C',
'D',
'E',
];
static final Animatable<Offset> _drawerDetailsTween = Tween<Offset>(
begin: const Offset(0.0, -1.0),
end: Offset.zero,
).chain(CurveTween(
curve: Curves.fastOutSlowIn,
));
AnimationController _controller;
Animation<double> _drawerContentsOpacity;
Animation<Offset> _drawerDetailsPosition;
bool _showDrawerContents = true;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
_drawerContentsOpacity = CurvedAnimation(
parent: ReverseAnimation(_controller),
curve: Curves.fastOutSlowIn,
);
_drawerDetailsPosition = _controller.drive(_drawerDetailsTween);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
IconData _backIcon() {
switch (Theme.of(context).platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
return Icons.arrow_back;
case TargetPlatform.iOS:
return Icons.arrow_back_ios;
}
assert(false);
return null;
}
void _showNotImplementedMessage() {
Navigator.pop(context); // Dismiss the drawer.
_scaffoldKey.currentState.showSnackBar(
const SnackBar(content: Text("The drawer's items don't do anything")));
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
leading: IconButton(
icon: Icon(_backIcon()),
alignment: Alignment.centerLeft,
tooltip: 'Back',
onPressed: () {
Navigator.pop(context);
},
),
title: const Text('Navigation drawer'),
actions: <Widget>[
MaterialDemoDocumentationButton(DrawerDemo.routeName)
],
),
drawer: Drawer(
child: Column(
children: <Widget>[
UserAccountsDrawerHeader(
accountName: const Text('Trevor Widget'),
accountEmail: const Text('trevor.widget@example.com'),
margin: EdgeInsets.zero,
onDetailsPressed: () {
_showDrawerContents = !_showDrawerContents;
if (_showDrawerContents)
_controller.reverse();
else
_controller.forward();
},
),
MediaQuery.removePadding(
context: context,
// DrawerHeader consumes top MediaQuery padding.
removeTop: true,
child: Expanded(
child: ListView(
padding: const EdgeInsets.only(top: 8.0),
children: <Widget>[
Stack(
children: <Widget>[
// The initial contents of the drawer.
FadeTransition(
opacity: _drawerContentsOpacity,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _drawerContents.map<Widget>((String id) {
return ListTile(
leading: CircleAvatar(child: Text(id)),
title: Text('Drawer item $id'),
onTap: _showNotImplementedMessage,
);
}).toList(),
),
),
// The drawer's "details" view.
SlideTransition(
position: _drawerDetailsPosition,
child: FadeTransition(
opacity: ReverseAnimation(_drawerContentsOpacity),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
ListTile(
leading: const Icon(Icons.add),
title: const Text('Add account'),
onTap: _showNotImplementedMessage,
),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('Manage accounts'),
onTap: _showNotImplementedMessage,
),
],
),
),
),
],
),
],
),
),
),
],
),
),
body: Center(
child: InkWell(
onTap: () {
_scaffoldKey.currentState.openDrawer();
},
child: Semantics(
button: true,
label: 'Open drawer',
excludeSemantics: true,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
width: 100.0,
height: 100.0,
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'Tap here to open the drawer',
style: Theme.of(context).textTheme.subhead,
),
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,92 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
class EditableTextDemo extends StatefulWidget {
static String routeName = '/material/editable_text';
@override
State<StatefulWidget> createState() => EditableTextDemoState();
}
class EditableTextDemoState extends State<EditableTextDemo> {
final cyanController = TextEditingController(text: 'Cyan');
final orangeController = TextEditingController(text: 'Orange');
final thickController = TextEditingController(text: 'Thick Rounded Cursor');
final multiController =
TextEditingController(text: 'First line\nSecond line');
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Text Editing'),
centerTitle: true,
),
body: ListView(
children: [
field(
cyanController,
color: Colors.cyan.shade50,
selection: Colors.cyan.shade200,
cursor: Colors.cyan.shade900,
),
field(
orangeController,
color: Colors.orange.shade50,
selection: Colors.orange.shade200,
cursor: Colors.orange.shade900,
center: true,
),
field(
thickController,
color: Colors.white,
selection: Colors.grey.shade200,
cursor: Colors.red.shade900,
radius: const Radius.circular(2),
cursorWidth: 8,
),
Banner(
child: TextField(
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
controller: multiController,
maxLines: 3,
),
message: 'W.I.P',
textDirection: TextDirection.ltr,
location: BannerLocation.bottomEnd,
),
],
),
);
}
}
Widget field(
TextEditingController controller, {
Color color,
Color selection,
Color cursor,
Radius radius = null,
double cursorWidth = 2,
bool center = false,
}) {
return Theme(
data: ThemeData(textSelectionColor: selection),
child: Container(
color: color,
child: TextField(
textAlign: center ? TextAlign.center : TextAlign.start,
decoration: InputDecoration(
contentPadding: EdgeInsets.fromLTRB(8, 16, 8, 16),
),
controller: controller,
cursorColor: cursor,
cursorRadius: radius,
cursorWidth: cursorWidth,
),
),
);
}

View File

@@ -0,0 +1,69 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
class ElevationDemo extends StatefulWidget {
static const String routeName = '/material/elevation';
@override
State<StatefulWidget> createState() => _ElevationDemoState();
}
class _ElevationDemoState extends State<ElevationDemo> {
bool _showElevation = true;
List<Widget> buildCards() {
const List<double> elevations = <double>[
0.0,
1.0,
2.0,
3.0,
4.0,
5.0,
8.0,
16.0,
24.0,
];
return elevations.map<Widget>((double elevation) {
return Center(
child: Card(
margin: const EdgeInsets.all(20.0),
elevation: _showElevation ? elevation : 0.0,
child: SizedBox(
height: 100.0,
width: 100.0,
child: Center(
child: Text('${elevation.toStringAsFixed(0)} pt'),
),
),
),
);
}).toList();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Elevation'),
actions: <Widget>[
MaterialDemoDocumentationButton(ElevationDemo.routeName),
IconButton(
icon: const Icon(Icons.sentiment_very_satisfied),
onPressed: () {
setState(() => _showElevation = !_showElevation);
},
)
],
),
body: ListView(
children: buildCards(),
),
);
}
}

View File

@@ -0,0 +1,335 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
@visibleForTesting
enum Location { Barbados, Bahamas, Bermuda }
typedef DemoItemBodyBuilder<T> = Widget Function(DemoItem<T> item);
typedef ValueToString<T> = String Function(T value);
class DualHeaderWithHint extends StatelessWidget {
const DualHeaderWithHint({this.name, this.value, this.hint, this.showHint});
final String name;
final String value;
final String hint;
final bool showHint;
Widget _crossFade(Widget first, Widget second, bool isExpanded) {
return AnimatedCrossFade(
firstChild: first,
secondChild: second,
firstCurve: const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn),
secondCurve: const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn),
sizeCurve: Curves.fastOutSlowIn,
crossFadeState:
isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst,
duration: const Duration(milliseconds: 200),
);
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final TextTheme textTheme = theme.textTheme;
return Row(children: <Widget>[
Expanded(
flex: 2,
child: Container(
margin: const EdgeInsets.only(left: 24.0),
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
name,
style: textTheme.body1.copyWith(fontSize: 15.0),
),
),
),
),
Expanded(
flex: 3,
child: Container(
margin: const EdgeInsets.only(left: 24.0),
child: _crossFade(
Text(value,
style: textTheme.caption.copyWith(fontSize: 15.0)),
Text(hint, style: textTheme.caption.copyWith(fontSize: 15.0)),
showHint)))
]);
}
}
class CollapsibleBody extends StatelessWidget {
const CollapsibleBody(
{this.margin = EdgeInsets.zero, this.child, this.onSave, this.onCancel});
final EdgeInsets margin;
final Widget child;
final VoidCallback onSave;
final VoidCallback onCancel;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final TextTheme textTheme = theme.textTheme;
return Column(children: <Widget>[
Container(
margin: const EdgeInsets.only(left: 24.0, right: 24.0, bottom: 24.0) -
margin,
child: Center(
child: DefaultTextStyle(
style: textTheme.caption.copyWith(fontSize: 15.0),
child: child))),
const Divider(height: 1.0),
Container(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child:
Row(mainAxisAlignment: MainAxisAlignment.end, children: <Widget>[
Container(
margin: const EdgeInsets.only(right: 8.0),
child: FlatButton(
onPressed: onCancel,
child: const Text('CANCEL',
style: TextStyle(
color: Colors.black54,
fontSize: 15.0,
fontWeight: FontWeight.w500)))),
Container(
margin: const EdgeInsets.only(right: 8.0),
child: FlatButton(
onPressed: onSave,
textTheme: ButtonTextTheme.accent,
child: const Text('SAVE')))
]))
]);
}
}
class DemoItem<T> {
DemoItem({this.name, this.value, this.hint, this.builder, this.valueToString})
: textController = TextEditingController(text: valueToString(value));
final String name;
final String hint;
final TextEditingController textController;
final DemoItemBodyBuilder<T> builder;
final ValueToString<T> valueToString;
T value;
bool isExpanded = false;
ExpansionPanelHeaderBuilder get headerBuilder {
return (BuildContext context, bool isExpanded) {
return DualHeaderWithHint(
name: name,
value: valueToString(value),
hint: hint,
showHint: isExpanded);
};
}
Widget build() => builder(this);
}
class ExpansionPanelsDemo extends StatefulWidget {
static const String routeName = '/material/expansion_panels';
@override
_ExpansionPanelsDemoState createState() => _ExpansionPanelsDemoState();
}
class _ExpansionPanelsDemoState extends State<ExpansionPanelsDemo> {
List<DemoItem<dynamic>> _demoItems;
@override
void initState() {
super.initState();
_demoItems = <DemoItem<dynamic>>[
DemoItem<String>(
name: 'Trip',
value: 'Caribbean cruise',
hint: 'Change trip name',
valueToString: (String value) => value,
builder: (DemoItem<String> item) {
void close() {
setState(() {
item.isExpanded = false;
});
}
return Form(
child: Builder(
builder: (BuildContext context) {
return CollapsibleBody(
margin: const EdgeInsets.symmetric(horizontal: 16.0),
onSave: () {
Form.of(context).save();
close();
},
onCancel: () {
Form.of(context).reset();
close();
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: TextFormField(
controller: item.textController,
decoration: InputDecoration(
hintText: item.hint,
labelText: item.name,
),
onSaved: (String value) {
item.value = value;
},
),
),
);
},
),
);
},
),
DemoItem<Location>(
name: 'Location',
value: Location.Bahamas,
hint: 'Select location',
valueToString: (Location location) =>
location.toString().split('.')[1],
builder: (DemoItem<Location> item) {
void close() {
setState(() {
item.isExpanded = false;
});
}
return Form(child: Builder(builder: (BuildContext context) {
return CollapsibleBody(
onSave: () {
Form.of(context).save();
close();
},
onCancel: () {
Form.of(context).reset();
close();
},
child: FormField<Location>(
initialValue: item.value,
onSaved: (Location result) {
item.value = result;
},
builder: (FormFieldState<Location> field) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
RadioListTile<Location>(
value: Location.Bahamas,
title: const Text('Bahamas'),
groupValue: field.value,
onChanged: field.didChange,
),
RadioListTile<Location>(
value: Location.Barbados,
title: const Text('Barbados'),
groupValue: field.value,
onChanged: field.didChange,
),
RadioListTile<Location>(
value: Location.Bermuda,
title: const Text('Bermuda'),
groupValue: field.value,
onChanged: field.didChange,
),
]);
}),
);
}));
}),
DemoItem<double>(
name: 'Sun',
value: 80.0,
hint: 'Select sun level',
valueToString: (double amount) => '${amount.round()}',
builder: (DemoItem<double> item) {
void close() {
setState(() {
item.isExpanded = false;
});
}
return Form(child: Builder(builder: (BuildContext context) {
return CollapsibleBody(
onSave: () {
Form.of(context).save();
close();
},
onCancel: () {
Form.of(context).reset();
close();
},
child: FormField<double>(
initialValue: item.value,
onSaved: (double value) {
item.value = value;
},
builder: (FormFieldState<double> field) {
return Slider(
min: 0.0,
max: 100.0,
divisions: 5,
activeColor:
Colors.orange[100 + (field.value * 5.0).round()],
label: '${field.value.round()}',
value: field.value,
onChanged: field.didChange,
);
},
),
);
}));
})
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Expansion panels'),
actions: <Widget>[
MaterialDemoDocumentationButton(ExpansionPanelsDemo.routeName),
],
),
body: SingleChildScrollView(
child: SafeArea(
top: false,
bottom: false,
child: Container(
margin: const EdgeInsets.all(24.0),
child: ExpansionPanelList(
expansionCallback: (int index, bool isExpanded) {
setState(() {
_demoItems[index].isExpanded = !isExpanded;
});
},
children:
_demoItems.map<ExpansionPanel>((DemoItem<dynamic> item) {
return ExpansionPanel(
isExpanded: item.isExpanded,
headerBuilder: item.headerBuilder,
body: item.build());
}).toList()),
),
),
),
);
}
}

View File

@@ -0,0 +1,232 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter_web/material.dart';
import 'package:intl/intl.dart';
// This demo is based on
// https://material.google.com/components/dialogs.html#dialogs-full-screen-dialogs
enum DismissDialogAction {
cancel,
discard,
save,
}
class DateTimeItem extends StatelessWidget {
DateTimeItem({Key key, DateTime dateTime, @required this.onChanged})
: assert(onChanged != null),
date = DateTime(dateTime.year, dateTime.month, dateTime.day),
time = TimeOfDay(hour: dateTime.hour, minute: dateTime.minute),
super(key: key);
final DateTime date;
final TimeOfDay time;
final ValueChanged<DateTime> onChanged;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return DefaultTextStyle(
style: theme.textTheme.subhead,
child: Row(children: <Widget>[
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: theme.dividerColor))),
child: InkWell(
onTap: () {
showDatePicker(
context: context,
initialDate: date,
firstDate:
date.subtract(const Duration(days: 30)),
lastDate: date.add(const Duration(days: 30)))
.then<void>((DateTime value) {
if (value != null)
onChanged(DateTime(value.year, value.month,
value.day, time.hour, time.minute));
});
},
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(DateFormat('EEE, MMM d yyyy').format(date)),
const Icon(Icons.arrow_drop_down,
color: Colors.black54),
])))),
Container(
margin: const EdgeInsets.only(left: 8.0),
padding: const EdgeInsets.symmetric(vertical: 8.0),
decoration: BoxDecoration(
border:
Border(bottom: BorderSide(color: theme.dividerColor))),
child: InkWell(
onTap: () {
showTimePicker(context: context, initialTime: time)
.then<void>((TimeOfDay value) {
if (value != null)
onChanged(DateTime(date.year, date.month, date.day,
value.hour, value.minute));
});
},
child: Row(children: <Widget>[
Text('${time.format(context)}'),
const Icon(Icons.arrow_drop_down, color: Colors.black54),
])))
]));
}
}
class FullScreenDialogDemo extends StatefulWidget {
@override
FullScreenDialogDemoState createState() => FullScreenDialogDemoState();
}
class FullScreenDialogDemoState extends State<FullScreenDialogDemo> {
DateTime _fromDateTime = DateTime.now();
DateTime _toDateTime = DateTime.now();
bool _allDayValue = false;
bool _saveNeeded = false;
bool _hasLocation = false;
bool _hasName = false;
String _eventName;
Future<bool> _onWillPop() async {
_saveNeeded = _hasLocation || _hasName || _saveNeeded;
if (!_saveNeeded) return true;
final ThemeData theme = Theme.of(context);
final TextStyle dialogTextStyle =
theme.textTheme.subhead.copyWith(color: theme.textTheme.caption.color);
return await showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: Text('Discard new event?', style: dialogTextStyle),
actions: <Widget>[
FlatButton(
child: const Text('CANCEL'),
onPressed: () {
Navigator.of(context).pop(
false); // Pops the confirmation dialog but not the page.
}),
FlatButton(
child: const Text('DISCARD'),
onPressed: () {
Navigator.of(context).pop(
true); // Returning true to _onWillPop will pop again.
})
],
);
},
) ??
false;
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: Text(_hasName ? _eventName : 'Event Name TBD'),
actions: <Widget>[
FlatButton(
child: Text('SAVE',
style: theme.textTheme.body1.copyWith(color: Colors.white)),
onPressed: () {
Navigator.pop(context, DismissDialogAction.save);
})
]),
body: Form(
onWillPop: _onWillPop,
child: ListView(
padding: const EdgeInsets.all(16.0),
children: <Widget>[
Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
alignment: Alignment.bottomLeft,
child: TextField(
decoration: const InputDecoration(
labelText: 'Event name', filled: true),
style: theme.textTheme.headline,
onChanged: (String value) {
setState(() {
_hasName = value.isNotEmpty;
if (_hasName) {
_eventName = value;
}
});
})),
Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
alignment: Alignment.bottomLeft,
child: TextField(
decoration: const InputDecoration(
labelText: 'Location',
hintText: 'Where is the event?',
filled: true),
onChanged: (String value) {
setState(() {
_hasLocation = value.isNotEmpty;
});
})),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('From', style: theme.textTheme.caption),
DateTimeItem(
dateTime: _fromDateTime,
onChanged: (DateTime value) {
setState(() {
_fromDateTime = value;
_saveNeeded = true;
});
})
]),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('To', style: theme.textTheme.caption),
DateTimeItem(
dateTime: _toDateTime,
onChanged: (DateTime value) {
setState(() {
_toDateTime = value;
_saveNeeded = true;
});
}),
const Text('All-day'),
]),
Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: theme.dividerColor))),
child: Row(children: <Widget>[
Checkbox(
value: _allDayValue,
onChanged: (bool value) {
setState(() {
_allDayValue = value;
_saveNeeded = true;
});
}),
const Text('All-day'),
]))
].map<Widget>((Widget child) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
height: 96.0,
child: child);
}).toList())),
);
}
}

View File

@@ -0,0 +1,397 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
enum GridDemoTileStyle { imageOnly, oneLine, twoLine }
typedef BannerTapCallback = void Function(Photo photo);
const double _kMinFlingVelocity = 800.0;
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
class Photo {
Photo({
this.assetName,
this.assetPackage,
this.title,
this.caption,
this.isFavorite = false,
});
final String assetName;
final String assetPackage;
final String title;
final String caption;
bool isFavorite;
String get tag => assetName; // Assuming that all asset names are unique.
bool get isValid =>
assetName != null &&
title != null &&
caption != null &&
isFavorite != null;
}
class GridPhotoViewer extends StatefulWidget {
const GridPhotoViewer({Key key, this.photo}) : super(key: key);
final Photo photo;
@override
_GridPhotoViewerState createState() => _GridPhotoViewerState();
}
class _GridTitleText extends StatelessWidget {
const _GridTitleText(this.text);
final String text;
@override
Widget build(BuildContext context) {
return FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(text),
);
}
}
class _GridPhotoViewerState extends State<GridPhotoViewer>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<Offset> _flingAnimation;
Offset _offset = Offset.zero;
double _scale = 1.0;
Offset _normalizedOffset;
double _previousScale;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this)
..addListener(_handleFlingAnimation);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
// The maximum offset value is 0,0. If the size of this renderer's box is w,h
// then the minimum offset value is w - _scale * w, h - _scale * h.
Offset _clampOffset(Offset offset) {
final Size size = context.size;
final Offset minOffset = Offset(size.width, size.height) * (1.0 - _scale);
return Offset(
offset.dx.clamp(minOffset.dx, 0.0), offset.dy.clamp(minOffset.dy, 0.0));
}
void _handleFlingAnimation() {
setState(() {
_offset = _flingAnimation.value;
});
}
void _handleOnScaleStart(ScaleStartDetails details) {
setState(() {
_previousScale = _scale;
_normalizedOffset = (details.focalPoint - _offset) / _scale;
// The fling animation stops if an input gesture starts.
_controller.stop();
});
}
void _handleOnScaleUpdate(ScaleUpdateDetails details) {
setState(() {
_scale = (_previousScale * details.scale).clamp(1.0, 4.0);
// Ensure that image location under the focal point stays in the same place despite scaling.
_offset = _clampOffset(details.focalPoint - _normalizedOffset * _scale);
});
}
void _handleOnScaleEnd(ScaleEndDetails details) {
final double magnitude = details.velocity.pixelsPerSecond.distance;
if (magnitude < _kMinFlingVelocity) return;
final Offset direction = details.velocity.pixelsPerSecond / magnitude;
final double distance = (Offset.zero & context.size).shortestSide;
_flingAnimation = _controller.drive(Tween<Offset>(
begin: _offset, end: _clampOffset(_offset + direction * distance)));
_controller
..value = 0.0
..fling(velocity: magnitude / 1000.0);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onScaleStart: _handleOnScaleStart,
onScaleUpdate: _handleOnScaleUpdate,
onScaleEnd: _handleOnScaleEnd,
child: ClipRect(
child: Transform(
transform: Matrix4.identity()
..translate(_offset.dx, _offset.dy)
..scale(_scale),
child: Image.asset(
'${widget.photo.assetName}',
// TODO(flutter_web): package: widget.photo.assetPackage,
fit: BoxFit.cover,
),
),
),
);
}
}
class GridDemoPhotoItem extends StatelessWidget {
GridDemoPhotoItem(
{Key key,
@required this.photo,
@required this.tileStyle,
@required this.onBannerTap})
: assert(photo != null && photo.isValid),
assert(tileStyle != null),
assert(onBannerTap != null),
super(key: key);
final Photo photo;
final GridDemoTileStyle tileStyle;
final BannerTapCallback
onBannerTap; // User taps on the photo's header or footer.
void showPhoto(BuildContext context) {
Navigator.push(context,
MaterialPageRoute<void>(builder: (BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(photo.title)),
body: SizedBox.expand(
child: Hero(
tag: photo.tag,
child: GridPhotoViewer(photo: photo),
),
),
);
}));
}
@override
Widget build(BuildContext context) {
final Widget image = GestureDetector(
onTap: () {
showPhoto(context);
},
child: Hero(
key: Key(photo.assetName),
tag: photo.tag,
child: Image.asset(
'${photo.assetName}',
// TODO(flutter_web): package: photo.assetPackage,
fit: BoxFit.cover,
)));
final IconData icon = photo.isFavorite ? Icons.star : Icons.star_border;
switch (tileStyle) {
case GridDemoTileStyle.imageOnly:
return image;
case GridDemoTileStyle.oneLine:
return GridTile(
header: GestureDetector(
onTap: () {
onBannerTap(photo);
},
child: GridTileBar(
title: _GridTitleText(photo.title),
backgroundColor: Colors.black45,
leading: Icon(
icon,
color: Colors.white,
),
),
),
child: image,
);
case GridDemoTileStyle.twoLine:
return GridTile(
footer: GestureDetector(
onTap: () {
onBannerTap(photo);
},
child: GridTileBar(
backgroundColor: Colors.black45,
title: _GridTitleText(photo.title),
subtitle: _GridTitleText(photo.caption),
trailing: Icon(
icon,
color: Colors.white,
),
),
),
child: image,
);
}
assert(tileStyle != null);
return null;
}
}
class GridListDemo extends StatefulWidget {
const GridListDemo({Key key}) : super(key: key);
static const String routeName = '/material/grid-list';
@override
GridListDemoState createState() => GridListDemoState();
}
class GridListDemoState extends State<GridListDemo> {
GridDemoTileStyle _tileStyle = GridDemoTileStyle.twoLine;
List<Photo> photos = <Photo>[
Photo(
assetName: 'places/india_chennai_flower_market.png',
assetPackage: _kGalleryAssetsPackage,
title: 'Chennai',
caption: 'Flower Market',
),
Photo(
assetName: 'places/india_tanjore_bronze_works.png',
assetPackage: _kGalleryAssetsPackage,
title: 'Tanjore',
caption: 'Bronze Works',
),
Photo(
assetName: 'places/india_tanjore_market_merchant.png',
assetPackage: _kGalleryAssetsPackage,
title: 'Tanjore',
caption: 'Market',
),
Photo(
assetName: 'places/india_tanjore_thanjavur_temple.png',
assetPackage: _kGalleryAssetsPackage,
title: 'Tanjore',
caption: 'Thanjavur Temple',
),
Photo(
assetName: 'places/india_tanjore_thanjavur_temple_carvings.png',
assetPackage: _kGalleryAssetsPackage,
title: 'Tanjore',
caption: 'Thanjavur Temple',
),
Photo(
assetName: 'places/india_pondicherry_salt_farm.png',
assetPackage: _kGalleryAssetsPackage,
title: 'Pondicherry',
caption: 'Salt Farm',
),
Photo(
assetName: 'places/india_chennai_highway.png',
assetPackage: _kGalleryAssetsPackage,
title: 'Chennai',
caption: 'Scooters',
),
Photo(
assetName: 'places/india_chettinad_silk_maker.png',
assetPackage: _kGalleryAssetsPackage,
title: 'Chettinad',
caption: 'Silk Maker',
),
Photo(
assetName: 'places/india_chettinad_produce.png',
assetPackage: _kGalleryAssetsPackage,
title: 'Chettinad',
caption: 'Lunch Prep',
),
Photo(
assetName: 'places/india_tanjore_market_technology.png',
assetPackage: _kGalleryAssetsPackage,
title: 'Tanjore',
caption: 'Market',
),
Photo(
assetName: 'places/india_pondicherry_beach.png',
assetPackage: _kGalleryAssetsPackage,
title: 'Pondicherry',
caption: 'Beach',
),
Photo(
assetName: 'places/india_pondicherry_fisherman.png',
assetPackage: _kGalleryAssetsPackage,
title: 'Pondicherry',
caption: 'Fisherman',
),
];
void changeTileStyle(GridDemoTileStyle value) {
setState(() {
_tileStyle = value;
});
}
@override
Widget build(BuildContext context) {
final Orientation orientation = MediaQuery.of(context).orientation;
return Scaffold(
appBar: AppBar(
title: const Text('Grid list'),
actions: <Widget>[
MaterialDemoDocumentationButton(GridListDemo.routeName),
PopupMenuButton<GridDemoTileStyle>(
onSelected: changeTileStyle,
itemBuilder: (BuildContext context) =>
<PopupMenuItem<GridDemoTileStyle>>[
const PopupMenuItem<GridDemoTileStyle>(
value: GridDemoTileStyle.imageOnly,
child: Text('Image only'),
),
const PopupMenuItem<GridDemoTileStyle>(
value: GridDemoTileStyle.oneLine,
child: Text('One line'),
),
const PopupMenuItem<GridDemoTileStyle>(
value: GridDemoTileStyle.twoLine,
child: Text('Two line'),
),
],
),
],
),
body: Column(
children: <Widget>[
Expanded(
child: SafeArea(
top: false,
bottom: false,
child: GridView.count(
crossAxisCount: (orientation == Orientation.portrait) ? 2 : 3,
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
padding: const EdgeInsets.all(4.0),
childAspectRatio:
(orientation == Orientation.portrait) ? 1.0 : 1.3,
children: photos.map<Widget>((Photo photo) {
return GridDemoPhotoItem(
photo: photo,
tileStyle: _tileStyle,
onBannerTap: (Photo photo) {
setState(() {
photo.isFavorite = !photo.isFavorite;
});
});
}).toList(),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,135 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
class IconsDemo extends StatefulWidget {
static const String routeName = '/material/icons';
@override
IconsDemoState createState() => IconsDemoState();
}
class IconsDemoState extends State<IconsDemo> {
static final List<MaterialColor> iconColors = <MaterialColor>[
Colors.red,
Colors.pink,
Colors.purple,
Colors.deepPurple,
Colors.indigo,
Colors.blue,
Colors.lightBlue,
Colors.cyan,
Colors.teal,
Colors.green,
Colors.lightGreen,
Colors.lime,
Colors.yellow,
Colors.amber,
Colors.orange,
Colors.deepOrange,
Colors.brown,
Colors.grey,
Colors.blueGrey,
];
int iconColorIndex = 8; // teal
Color get iconColor => iconColors[iconColorIndex];
void handleIconButtonPress() {
setState(() {
iconColorIndex = (iconColorIndex + 1) % iconColors.length;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Icons'),
actions: <Widget>[MaterialDemoDocumentationButton(IconsDemo.routeName)],
),
body: IconTheme(
data: IconThemeData(color: iconColor),
child: SafeArea(
top: false,
bottom: false,
child: ListView(
padding: const EdgeInsets.all(24.0),
children: <Widget>[
_IconsDemoCard(
handleIconButtonPress, Icons.face), // direction-agnostic icon
const SizedBox(height: 24.0),
_IconsDemoCard(handleIconButtonPress,
Icons.battery_unknown), // direction-aware icon
],
),
),
),
);
}
}
class _IconsDemoCard extends StatelessWidget {
const _IconsDemoCard(this.handleIconButtonPress, this.icon);
final VoidCallback handleIconButtonPress;
final IconData icon;
Widget _buildIconButton(double iconSize, IconData icon, bool enabled) {
return IconButton(
icon: Icon(icon),
iconSize: iconSize,
tooltip: "${enabled ? 'Enabled' : 'Disabled'} icon button",
onPressed: enabled ? handleIconButtonPress : null);
}
Widget _centeredText(String label) => Padding(
// Match the default padding of IconButton.
padding: const EdgeInsets.all(8.0),
child: Text(label, textAlign: TextAlign.center),
);
TableRow _buildIconRow(double size) {
return TableRow(
children: <Widget>[
_centeredText(size.floor().toString()),
_buildIconButton(size, icon, true),
_buildIconButton(size, icon, false),
],
);
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final TextStyle textStyle =
theme.textTheme.subhead.copyWith(color: theme.textTheme.caption.color);
return Card(
child: DefaultTextStyle(
style: textStyle,
child: Semantics(
explicitChildNodes: true,
child: Table(
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
children: <TableRow>[
TableRow(children: <Widget>[
_centeredText('Size'),
_centeredText('Enabled'),
_centeredText('Disabled'),
]),
_buildIconRow(18.0),
_buildIconRow(24.0),
_buildIconRow(36.0),
_buildIconRow(48.0),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,228 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:collection/collection.dart' show lowerBound;
import 'package:flutter_web/material.dart';
import 'package:flutter_web/semantics.dart';
import '../../gallery/demo.dart';
enum LeaveBehindDemoAction { reset, horizontalSwipe, leftSwipe, rightSwipe }
class LeaveBehindItem implements Comparable<LeaveBehindItem> {
LeaveBehindItem({this.index, this.name, this.subject, this.body});
LeaveBehindItem.from(LeaveBehindItem item)
: index = item.index,
name = item.name,
subject = item.subject,
body = item.body;
final int index;
final String name;
final String subject;
final String body;
@override
int compareTo(LeaveBehindItem other) => index.compareTo(other.index);
}
class LeaveBehindDemo extends StatefulWidget {
const LeaveBehindDemo({Key key}) : super(key: key);
static const String routeName = '/material/leave-behind';
@override
LeaveBehindDemoState createState() => LeaveBehindDemoState();
}
class LeaveBehindDemoState extends State<LeaveBehindDemo> {
static final GlobalKey<ScaffoldState> _scaffoldKey =
GlobalKey<ScaffoldState>();
DismissDirection _dismissDirection = DismissDirection.horizontal;
List<LeaveBehindItem> leaveBehindItems;
void initListItems() {
leaveBehindItems = List<LeaveBehindItem>.generate(16, (int index) {
return LeaveBehindItem(
index: index,
name: 'Item $index Sender',
subject: 'Subject: $index',
body: "[$index] first line of the message's body...");
});
}
@override
void initState() {
super.initState();
initListItems();
}
void handleDemoAction(LeaveBehindDemoAction action) {
setState(() {
switch (action) {
case LeaveBehindDemoAction.reset:
initListItems();
break;
case LeaveBehindDemoAction.horizontalSwipe:
_dismissDirection = DismissDirection.horizontal;
break;
case LeaveBehindDemoAction.leftSwipe:
_dismissDirection = DismissDirection.endToStart;
break;
case LeaveBehindDemoAction.rightSwipe:
_dismissDirection = DismissDirection.startToEnd;
break;
}
});
}
void handleUndo(LeaveBehindItem item) {
final int insertionIndex = lowerBound(leaveBehindItems, item);
setState(() {
leaveBehindItems.insert(insertionIndex, item);
});
}
void _handleArchive(LeaveBehindItem item) {
setState(() {
leaveBehindItems.remove(item);
});
_scaffoldKey.currentState.showSnackBar(SnackBar(
content: Text('You archived item ${item.index}'),
action: SnackBarAction(
label: 'UNDO',
onPressed: () {
handleUndo(item);
})));
}
void _handleDelete(LeaveBehindItem item) {
setState(() {
leaveBehindItems.remove(item);
});
_scaffoldKey.currentState.showSnackBar(SnackBar(
content: Text('You deleted item ${item.index}'),
action: SnackBarAction(
label: 'UNDO',
onPressed: () {
handleUndo(item);
})));
}
@override
Widget build(BuildContext context) {
Widget body;
if (leaveBehindItems.isEmpty) {
body = Center(
child: RaisedButton(
onPressed: () => handleDemoAction(LeaveBehindDemoAction.reset),
child: const Text('Reset the list'),
),
);
} else {
body = ListView(
children: leaveBehindItems.map<Widget>((LeaveBehindItem item) {
return _LeaveBehindListItem(
item: item,
onArchive: _handleArchive,
onDelete: _handleDelete,
dismissDirection: _dismissDirection,
);
}).toList());
}
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(title: const Text('Swipe to dismiss'), actions: <Widget>[
MaterialDemoDocumentationButton(LeaveBehindDemo.routeName),
PopupMenuButton<LeaveBehindDemoAction>(
onSelected: handleDemoAction,
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<LeaveBehindDemoAction>>[
const PopupMenuItem<LeaveBehindDemoAction>(
value: LeaveBehindDemoAction.reset,
child: Text('Reset the list')),
const PopupMenuDivider(),
CheckedPopupMenuItem<LeaveBehindDemoAction>(
value: LeaveBehindDemoAction.horizontalSwipe,
checked: _dismissDirection == DismissDirection.horizontal,
child: const Text('Horizontal swipe')),
CheckedPopupMenuItem<LeaveBehindDemoAction>(
value: LeaveBehindDemoAction.leftSwipe,
checked: _dismissDirection == DismissDirection.endToStart,
child: const Text('Only swipe left')),
CheckedPopupMenuItem<LeaveBehindDemoAction>(
value: LeaveBehindDemoAction.rightSwipe,
checked: _dismissDirection == DismissDirection.startToEnd,
child: const Text('Only swipe right'))
])
]),
body: body,
);
}
}
class _LeaveBehindListItem extends StatelessWidget {
const _LeaveBehindListItem({
Key key,
@required this.item,
@required this.onArchive,
@required this.onDelete,
@required this.dismissDirection,
}) : super(key: key);
final LeaveBehindItem item;
final DismissDirection dismissDirection;
final void Function(LeaveBehindItem) onArchive;
final void Function(LeaveBehindItem) onDelete;
void _handleArchive() {
onArchive(item);
}
void _handleDelete() {
onDelete(item);
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return Semantics(
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
const CustomSemanticsAction(label: 'Archive'): _handleArchive,
const CustomSemanticsAction(label: 'Delete'): _handleDelete,
},
child: Dismissible(
key: ObjectKey(item),
direction: dismissDirection,
onDismissed: (DismissDirection direction) {
if (direction == DismissDirection.endToStart)
_handleArchive();
else
_handleDelete();
},
background: Container(
color: theme.primaryColor,
child: const ListTile(
leading: Icon(Icons.delete, color: Colors.white, size: 36.0))),
secondaryBackground: Container(
color: theme.primaryColor,
child: const ListTile(
trailing:
Icon(Icons.archive, color: Colors.white, size: 36.0))),
child: Container(
decoration: BoxDecoration(
color: theme.canvasColor,
border: Border(bottom: BorderSide(color: theme.dividerColor))),
child: ListTile(
title: Text(item.name),
subtitle: Text('${item.subject}\n${item.body}'),
isThreeLine: true),
),
),
);
}
}

View File

@@ -0,0 +1,273 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
enum _MaterialListType {
/// A list tile that contains a single line of text.
oneLine,
/// A list tile that contains a [CircleAvatar] followed by a single line of text.
oneLineWithAvatar,
/// A list tile that contains two lines of text.
twoLine,
/// A list tile that contains three lines of text.
threeLine,
}
class ListDemo extends StatefulWidget {
const ListDemo({Key key}) : super(key: key);
static const String routeName = '/material/list';
@override
_ListDemoState createState() => _ListDemoState();
}
class _ListDemoState extends State<ListDemo> {
static final GlobalKey<ScaffoldState> scaffoldKey =
GlobalKey<ScaffoldState>();
PersistentBottomSheetController<void> _bottomSheet;
_MaterialListType _itemType = _MaterialListType.threeLine;
bool _dense = false;
bool _showAvatars = true;
bool _showIcons = false;
bool _showDividers = false;
bool _reverseSort = false;
List<String> items = <String>[
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
];
void changeItemType(_MaterialListType type) {
setState(() {
_itemType = type;
});
_bottomSheet?.setState(() {});
}
void _showConfigurationSheet() {
final PersistentBottomSheetController<void> bottomSheet = scaffoldKey
.currentState
.showBottomSheet<void>((BuildContext bottomSheetContext) {
return Container(
decoration: const BoxDecoration(
border: Border(top: BorderSide(color: Colors.black26)),
),
child: ListView(
shrinkWrap: true,
primary: false,
children: <Widget>[
MergeSemantics(
child: ListTile(
dense: true,
title: const Text('One-line'),
trailing: Radio<_MaterialListType>(
value: _showAvatars
? _MaterialListType.oneLineWithAvatar
: _MaterialListType.oneLine,
groupValue: _itemType,
onChanged: changeItemType,
)),
),
MergeSemantics(
child: ListTile(
dense: true,
title: const Text('Two-line'),
trailing: Radio<_MaterialListType>(
value: _MaterialListType.twoLine,
groupValue: _itemType,
onChanged: changeItemType,
)),
),
MergeSemantics(
child: ListTile(
dense: true,
title: const Text('Three-line'),
trailing: Radio<_MaterialListType>(
value: _MaterialListType.threeLine,
groupValue: _itemType,
onChanged: changeItemType,
),
),
),
MergeSemantics(
child: ListTile(
dense: true,
title: const Text('Show avatar'),
trailing: Checkbox(
value: _showAvatars,
onChanged: (bool value) {
setState(() {
_showAvatars = value;
});
_bottomSheet?.setState(() {});
},
),
),
),
MergeSemantics(
child: ListTile(
dense: true,
title: const Text('Show icon'),
trailing: Checkbox(
value: _showIcons,
onChanged: (bool value) {
setState(() {
_showIcons = value;
});
_bottomSheet?.setState(() {});
},
),
),
),
MergeSemantics(
child: ListTile(
dense: true,
title: const Text('Show dividers'),
trailing: Checkbox(
value: _showDividers,
onChanged: (bool value) {
setState(() {
_showDividers = value;
});
_bottomSheet?.setState(() {});
},
),
),
),
MergeSemantics(
child: ListTile(
dense: true,
title: const Text('Dense layout'),
trailing: Checkbox(
value: _dense,
onChanged: (bool value) {
setState(() {
_dense = value;
});
_bottomSheet?.setState(() {});
},
),
),
),
],
),
);
});
setState(() {
_bottomSheet = bottomSheet;
});
_bottomSheet.closed.whenComplete(() {
if (mounted) {
setState(() {
_bottomSheet = null;
});
}
});
}
Widget buildListTile(BuildContext context, String item) {
Widget secondary;
if (_itemType == _MaterialListType.twoLine) {
secondary = const Text('Additional item information.');
} else if (_itemType == _MaterialListType.threeLine) {
secondary = const Text(
'Even more additional list item information appears on line three.',
);
}
return MergeSemantics(
child: ListTile(
isThreeLine: _itemType == _MaterialListType.threeLine,
dense: _dense,
leading: _showAvatars
? ExcludeSemantics(child: CircleAvatar(child: Text(item)))
: null,
title: Text('This item represents $item.'),
subtitle: secondary,
trailing: _showIcons
? Icon(Icons.info, color: Theme.of(context).disabledColor)
: null,
),
);
}
@override
Widget build(BuildContext context) {
final String layoutText = _dense ? ' \u2013 Dense' : '';
String itemTypeText;
switch (_itemType) {
case _MaterialListType.oneLine:
case _MaterialListType.oneLineWithAvatar:
itemTypeText = 'Single-line';
break;
case _MaterialListType.twoLine:
itemTypeText = 'Two-line';
break;
case _MaterialListType.threeLine:
itemTypeText = 'Three-line';
break;
}
Iterable<Widget> listTiles =
items.map<Widget>((String item) => buildListTile(context, item));
if (_showDividers)
listTiles = ListTile.divideTiles(context: context, tiles: listTiles);
return Scaffold(
key: scaffoldKey,
appBar: AppBar(
title: Text('Scrolling list\n$itemTypeText$layoutText'),
actions: <Widget>[
MaterialDemoDocumentationButton(ListDemo.routeName),
IconButton(
icon: const Icon(Icons.sort_by_alpha),
tooltip: 'Sort',
onPressed: () {
setState(() {
_reverseSort = !_reverseSort;
items.sort((String a, String b) =>
_reverseSort ? b.compareTo(a) : a.compareTo(b));
});
},
),
IconButton(
icon: Icon(
Theme.of(context).platform == TargetPlatform.iOS
? Icons.more_horiz
: Icons.more_vert,
),
tooltip: 'Show menu',
onPressed: _bottomSheet == null ? _showConfigurationSheet : null,
),
],
),
body: Scrollbar(
child: ListView(
padding: EdgeInsets.symmetric(vertical: _dense ? 4.0 : 8.0),
children: listTiles.toList(),
),
),
);
}
}

View File

@@ -0,0 +1,39 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
export 'backdrop_demo.dart';
export 'bottom_app_bar_demo.dart';
export 'bottom_navigation_demo.dart';
export 'material_button_demo.dart';
export 'cards_demo.dart';
export 'chip_demo.dart';
export 'data_table_demo.dart';
export 'date_and_time_picker_demo.dart';
export 'dialog_demo.dart';
export 'drawer_demo.dart';
export 'editable_text_demo.dart';
export 'elevation_demo.dart';
export 'expansion_panels_demo.dart';
export 'grid_list_demo.dart';
export 'icons_demo.dart';
export 'leave_behind_demo.dart';
export 'list_demo.dart';
export 'menu_demo.dart';
export 'modal_bottom_sheet_demo.dart';
export 'overscroll_demo.dart';
export 'page_selector_demo.dart';
export 'persistent_bottom_sheet_demo.dart';
export 'progress_indicator_demo.dart';
export 'reorderable_list_demo.dart';
export 'scrollable_tabs_demo.dart';
export 'search_demo.dart';
export 'selection_controls_demo.dart';
export 'slider_demo.dart';
export 'snack_bar_demo.dart';
export 'tabs_demo.dart';
export 'tabs_fab_demo.dart';
export 'text_demo.dart';
export 'text_form_field_demo.dart';
export 'tooltip_demo.dart';
export 'two_level_list_demo.dart';

View File

@@ -0,0 +1,103 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
class ButtonsDemo extends StatelessWidget {
static const String routeName = '/material/buttons';
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
@override
Widget build(BuildContext context) {
IconData _backIcon() {
switch (Theme.of(context).platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
return Icons.arrow_back;
case TargetPlatform.iOS:
return Icons.arrow_back_ios;
}
assert(false);
return null;
}
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
leading: IconButton(
icon: Icon(_backIcon()),
alignment: Alignment.centerLeft,
tooltip: 'Back',
onPressed: () {
Navigator.pop(context);
},
),
title: const Text('Material buttons'),
actions: <Widget>[
MaterialDemoDocumentationButton(ButtonsDemo.routeName)
],
),
body: Center(
child: _buildButtons(),
),
);
}
Widget _buildButtons() {
return Column(
children: [
pad(MaterialButton(
onPressed: () {
print('MaterialButton pressed');
},
elevation: 3.0,
child: Text('MaterialButton'),
)),
pad(FlatButton(
onPressed: () {
print('FlatButton pressed');
},
child: Text('FlatButton'),
)),
pad(RaisedButton(
onPressed: () {},
elevation: 0.0,
child: Text('RaisedButton 0.0'),
)),
pad(RaisedButton(
onPressed: () {},
elevation: 1.0,
child: Text('RaisedButton 1.0'),
)),
pad(RaisedButton(
onPressed: () {},
elevation: 2.0,
child: Text('RaisedButton 2.0'),
)),
pad(RaisedButton(
onPressed: () {},
elevation: 3.0,
child: Text('RaisedButton 3.0'),
)),
pad(RaisedButton(
onPressed: () {},
elevation: 4.0,
child: Text('RaisedButton 4.0'),
)),
pad(RaisedButton(
onPressed: () {},
elevation: 8.0,
child: Text('RaisedButton 8.0'),
)),
],
);
}
}
Padding pad(Widget widget) => Padding(
padding: EdgeInsets.all(10.0),
child: widget,
);

View File

@@ -0,0 +1,181 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
class MenuDemo extends StatefulWidget {
const MenuDemo({Key key}) : super(key: key);
static const String routeName = '/material/menu';
@override
MenuDemoState createState() => MenuDemoState();
}
class MenuDemoState extends State<MenuDemo> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final String _simpleValue1 = 'Menu item value one';
final String _simpleValue2 = 'Menu item value two';
final String _simpleValue3 = 'Menu item value three';
String _simpleValue;
final String _checkedValue1 = 'One';
final String _checkedValue2 = 'Two';
final String _checkedValue3 = 'Free';
final String _checkedValue4 = 'Four';
List<String> _checkedValues;
@override
void initState() {
super.initState();
_simpleValue = _simpleValue2;
_checkedValues = <String>[_checkedValue3];
}
void showInSnackBar(String value) {
_scaffoldKey.currentState.showSnackBar(SnackBar(content: Text(value)));
}
void showMenuSelection(String value) {
if (<String>[_simpleValue1, _simpleValue2, _simpleValue3].contains(value))
_simpleValue = value;
showInSnackBar('You selected: $value');
}
void showCheckedMenuSelections(String value) {
if (_checkedValues.contains(value))
_checkedValues.remove(value);
else
_checkedValues.add(value);
showInSnackBar('Checked $_checkedValues');
}
bool isChecked(String value) => _checkedValues.contains(value);
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: const Text('Menus'),
actions: <Widget>[
MaterialDemoDocumentationButton(MenuDemo.routeName),
PopupMenuButton<String>(
onSelected: showMenuSelection,
itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
const PopupMenuItem<String>(
value: 'Toolbar menu', child: Text('Toolbar menu')),
const PopupMenuItem<String>(
value: 'Right here', child: Text('Right here')),
const PopupMenuItem<String>(
value: 'Hooray!', child: Text('Hooray!')),
],
),
],
),
body: ListView(padding: kMaterialListPadding, children: <Widget>[
// Pressing the PopupMenuButton on the right of this item shows
// a simple menu with one disabled item. Typically the contents
// of this "contextual menu" would reflect the app's state.
ListTile(
title: const Text('An item with a context menu button'),
trailing: PopupMenuButton<String>(
padding: EdgeInsets.zero,
onSelected: showMenuSelection,
itemBuilder: (BuildContext context) =>
<PopupMenuItem<String>>[
PopupMenuItem<String>(
value: _simpleValue1,
child: const Text('Context menu item one')),
const PopupMenuItem<String>(
enabled: false,
child: Text('A disabled menu item')),
PopupMenuItem<String>(
value: _simpleValue3,
child: const Text('Context menu item three')),
])),
// Pressing the PopupMenuButton on the right of this item shows
// a menu whose items have text labels and icons and a divider
// That separates the first three items from the last one.
ListTile(
title: const Text('An item with a sectioned menu'),
trailing: PopupMenuButton<String>(
padding: EdgeInsets.zero,
onSelected: showMenuSelection,
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[
const PopupMenuItem<String>(
value: 'Preview',
child: ListTile(
leading: Icon(Icons.visibility),
title: Text('Preview'))),
const PopupMenuItem<String>(
value: 'Share',
child: ListTile(
leading: Icon(Icons.person_add),
title: Text('Share'))),
const PopupMenuItem<String>(
value: 'Get Link',
child: ListTile(
leading: Icon(Icons.link),
title: Text('Get link'))),
const PopupMenuDivider(),
const PopupMenuItem<String>(
value: 'Remove',
child: ListTile(
leading: Icon(Icons.delete),
title: Text('Remove')))
])),
// This entire list item is a PopupMenuButton. Tapping anywhere shows
// a menu whose current value is highlighted and aligned over the
// list item's center line.
PopupMenuButton<String>(
padding: EdgeInsets.zero,
initialValue: _simpleValue,
onSelected: showMenuSelection,
child: ListTile(
title: const Text('An item with a simple menu'),
subtitle: Text(_simpleValue)),
itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
PopupMenuItem<String>(
value: _simpleValue1, child: Text(_simpleValue1)),
PopupMenuItem<String>(
value: _simpleValue2, child: Text(_simpleValue2)),
PopupMenuItem<String>(
value: _simpleValue3, child: Text(_simpleValue3))
]),
// Pressing the PopupMenuButton on the right of this item shows a menu
// whose items have checked icons that reflect this app's state.
ListTile(
title: const Text('An item with a checklist menu'),
trailing: PopupMenuButton<String>(
padding: EdgeInsets.zero,
onSelected: showCheckedMenuSelections,
itemBuilder: (BuildContext context) =>
<PopupMenuItem<String>>[
CheckedPopupMenuItem<String>(
value: _checkedValue1,
checked: isChecked(_checkedValue1),
child: Text(_checkedValue1)),
CheckedPopupMenuItem<String>(
value: _checkedValue2,
enabled: false,
checked: isChecked(_checkedValue2),
child: Text(_checkedValue2)),
CheckedPopupMenuItem<String>(
value: _checkedValue3,
checked: isChecked(_checkedValue3),
child: Text(_checkedValue3)),
CheckedPopupMenuItem<String>(
value: _checkedValue4,
checked: isChecked(_checkedValue4),
child: Text(_checkedValue4))
]))
]));
}
}

View File

@@ -0,0 +1,38 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
class ModalBottomSheetDemo extends StatelessWidget {
static const String routeName = '/material/modal-bottom-sheet';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Modal bottom sheet'),
actions: <Widget>[MaterialDemoDocumentationButton(routeName)],
),
body: Center(
child: RaisedButton(
child: const Text('SHOW BOTTOM SHEET'),
onPressed: () {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return Container(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Text(
'This is the modal bottom sheet. Tap anywhere to dismiss.',
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).accentColor,
fontSize: 24.0))));
});
})));
}
}

View File

@@ -0,0 +1,92 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
enum IndicatorType { overscroll, refresh }
class OverscrollDemo extends StatefulWidget {
const OverscrollDemo({Key key}) : super(key: key);
static const String routeName = '/material/overscroll';
@override
OverscrollDemoState createState() => OverscrollDemoState();
}
class OverscrollDemoState extends State<OverscrollDemo> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
GlobalKey<RefreshIndicatorState>();
static final List<String> _items = <String>[
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N'
];
Future<void> _handleRefresh() {
final Completer<void> completer = Completer<void>();
Timer(const Duration(seconds: 3), () {
completer.complete();
});
return completer.future.then<void>((_) {
_scaffoldKey.currentState?.showSnackBar(SnackBar(
content: const Text('Refresh complete'),
action: SnackBarAction(
label: 'RETRY',
onPressed: () {
_refreshIndicatorKey.currentState.show();
})));
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(title: const Text('Pull to refresh'), actions: <Widget>[
MaterialDemoDocumentationButton(OverscrollDemo.routeName),
IconButton(
icon: const Icon(Icons.refresh),
tooltip: 'Refresh',
onPressed: () {
_refreshIndicatorKey.currentState.show();
}),
]),
body: RefreshIndicator(
key: _refreshIndicatorKey,
onRefresh: _handleRefresh,
child: ListView.builder(
padding: kMaterialListPadding,
itemCount: _items.length,
itemBuilder: (BuildContext context, int index) {
final String item = _items[index];
return ListTile(
isThreeLine: true,
leading: CircleAvatar(child: Text(item)),
title: Text('This item represents $item.'),
subtitle: const Text(
'Even more additional list item information appears on line three.'),
);
},
),
),
);
}
}

View File

@@ -0,0 +1,98 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
class _PageSelector extends StatelessWidget {
const _PageSelector({this.icons});
final List<Icon> icons;
void _handleArrowButtonPress(BuildContext context, int delta) {
final TabController controller = DefaultTabController.of(context);
if (!controller.indexIsChanging)
controller
.animateTo((controller.index + delta).clamp(0, icons.length - 1));
}
@override
Widget build(BuildContext context) {
final TabController controller = DefaultTabController.of(context);
final Color color = Theme.of(context).accentColor;
return SafeArea(
top: false,
bottom: false,
child: Column(
children: <Widget>[
Container(
margin: const EdgeInsets.only(top: 16.0),
child: Row(children: <Widget>[
IconButton(
icon: const Icon(Icons.chevron_left),
color: color,
onPressed: () {
_handleArrowButtonPress(context, -1);
},
tooltip: 'Page back'),
TabPageSelector(controller: controller),
IconButton(
icon: const Icon(Icons.chevron_right),
color: color,
onPressed: () {
_handleArrowButtonPress(context, 1);
},
tooltip: 'Page forward')
], mainAxisAlignment: MainAxisAlignment.spaceBetween)),
Expanded(
child: IconTheme(
data: IconThemeData(
size: 128.0,
color: color,
),
child: TabBarView(
children: icons.map<Widget>((Icon icon) {
return Container(
padding: const EdgeInsets.all(12.0),
child: Card(
child: Center(
child: icon,
),
),
);
}).toList()),
),
),
],
),
);
}
}
class PageSelectorDemo extends StatelessWidget {
static const String routeName = '/material/page-selector';
static final List<Icon> icons = <Icon>[
const Icon(Icons.event, semanticLabel: 'Event'),
const Icon(Icons.home, semanticLabel: 'Home'),
const Icon(Icons.android, semanticLabel: 'Android'),
const Icon(Icons.alarm, semanticLabel: 'Alarm'),
const Icon(Icons.face, semanticLabel: 'Face'),
const Icon(Icons.language, semanticLabel: 'Language'),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Page selector'),
actions: <Widget>[MaterialDemoDocumentationButton(routeName)],
),
body: DefaultTabController(
length: icons.length,
child: _PageSelector(icons: icons),
),
);
}
}

View File

@@ -0,0 +1,103 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
class PersistentBottomSheetDemo extends StatefulWidget {
static const String routeName = '/material/persistent-bottom-sheet';
@override
_PersistentBottomSheetDemoState createState() =>
_PersistentBottomSheetDemoState();
}
class _PersistentBottomSheetDemoState extends State<PersistentBottomSheetDemo> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
VoidCallback _showBottomSheetCallback;
@override
void initState() {
super.initState();
_showBottomSheetCallback = _showBottomSheet;
}
void _showBottomSheet() {
setState(() {
// disable the button
_showBottomSheetCallback = null;
});
_scaffoldKey.currentState
.showBottomSheet<Null>((BuildContext context) {
final ThemeData themeData = Theme.of(context);
return Container(
decoration: BoxDecoration(
border:
Border(top: BorderSide(color: themeData.disabledColor))),
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Text(
'This is a Material persistent bottom sheet. Drag downwards to dismiss it.',
textAlign: TextAlign.center,
style: TextStyle(color: themeData.accentColor, fontSize: 24.0),
),
),
);
})
.closed
.whenComplete(() {
if (mounted) {
setState(() {
// re-enable the button
_showBottomSheetCallback = _showBottomSheet;
});
}
});
}
void _showMessage() {
showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: const Text('You tapped the floating action button.'),
actions: <Widget>[
FlatButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('OK'))
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: const Text('Persistent bottom sheet'),
actions: <Widget>[
MaterialDemoDocumentationButton(
PersistentBottomSheetDemo.routeName),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _showMessage,
backgroundColor: Colors.redAccent,
child: const Icon(
Icons.add,
semanticLabel: 'Add',
),
),
body: Center(
child: RaisedButton(
onPressed: _showBottomSheetCallback,
child: const Text('SHOW BOTTOM SHEET'))));
}
}

View File

@@ -0,0 +1,132 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
class ProgressIndicatorDemo extends StatefulWidget {
static const String routeName = '/material/progress-indicator';
@override
_ProgressIndicatorDemoState createState() => _ProgressIndicatorDemoState();
}
class _ProgressIndicatorDemoState extends State<ProgressIndicatorDemo>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
animationBehavior: AnimationBehavior.preserve,
)..forward();
_animation = CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.9, curve: Curves.fastOutSlowIn),
reverseCurve: Curves.fastOutSlowIn)
..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.dismissed)
_controller.forward();
else if (status == AnimationStatus.completed) _controller.reverse();
});
}
@override
void dispose() {
_controller.stop();
super.dispose();
}
void _handleTap() {
setState(() {
// valueAnimation.isAnimating is part of our build state
if (_controller.isAnimating) {
_controller.stop();
} else {
switch (_controller.status) {
case AnimationStatus.dismissed:
case AnimationStatus.forward:
_controller.forward();
break;
case AnimationStatus.reverse:
case AnimationStatus.completed:
_controller.reverse();
break;
}
}
});
}
Widget _buildIndicators(BuildContext context, Widget child) {
final List<Widget> indicators = <Widget>[
const SizedBox(width: 200.0, child: LinearProgressIndicator()),
const LinearProgressIndicator(),
const LinearProgressIndicator(),
LinearProgressIndicator(value: _animation.value),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
const CircularProgressIndicator(),
SizedBox(
width: 20.0,
height: 20.0,
child: CircularProgressIndicator(value: _animation.value)),
SizedBox(
width: 100.0,
height: 20.0,
child: Text('${(_animation.value * 100.0).toStringAsFixed(1)}%',
textAlign: TextAlign.right),
),
],
),
];
return Column(
children: indicators
.map<Widget>((Widget c) => Container(
child: c,
margin:
const EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0)))
.toList(),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Progress indicators'),
actions: <Widget>[
MaterialDemoDocumentationButton(ProgressIndicatorDemo.routeName)
],
),
body: Center(
child: SingleChildScrollView(
child: DefaultTextStyle(
style: Theme.of(context).textTheme.title,
child: GestureDetector(
onTap: _handleTap,
behavior: HitTestBehavior.opaque,
child: SafeArea(
top: false,
bottom: false,
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 12.0, horizontal: 8.0),
child: AnimatedBuilder(
animation: _animation, builder: _buildIndicators),
),
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,219 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/foundation.dart';
import 'package:flutter_web/material.dart';
import 'package:flutter_web/rendering.dart';
import '../../gallery/demo.dart';
enum _ReorderableListType {
/// A list tile that contains a [CircleAvatar].
horizontalAvatar,
/// A list tile that contains a [CircleAvatar].
verticalAvatar,
/// A list tile that contains three lines of text and a checkbox.
threeLine,
}
class ReorderableListDemo extends StatefulWidget {
const ReorderableListDemo({Key key}) : super(key: key);
static const String routeName = '/material/reorderable-list';
@override
_ListDemoState createState() => _ListDemoState();
}
class _ListItem {
_ListItem(this.value, this.checkState);
final String value;
bool checkState;
}
class _ListDemoState extends State<ReorderableListDemo> {
static final GlobalKey<ScaffoldState> scaffoldKey =
GlobalKey<ScaffoldState>();
PersistentBottomSheetController<void> _bottomSheet;
_ReorderableListType _itemType = _ReorderableListType.threeLine;
bool _reverseSort = false;
final List<_ListItem> _items = <String>[
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
].map<_ListItem>((String item) => _ListItem(item, false)).toList();
void changeItemType(_ReorderableListType type) {
setState(() {
_itemType = type;
});
// Rebuild the bottom sheet to reflect the selected list view.
_bottomSheet?.setState(() {});
// Close the bottom sheet to give the user a clear view of the list.
_bottomSheet?.close();
}
void _showConfigurationSheet() {
setState(() {
_bottomSheet = scaffoldKey.currentState
.showBottomSheet<void>((BuildContext bottomSheetContext) {
return DecoratedBox(
decoration: const BoxDecoration(
border: Border(top: BorderSide(color: Colors.black26)),
),
child: ListView(
shrinkWrap: true,
primary: false,
children: <Widget>[
RadioListTile<_ReorderableListType>(
dense: true,
title: const Text('Horizontal Avatars'),
value: _ReorderableListType.horizontalAvatar,
groupValue: _itemType,
onChanged: changeItemType,
),
RadioListTile<_ReorderableListType>(
dense: true,
title: const Text('Vertical Avatars'),
value: _ReorderableListType.verticalAvatar,
groupValue: _itemType,
onChanged: changeItemType,
),
RadioListTile<_ReorderableListType>(
dense: true,
title: const Text('Three-line'),
value: _ReorderableListType.threeLine,
groupValue: _itemType,
onChanged: changeItemType,
),
],
),
);
});
// Garbage collect the bottom sheet when it closes.
_bottomSheet.closed.whenComplete(() {
if (mounted) {
setState(() {
_bottomSheet = null;
});
}
});
});
}
Widget buildListTile(_ListItem item) {
const Widget secondary = Text(
'Even more additional list item information appears on line three.',
);
Widget listTile;
switch (_itemType) {
case _ReorderableListType.threeLine:
listTile = CheckboxListTile(
key: Key(item.value),
isThreeLine: true,
value: item.checkState ?? false,
onChanged: (bool newValue) {
setState(() {
item.checkState = newValue;
});
},
title: Text('This item represents ${item.value}.'),
subtitle: secondary,
secondary: const Icon(Icons.drag_handle),
);
break;
case _ReorderableListType.horizontalAvatar:
case _ReorderableListType.verticalAvatar:
listTile = Container(
key: Key(item.value),
height: 100.0,
width: 100.0,
child: CircleAvatar(
child: Text(item.value),
backgroundColor: Colors.green,
),
);
break;
}
return listTile;
}
void _onReorder(int oldIndex, int newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final _ListItem item = _items.removeAt(oldIndex);
_items.insert(newIndex, item);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: scaffoldKey,
appBar: AppBar(
title: const Text('Reorderable list'),
actions: <Widget>[
MaterialDemoDocumentationButton(ReorderableListDemo.routeName),
IconButton(
icon: const Icon(Icons.sort_by_alpha),
tooltip: 'Sort',
onPressed: () {
setState(() {
_reverseSort = !_reverseSort;
_items.sort((_ListItem a, _ListItem b) => _reverseSort
? b.value.compareTo(a.value)
: a.value.compareTo(b.value));
});
},
),
IconButton(
icon: Icon(
Theme.of(context).platform == TargetPlatform.iOS
? Icons.more_horiz
: Icons.more_vert,
),
tooltip: 'Show menu',
onPressed: _bottomSheet == null ? _showConfigurationSheet : null,
),
],
),
body: Scrollbar(
child: ReorderableListView(
header: _itemType != _ReorderableListType.threeLine
? Padding(
padding: const EdgeInsets.all(8.0),
child: Text('Header of the list',
style: Theme.of(context).textTheme.headline))
: null,
onReorder: _onReorder,
scrollDirection: _itemType == _ReorderableListType.horizontalAvatar
? Axis.horizontal
: Axis.vertical,
padding: const EdgeInsets.symmetric(vertical: 8.0),
children: _items.map<Widget>(buildListTile).toList(),
),
),
);
}
}

View File

@@ -0,0 +1,195 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
enum TabsDemoStyle { iconsAndText, iconsOnly, textOnly }
class _Page {
const _Page({this.icon, this.text});
final IconData icon;
final String text;
}
const List<_Page> _allPages = <_Page>[
_Page(icon: Icons.grade, text: 'TRIUMPH'),
_Page(icon: Icons.playlist_add, text: 'NOTE'),
_Page(icon: Icons.check_circle, text: 'SUCCESS'),
_Page(icon: Icons.question_answer, text: 'OVERSTATE'),
_Page(icon: Icons.sentiment_very_satisfied, text: 'SATISFACTION'),
_Page(icon: Icons.camera, text: 'APERTURE'),
_Page(icon: Icons.assignment_late, text: 'WE MUST'),
_Page(icon: Icons.assignment_turned_in, text: 'WE CAN'),
_Page(icon: Icons.group, text: 'ALL'),
_Page(icon: Icons.block, text: 'EXCEPT'),
_Page(icon: Icons.sentiment_very_dissatisfied, text: 'CRYING'),
_Page(icon: Icons.error, text: 'MISTAKE'),
_Page(icon: Icons.loop, text: 'TRYING'),
_Page(icon: Icons.cake, text: 'CAKE'),
];
class ScrollableTabsDemo extends StatefulWidget {
static const String routeName = '/material/scrollable-tabs';
@override
ScrollableTabsDemoState createState() => ScrollableTabsDemoState();
}
class ScrollableTabsDemoState extends State<ScrollableTabsDemo>
with SingleTickerProviderStateMixin {
TabController _controller;
TabsDemoStyle _demoStyle = TabsDemoStyle.iconsAndText;
bool _customIndicator = false;
@override
void initState() {
super.initState();
_controller = TabController(vsync: this, length: _allPages.length);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void changeDemoStyle(TabsDemoStyle style) {
setState(() {
_demoStyle = style;
});
}
Decoration getIndicator() {
if (!_customIndicator) return const UnderlineTabIndicator();
switch (_demoStyle) {
case TabsDemoStyle.iconsAndText:
return ShapeDecoration(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(4.0)),
side: BorderSide(
color: Colors.white24,
width: 2.0,
),
) +
const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(4.0)),
side: BorderSide(
color: Colors.transparent,
width: 4.0,
),
),
);
case TabsDemoStyle.iconsOnly:
return ShapeDecoration(
shape: const CircleBorder(
side: BorderSide(
color: Colors.white24,
width: 4.0,
),
) +
const CircleBorder(
side: BorderSide(
color: Colors.transparent,
width: 4.0,
),
),
);
case TabsDemoStyle.textOnly:
return ShapeDecoration(
shape: const StadiumBorder(
side: BorderSide(
color: Colors.white24,
width: 2.0,
),
) +
const StadiumBorder(
side: BorderSide(
color: Colors.transparent,
width: 4.0,
),
),
);
}
return null;
}
@override
Widget build(BuildContext context) {
final Color iconColor = Theme.of(context).accentColor;
return Scaffold(
appBar: AppBar(
title: const Text('Scrollable tabs'),
actions: <Widget>[
MaterialDemoDocumentationButton(ScrollableTabsDemo.routeName),
IconButton(
icon: const Icon(Icons.sentiment_very_satisfied),
onPressed: () {
setState(() {
_customIndicator = !_customIndicator;
});
},
),
PopupMenuButton<TabsDemoStyle>(
onSelected: changeDemoStyle,
itemBuilder: (BuildContext context) =>
<PopupMenuItem<TabsDemoStyle>>[
const PopupMenuItem<TabsDemoStyle>(
value: TabsDemoStyle.iconsAndText,
child: Text('Icons and text')),
const PopupMenuItem<TabsDemoStyle>(
value: TabsDemoStyle.iconsOnly,
child: Text('Icons only')),
const PopupMenuItem<TabsDemoStyle>(
value: TabsDemoStyle.textOnly, child: Text('Text only')),
],
),
],
bottom: TabBar(
controller: _controller,
isScrollable: true,
indicator: getIndicator(),
tabs: _allPages.map<Tab>((_Page page) {
assert(_demoStyle != null);
switch (_demoStyle) {
case TabsDemoStyle.iconsAndText:
return Tab(text: page.text, icon: Icon(page.icon));
case TabsDemoStyle.iconsOnly:
return Tab(icon: Icon(page.icon));
case TabsDemoStyle.textOnly:
return Tab(text: page.text);
}
return null;
}).toList(),
),
),
body: TabBarView(
controller: _controller,
children: _allPages.map<Widget>((_Page page) {
return SafeArea(
top: false,
bottom: false,
child: Container(
key: ObjectKey(page.icon),
padding: const EdgeInsets.all(12.0),
child: Card(
child: Center(
child: Icon(
page.icon,
color: iconColor,
size: 128.0,
semanticLabel: 'Placeholder for ${page.text} tab',
),
),
),
),
);
}).toList()),
);
}
}

View File

@@ -0,0 +1,295 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
class SearchDemo extends StatefulWidget {
static const String routeName = '/material/search';
@override
_SearchDemoState createState() => _SearchDemoState();
}
class _SearchDemoState extends State<SearchDemo> {
final _SearchDemoSearchDelegate _delegate = _SearchDemoSearchDelegate();
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
int _lastIntegerSelected;
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
leading: IconButton(
tooltip: 'Navigation menu',
icon: AnimatedIcon(
icon: AnimatedIcons.menu_arrow,
color: Colors.white,
progress: _delegate.transitionAnimation,
),
onPressed: () {
_scaffoldKey.currentState.openDrawer();
},
),
title: const Text('Numbers'),
actions: <Widget>[
IconButton(
tooltip: 'Search',
icon: const Icon(Icons.search),
onPressed: () async {
final int selected = await showSearch<int>(
context: context,
delegate: _delegate,
);
if (selected != null && selected != _lastIntegerSelected) {
setState(() {
_lastIntegerSelected = selected;
});
}
},
),
MaterialDemoDocumentationButton(SearchDemo.routeName),
IconButton(
tooltip: 'More (not implemented)',
icon: Icon(
Theme.of(context).platform == TargetPlatform.iOS
? Icons.more_horiz
: Icons.more_vert,
),
onPressed: () {},
),
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
MergeSemantics(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
Text('Press the '),
Tooltip(
message: 'search',
child: Icon(
Icons.search,
size: 18.0,
),
),
Text(' icon in the AppBar'),
],
),
const Text(
'and search for an integer between 0 and 100,000.'),
],
),
),
const SizedBox(height: 64.0),
Text('Last selected integer: ${_lastIntegerSelected ?? 'NONE'}.')
],
),
),
floatingActionButton: FloatingActionButton.extended(
tooltip: 'Back', // Tests depend on this label to exit the demo.
onPressed: () {
Navigator.of(context).pop();
},
label: const Text('Close demo'),
icon: const Icon(Icons.close),
),
drawer: Drawer(
child: Column(
children: <Widget>[
const UserAccountsDrawerHeader(
accountName: Text('Peter Widget'),
accountEmail: Text('peter.widget@example.com'),
currentAccountPicture: CircleAvatar(
backgroundImage: AssetImage(
'people/square/peter.png',
),
),
margin: EdgeInsets.zero,
),
MediaQuery.removePadding(
context: context,
// DrawerHeader consumes top MediaQuery padding.
removeTop: true,
child: const ListTile(
leading: Icon(Icons.payment),
title: Text('Placeholder'),
),
),
],
),
),
);
}
}
class _SearchDemoSearchDelegate extends SearchDelegate<int> {
final List<int> _data =
List<int>.generate(100001, (int i) => i).reversed.toList();
final List<int> _history = <int>[42607, 85604, 66374, 44, 174];
@override
Widget buildLeading(BuildContext context) {
return IconButton(
tooltip: 'Back',
icon: AnimatedIcon(
icon: AnimatedIcons.menu_arrow,
progress: transitionAnimation,
),
onPressed: () {
close(context, null);
},
);
}
@override
Widget buildSuggestions(BuildContext context) {
final Iterable<int> suggestions = query.isEmpty
? _history
: _data.where((int i) => '$i'.startsWith(query));
return _SuggestionList(
query: query,
suggestions: suggestions.map<String>((int i) => '$i').toList(),
onSelected: (String suggestion) {
query = suggestion;
showResults(context);
},
);
}
@override
Widget buildResults(BuildContext context) {
final int searched = int.tryParse(query);
if (searched == null || !_data.contains(searched)) {
return Center(
child: Text(
'"$query"\n is not a valid integer between 0 and 100,000.\nTry again.',
textAlign: TextAlign.center,
),
);
}
return ListView(
children: <Widget>[
_ResultCard(
title: 'This integer',
integer: searched,
searchDelegate: this,
),
_ResultCard(
title: 'Next integer',
integer: searched + 1,
searchDelegate: this,
),
_ResultCard(
title: 'Previous integer',
integer: searched - 1,
searchDelegate: this,
),
],
);
}
@override
List<Widget> buildActions(BuildContext context) {
return <Widget>[
query.isEmpty
? IconButton(
tooltip: 'Voice Search',
icon: const Icon(Icons.mic),
onPressed: () {
query = 'TODO: implement voice input';
},
)
: IconButton(
tooltip: 'Clear',
icon: const Icon(Icons.clear),
onPressed: () {
query = '';
showSuggestions(context);
},
)
];
}
}
class _ResultCard extends StatelessWidget {
const _ResultCard({this.integer, this.title, this.searchDelegate});
final int integer;
final String title;
final SearchDelegate<int> searchDelegate;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return GestureDetector(
onTap: () {
searchDelegate.close(context, integer);
},
child: Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
Text(title),
Text(
'$integer',
style: theme.textTheme.headline.copyWith(fontSize: 72.0),
),
],
),
),
),
);
}
}
class _SuggestionList extends StatelessWidget {
const _SuggestionList({this.suggestions, this.query, this.onSelected});
final List<String> suggestions;
final String query;
final ValueChanged<String> onSelected;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return ListView.builder(
itemCount: suggestions.length,
itemBuilder: (BuildContext context, int i) {
final String suggestion = suggestions[i];
return ListTile(
leading: query.isEmpty ? const Icon(Icons.history) : const Icon(null),
title: RichText(
text: TextSpan(
text: suggestion.substring(0, query.length),
style:
theme.textTheme.subhead.copyWith(fontWeight: FontWeight.bold),
children: <TextSpan>[
TextSpan(
text: suggestion.substring(query.length),
style: theme.textTheme.subhead,
),
],
),
),
onTap: () {
onSelected(suggestion);
},
);
},
);
}
}

View File

@@ -0,0 +1,111 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
class SelectionControlsDemo extends StatefulWidget {
static const String routeName = '/material/selection';
_SelectionControlsDemoState createState() => _SelectionControlsDemoState();
}
class _SelectionControlsDemoState extends State<SelectionControlsDemo> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
bool checkboxValueA = true;
bool checkboxValueB = false;
bool checkboxValueC;
int radioValue = 0;
void handleRadioValueChanged(int value) {
setState(() {
radioValue = value;
});
}
@override
Widget build(BuildContext context) {
return wrapScaffold('Selection Controls', context, _scaffoldKey,
_buildContents(), SelectionControlsDemo.routeName);
}
Widget _buildContents() {
return Material(
color: Colors.white,
child: new Column(
children: <Widget>[buildCheckbox(), Divider(), buildRadio()]));
}
Widget buildCheckbox() {
return Align(
alignment: const Alignment(0.0, -0.2),
child: Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Checkbox(
value: checkboxValueA,
onChanged: (bool value) {
setState(() {
checkboxValueA = value;
});
},
),
Checkbox(
value: checkboxValueB,
onChanged: (bool value) {
setState(() {
checkboxValueB = value;
});
},
),
Checkbox(
value: checkboxValueC,
tristate: true,
onChanged: (bool value) {
setState(() {
checkboxValueC = value;
});
},
),
],
),
Row(mainAxisSize: MainAxisSize.min, children: const <Widget>[
// Disabled checkboxes
Checkbox(value: true, onChanged: null),
Checkbox(value: false, onChanged: null),
Checkbox(value: null, tristate: true, onChanged: null),
])
]));
}
Widget buildRadio() {
return Align(
alignment: const Alignment(0.0, -0.2),
child: Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
Row(mainAxisSize: MainAxisSize.min, children: <Widget>[
Radio<int>(
value: 0,
groupValue: radioValue,
onChanged: handleRadioValueChanged),
Radio<int>(
value: 1,
groupValue: radioValue,
onChanged: handleRadioValueChanged),
Radio<int>(
value: 2,
groupValue: radioValue,
onChanged: handleRadioValueChanged)
]),
// Disabled radio buttons
Row(mainAxisSize: MainAxisSize.min, children: const <Widget>[
Radio<int>(value: 0, groupValue: 0, onChanged: null),
Radio<int>(value: 1, groupValue: 0, onChanged: null),
Radio<int>(value: 2, groupValue: 0, onChanged: null)
])
]));
}
}

View File

@@ -0,0 +1,240 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
class SliderDemo extends StatefulWidget {
static const String routeName = '/material/slider';
@override
_SliderDemoState createState() => _SliderDemoState();
}
Path _triangle(double size, Offset thumbCenter, {bool invert = false}) {
final Path thumbPath = Path();
final double height = math.sqrt(3.0) / 2.0;
final double halfSide = size / 2.0;
final double centerHeight = size * height / 3.0;
final double sign = invert ? -1.0 : 1.0;
thumbPath.moveTo(
thumbCenter.dx - halfSide, thumbCenter.dy + sign * centerHeight);
thumbPath.lineTo(thumbCenter.dx, thumbCenter.dy - 2.0 * sign * centerHeight);
thumbPath.lineTo(
thumbCenter.dx + halfSide, thumbCenter.dy + sign * centerHeight);
thumbPath.close();
return thumbPath;
}
class _CustomThumbShape extends SliderComponentShape {
static const double _thumbSize = 4.0;
static const double _disabledThumbSize = 3.0;
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return isEnabled
? const Size.fromRadius(_thumbSize)
: const Size.fromRadius(_disabledThumbSize);
}
static final Animatable<double> sizeTween = Tween<double>(
begin: _disabledThumbSize,
end: _thumbSize,
);
@override
void paint(
PaintingContext context,
Offset thumbCenter, {
Animation<double> activationAnimation,
Animation<double> enableAnimation,
bool isDiscrete,
TextPainter labelPainter,
RenderBox parentBox,
SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
}) {
final Canvas canvas = context.canvas;
final ColorTween colorTween = ColorTween(
begin: sliderTheme.disabledThumbColor,
end: sliderTheme.thumbColor,
);
final double size = _thumbSize * sizeTween.evaluate(enableAnimation);
final Path thumbPath = _triangle(size, thumbCenter);
canvas.drawPath(
thumbPath, Paint()..color = colorTween.evaluate(enableAnimation));
}
}
class _CustomValueIndicatorShape extends SliderComponentShape {
static const double _indicatorSize = 4.0;
static const double _disabledIndicatorSize = 3.0;
static const double _slideUpHeight = 40.0;
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return Size.fromRadius(isEnabled ? _indicatorSize : _disabledIndicatorSize);
}
static final Animatable<double> sizeTween = Tween<double>(
begin: _disabledIndicatorSize,
end: _indicatorSize,
);
@override
void paint(
PaintingContext context,
Offset thumbCenter, {
Animation<double> activationAnimation,
Animation<double> enableAnimation,
bool isDiscrete,
TextPainter labelPainter,
RenderBox parentBox,
SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
}) {
final Canvas canvas = context.canvas;
final ColorTween enableColor = ColorTween(
begin: sliderTheme.disabledThumbColor,
end: sliderTheme.valueIndicatorColor,
);
final Tween<double> slideUpTween = Tween<double>(
begin: 0.0,
end: _slideUpHeight,
);
final double size = _indicatorSize * sizeTween.evaluate(enableAnimation);
final Offset slideUpOffset =
Offset(0.0, -slideUpTween.evaluate(activationAnimation));
final Path thumbPath = _triangle(
size,
thumbCenter + slideUpOffset,
invert: true,
);
final Color paintColor = enableColor
.evaluate(enableAnimation)
.withAlpha((255.0 * activationAnimation.value).round());
canvas.drawPath(
thumbPath,
Paint()..color = paintColor,
);
canvas.drawLine(
thumbCenter,
thumbCenter + slideUpOffset,
Paint()
..color = paintColor
..style = PaintingStyle.stroke
..strokeWidth = 2.0);
labelPainter.paint(
canvas,
thumbCenter +
slideUpOffset +
Offset(-labelPainter.width / 2.0, -labelPainter.height - 4.0));
}
}
class _SliderDemoState extends State<SliderDemo> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
double _value = 25.0;
double _discreteValue = 40.0;
@override
Widget build(BuildContext context) {
return wrapScaffold('Slider Demo', context, _scaffoldKey,
_buildContents(context), SliderDemo.routeName);
}
Widget _buildContents(BuildContext context) {
final ThemeData theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 40.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Slider(
value: _value,
min: 0.0,
max: 100.0,
onChanged: (double value) {
setState(() {
_value = value;
});
},
),
const Text('Continuous'),
],
),
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Slider(value: 0.25, onChanged: (double val) {}),
Text('Disabled'),
],
),
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Slider(
value: _discreteValue,
min: 0.0,
max: 200.0,
divisions: 5,
label: '${_discreteValue.round()}',
onChanged: (double value) {
setState(() {
_discreteValue = value;
});
},
),
const Text('Discrete'),
],
),
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SliderTheme(
data: theme.sliderTheme.copyWith(
activeTrackColor: Colors.deepPurple,
inactiveTrackColor: Colors.black26,
activeTickMarkColor: Colors.white70,
inactiveTickMarkColor: Colors.black,
overlayColor: Colors.black12,
thumbColor: Colors.deepPurple,
valueIndicatorColor: Colors.deepPurpleAccent,
thumbShape: _CustomThumbShape(),
valueIndicatorShape: _CustomValueIndicatorShape(),
valueIndicatorTextStyle: theme.accentTextTheme.body2
.copyWith(color: Colors.black87),
),
child: Slider(
value: _discreteValue,
min: 0.0,
max: 200.0,
divisions: 5,
semanticFormatterCallback: (double value) =>
value.round().toString(),
label: '${_discreteValue.round()}',
onChanged: (double value) {
setState(() {
_discreteValue = value;
});
},
),
),
const Text('Discrete with Custom Theme'),
],
),
],
),
);
}
}

View File

@@ -0,0 +1,83 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
const String _text1 =
'Snackbars provide lightweight feedback about an operation by '
'showing a brief message at the bottom of the screen. Snackbars '
'can contain an action.';
const String _text2 =
'Snackbars should contain a single line of text directly related '
'to the operation performed. They cannot contain icons.';
const String _text3 =
'By default snackbars automatically disappear after a few seconds ';
class SnackBarDemo extends StatefulWidget {
const SnackBarDemo({Key key}) : super(key: key);
static const String routeName = '/material/snack-bar';
@override
_SnackBarDemoState createState() => _SnackBarDemoState();
}
class _SnackBarDemoState extends State<SnackBarDemo> {
int _snackBarIndex = 1;
Widget buildBody(BuildContext context) {
return SafeArea(
top: false,
bottom: false,
child: ListView(
padding: const EdgeInsets.all(24.0),
children: <Widget>[
const Text(_text1),
const Text(_text2),
Center(
child: Row(children: <Widget>[
RaisedButton(
child: const Text('SHOW A SNACKBAR'),
onPressed: () {
final int thisSnackBarIndex = _snackBarIndex++;
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('This is snackbar #$thisSnackBarIndex.'),
action: SnackBarAction(
label: 'ACTION',
onPressed: () {
Scaffold.of(context).showSnackBar(SnackBar(
content: Text(
'You pressed snackbar $thisSnackBarIndex\'s action.')));
}),
));
}),
]),
),
const Text(_text3),
].map<Widget>((Widget child) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 12.0),
child: child);
}).toList()),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Snackbar'),
actions: <Widget>[
MaterialDemoDocumentationButton(SnackBarDemo.routeName)
],
),
body: Builder(
// Create an inner BuildContext so that the snackBar onPressed methods
// can refer to the Scaffold with Scaffold.of().
builder: buildBody));
}
}

View File

@@ -0,0 +1,23 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
class StackDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
border: Border.all(
color: Colors.greenAccent,
width: 1.0,
),
),
child: Stack(children: [
Text('A'),
Text('B'),
]),
);
}
}

View File

@@ -0,0 +1,42 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
class SwitchDemo extends StatefulWidget {
static const routeName = '/material/switch';
@override
SwitchDemoState createState() => SwitchDemoState();
}
class SwitchDemoState extends State<SwitchDemo> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
@override
Widget build(BuildContext context) {
return wrapScaffold('Switch Demo', context, _scaffoldKey, _buildContents(),
SwitchDemo.routeName);
}
bool _value = true;
Widget _buildContents() {
return Material(
child: Column(
children: [
Switch(
value: _value,
onChanged: (bool newValue) {
setState(() {
_value = newValue;
});
}),
],
),
);
}
}

View File

@@ -0,0 +1,209 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Each TabBarView contains a _Page and for each _Page there is a list
// of _CardData objects. Each _CardData object is displayed by a _CardItem.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
class _Page {
_Page({this.label});
final String label;
String get id => label[0];
@override
String toString() => '$runtimeType("$label")';
}
class _CardData {
const _CardData({this.title, this.imageAsset, this.imageAssetPackage});
final String title;
final String imageAsset;
final String imageAssetPackage;
}
final Map<_Page, List<_CardData>> _allPages = <_Page, List<_CardData>>{
_Page(label: 'HOME'): <_CardData>[
const _CardData(
title: 'Flatwear',
imageAsset: 'products/flatwear.png',
imageAssetPackage: _kGalleryAssetsPackage,
),
const _CardData(
title: 'Pine Table',
imageAsset: 'products/table.png',
imageAssetPackage: _kGalleryAssetsPackage,
),
const _CardData(
title: 'Blue Cup',
imageAsset: 'products/cup.png',
imageAssetPackage: _kGalleryAssetsPackage,
),
const _CardData(
title: 'Tea Set',
imageAsset: 'products/teaset.png',
imageAssetPackage: _kGalleryAssetsPackage,
),
const _CardData(
title: 'Desk Set',
imageAsset: 'products/deskset.png',
imageAssetPackage: _kGalleryAssetsPackage,
),
const _CardData(
title: 'Blue Linen Napkins',
imageAsset: 'products/napkins.png',
imageAssetPackage: _kGalleryAssetsPackage,
),
const _CardData(
title: 'Planters',
imageAsset: 'products/planters.png',
imageAssetPackage: _kGalleryAssetsPackage,
),
const _CardData(
title: 'Kitchen Quattro',
imageAsset: 'products/kitchen_quattro.png',
imageAssetPackage: _kGalleryAssetsPackage,
),
const _CardData(
title: 'Platter',
imageAsset: 'products/platter.png',
imageAssetPackage: _kGalleryAssetsPackage,
),
],
_Page(label: 'APPAREL'): <_CardData>[
const _CardData(
title: 'Cloud-White Dress',
imageAsset: 'products/dress.png',
imageAssetPackage: _kGalleryAssetsPackage,
),
const _CardData(
title: 'Ginger Scarf',
imageAsset: 'products/scarf.png',
imageAssetPackage: _kGalleryAssetsPackage,
),
const _CardData(
title: 'Blush Sweats',
imageAsset: 'products/sweats.png',
imageAssetPackage: _kGalleryAssetsPackage,
),
],
};
class _CardDataItem extends StatelessWidget {
const _CardDataItem({this.page, this.data});
static const double height = 272.0;
final _Page page;
final _CardData data;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Align(
alignment:
page.id == 'H' ? Alignment.centerLeft : Alignment.centerRight,
child: CircleAvatar(child: Text('${page.id}')),
),
SizedBox(width: 144.0, height: 144.0, child: new Text('image')
// Image.asset(
// data.imageAsset,
// package: data.imageAssetPackage,
// fit: BoxFit.contain,
// ),
),
Center(
child: Text(
data.title,
style: Theme.of(context).textTheme.title,
),
),
],
),
),
);
}
}
class TabsDemo extends StatelessWidget {
static const String routeName = '/material/tabs';
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: _allPages.length,
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
title: const Text('Tabs and scrolling'),
actions: <Widget>[MaterialDemoDocumentationButton(routeName)],
pinned: true,
expandedHeight: 150.0,
forceElevated: innerBoxIsScrolled,
bottom: TabBar(
tabs: _allPages.keys
.map<Widget>(
(_Page page) => Tab(text: page.label),
)
.toList(),
),
),
];
},
body: TabBarView(
children: _allPages.keys.map<Widget>((_Page page) {
return SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (BuildContext context) {
return CustomScrollView(
key: PageStorageKey<_Page>(page),
slivers: <Widget>[
SliverPadding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
sliver: SliverFixedExtentList(
itemExtent: _CardDataItem.height,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
final _CardData data = _allPages[page][index];
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
),
child: _CardDataItem(
page: page,
data: data,
),
);
},
childCount: _allPages[page].length,
),
),
),
],
);
},
),
);
}).toList(),
),
),
),
);
}
}

View File

@@ -0,0 +1,150 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
const String _explanatoryText =
"When the Scaffold's floating action button changes, the new button fades and "
'turns into view. In this demo, changing tabs can cause the app to be rebuilt '
'with a FloatingActionButton that the Scaffold distinguishes from the others '
'by its key.';
class _Page {
_Page({this.label, this.colors, this.icon});
final String label;
final MaterialColor colors;
final IconData icon;
Color get labelColor =>
colors != null ? colors.shade300 : Colors.grey.shade300;
bool get fabDefined => colors != null && icon != null;
Color get fabColor => colors.shade400;
Icon get fabIcon => Icon(icon);
Key get fabKey => ValueKey<Color>(fabColor);
}
final List<_Page> _allPages = <_Page>[
_Page(label: 'Blue', colors: Colors.indigo, icon: Icons.add),
_Page(label: 'Eco', colors: Colors.green, icon: Icons.create),
_Page(label: 'No'),
_Page(label: 'Teal', colors: Colors.teal, icon: Icons.add),
_Page(label: 'Red', colors: Colors.red, icon: Icons.create),
];
class TabsFabDemo extends StatefulWidget {
static const String routeName = '/material/tabs-fab';
@override
_TabsFabDemoState createState() => _TabsFabDemoState();
}
class _TabsFabDemoState extends State<TabsFabDemo>
with SingleTickerProviderStateMixin {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
TabController _controller;
_Page _selectedPage;
bool _extendedButtons = false;
@override
void initState() {
super.initState();
_controller = TabController(vsync: this, length: _allPages.length);
_controller.addListener(_handleTabSelection);
_selectedPage = _allPages[0];
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _handleTabSelection() {
setState(() {
_selectedPage = _allPages[_controller.index];
});
}
void _showExplanatoryText() {
_scaffoldKey.currentState.showBottomSheet<Null>((BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: Theme.of(context).dividerColor))),
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Text(_explanatoryText,
style: Theme.of(context).textTheme.subhead)));
});
}
Widget buildTabView(_Page page) {
return Builder(builder: (BuildContext context) {
return Container(
key: ValueKey<String>(page.label),
padding: const EdgeInsets.fromLTRB(48.0, 48.0, 48.0, 96.0),
child: Card(
child: Center(
child: Text(page.label,
style: TextStyle(color: page.labelColor, fontSize: 32.0),
textAlign: TextAlign.center))));
});
}
Widget buildFloatingActionButton(_Page page) {
if (!page.fabDefined) return null;
if (_extendedButtons) {
return FloatingActionButton.extended(
key: ValueKey<Key>(page.fabKey),
tooltip: 'Show explanation',
backgroundColor: page.fabColor,
icon: page.fabIcon,
label: Text(page.label.toUpperCase()),
onPressed: _showExplanatoryText);
}
return FloatingActionButton(
key: page.fabKey,
tooltip: 'Show explanation',
backgroundColor: page.fabColor,
child: page.fabIcon,
onPressed: _showExplanatoryText);
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: const Text('FAB per tab'),
bottom: TabBar(
controller: _controller,
tabs: _allPages
.map<Widget>((_Page page) => Tab(text: page.label.toUpperCase()))
.toList(),
),
actions: <Widget>[
MaterialDemoDocumentationButton(TabsFabDemo.routeName),
IconButton(
icon: const Icon(Icons.sentiment_very_satisfied),
onPressed: () {
setState(() {
_extendedButtons = !_extendedButtons;
});
},
),
],
),
floatingActionButton: buildFloatingActionButton(_selectedPage),
body: TabBarView(
controller: _controller,
children: _allPages.map<Widget>(buildTabView).toList()),
);
}
}

View File

@@ -0,0 +1,52 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
class TextDemo extends StatelessWidget {
static const routeName = '/material/text';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Text'),
centerTitle: true,
),
body: ListView(
children: [
pad(Text('Single line of text')),
Divider(),
// Single line with many whitespaces in between.
pad(Text(' Text with a lot of whitespace ')),
Divider(),
// Forced multi-line because of the \n.
pad(Text('Text with a newline\ncharacter should render in 2 lines')),
Divider(),
// Multi-line with regular whitespace.
pad(Text(
'''Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas auctor
vel ligula eget fermentum. Integer mattis nulla vitae ullamcorper
dignissim. Donec vel velit vel eros lobortis laoreet at sit amet turpis.
Ut in orci blandit, rhoncus metus quis, finibus augue. Nullam a elit
venenatis metus accumsan dapibus. Vestibulum imperdiet tristique viverra.''',
)),
Divider(),
// Multi-line with a lot of whitespace in between.
pad(Text(
'''
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Maecenas auctor vel ligula eget fermentum.
Integer mattis nulla vitae ullamcorper dignissim.
Donec vel velit vel eros lobortis laoreet at sit amet turpis.''',
)),
Divider(),
],
),
);
}
Padding pad(Widget child) =>
Padding(padding: EdgeInsets.all(12), child: child);
}

View File

@@ -0,0 +1,341 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter_web/material.dart';
import 'package:flutter_web/services.dart';
import '../../gallery/demo.dart';
class TextFormFieldDemo extends StatefulWidget {
const TextFormFieldDemo({Key key}) : super(key: key);
static const String routeName = '/material/text-form-field';
@override
TextFormFieldDemoState createState() => TextFormFieldDemoState();
}
class PersonData {
String name = '';
String phoneNumber = '';
String email = '';
String password = '';
}
class PasswordField extends StatefulWidget {
const PasswordField({
this.fieldKey,
this.hintText,
this.labelText,
this.helperText,
this.onSaved,
this.validator,
this.onFieldSubmitted,
});
final Key fieldKey;
final String hintText;
final String labelText;
final String helperText;
final FormFieldSetter<String> onSaved;
final FormFieldValidator<String> validator;
final ValueChanged<String> onFieldSubmitted;
@override
_PasswordFieldState createState() => _PasswordFieldState();
}
class _PasswordFieldState extends State<PasswordField> {
bool _obscureText = true;
@override
Widget build(BuildContext context) {
return TextFormField(
key: widget.fieldKey,
obscureText: _obscureText,
maxLength: 8,
onSaved: widget.onSaved,
validator: widget.validator,
onFieldSubmitted: widget.onFieldSubmitted,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
filled: true,
hintText: widget.hintText,
labelText: widget.labelText,
helperText: widget.helperText,
suffixIcon: GestureDetector(
onTap: () {
setState(() {
_obscureText = !_obscureText;
});
},
child: Icon(
_obscureText ? Icons.visibility : Icons.visibility_off,
semanticLabel: _obscureText ? 'show password' : 'hide password',
),
),
),
);
}
}
class TextFormFieldDemoState extends State<TextFormFieldDemo> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
PersonData person = PersonData();
void showInSnackBar(String value) {
_scaffoldKey.currentState.showSnackBar(SnackBar(content: Text(value)));
}
bool _autovalidate = false;
bool _formWasEdited = false;
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final GlobalKey<FormFieldState<String>> _passwordFieldKey =
GlobalKey<FormFieldState<String>>();
final _UsNumberTextInputFormatter _phoneNumberFormatter =
_UsNumberTextInputFormatter();
void _handleSubmitted() {
final FormState form = _formKey.currentState;
if (!form.validate()) {
_autovalidate = true; // Start validating on every change.
showInSnackBar('Please fix the errors in red before submitting.');
} else {
form.save();
showInSnackBar('${person.name}\'s phone number is ${person.phoneNumber}');
}
}
String _validateName(String value) {
_formWasEdited = true;
if (value.isEmpty) return 'Name is required.';
final RegExp nameExp = RegExp(r'^[A-Za-z ]+$');
if (!nameExp.hasMatch(value))
return 'Please enter only alphabetical characters.';
return null;
}
String _validatePhoneNumber(String value) {
_formWasEdited = true;
final RegExp phoneExp = RegExp(r'^\(\d\d\d\) \d\d\d\-\d\d\d\d$');
if (!phoneExp.hasMatch(value))
return '(###) ###-#### - Enter a US phone number.';
return null;
}
String _validatePassword(String value) {
_formWasEdited = true;
final FormFieldState<String> passwordField = _passwordFieldKey.currentState;
if (passwordField.value == null || passwordField.value.isEmpty)
return 'Please enter a password.';
if (passwordField.value != value) return 'The passwords don\'t match';
return null;
}
Future<bool> _warnUserAboutInvalidData() async {
final FormState form = _formKey.currentState;
if (form == null || !_formWasEdited || form.validate()) return true;
return await showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('This form has errors'),
content: const Text('Really leave this form?'),
actions: <Widget>[
FlatButton(
child: const Text('YES'),
onPressed: () {
Navigator.of(context).pop(true);
},
),
FlatButton(
child: const Text('NO'),
onPressed: () {
Navigator.of(context).pop(false);
},
),
],
);
},
) ??
false;
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: const Text('Text fields'),
actions: <Widget>[
MaterialDemoDocumentationButton(TextFormFieldDemo.routeName)
],
),
body: SafeArea(
top: false,
bottom: false,
child: Form(
key: _formKey,
autovalidate: _autovalidate,
onWillPop: _warnUserAboutInvalidData,
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
const SizedBox(height: 24.0),
TextFormField(
textCapitalization: TextCapitalization.words,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
filled: true,
icon: Icon(Icons.person),
hintText: 'What do people call you?',
labelText: 'Name * ',
),
onSaved: (String value) {
person.name = value;
},
validator: _validateName,
),
const SizedBox(height: 24.0),
TextFormField(
decoration: const InputDecoration(
border: UnderlineInputBorder(),
filled: true,
icon: Icon(Icons.phone),
hintText: 'Where can we reach you?',
labelText: 'Phone Number * ',
prefixText: '+1',
),
keyboardType: TextInputType.phone,
onSaved: (String value) {
person.phoneNumber = value;
},
validator: _validatePhoneNumber,
// TextInputFormatters are applied in sequence.
inputFormatters: <TextInputFormatter>[
WhitelistingTextInputFormatter.digitsOnly,
// Fit the validating format.
_phoneNumberFormatter,
],
),
const SizedBox(height: 24.0),
TextFormField(
decoration: const InputDecoration(
border: UnderlineInputBorder(),
filled: true,
icon: Icon(Icons.email),
hintText: 'Your email address',
labelText: 'E-mail',
),
keyboardType: TextInputType.emailAddress,
onSaved: (String value) {
person.email = value;
},
),
const SizedBox(height: 24.0),
TextFormField(
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText:
'Tell us about yourself (e.g., write down what you do or what hobbies you have)',
helperText: 'Keep it short, this is just a demo.',
labelText: 'Life story',
),
maxLines: 3,
),
const SizedBox(height: 24.0),
TextFormField(
keyboardType: TextInputType.number,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Salary',
prefixText: '\$',
suffixText: 'USD',
suffixStyle: TextStyle(color: Colors.green)),
maxLines: 1,
),
const SizedBox(height: 24.0),
PasswordField(
fieldKey: _passwordFieldKey,
helperText: 'No more than 8 characters.',
labelText: 'Password *',
onFieldSubmitted: (String value) {
setState(() {
person.password = value;
});
},
),
const SizedBox(height: 24.0),
TextFormField(
enabled:
person.password != null && person.password.isNotEmpty,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
filled: true,
labelText: 'Re-type password',
),
maxLength: 8,
obscureText: true,
validator: _validatePassword,
),
const SizedBox(height: 24.0),
Center(
child: RaisedButton(
child: const Text('SUBMIT'),
onPressed: _handleSubmitted,
),
),
const SizedBox(height: 24.0),
Text('* indicates required field',
style: Theme.of(context).textTheme.caption),
const SizedBox(height: 24.0),
],
),
),
),
),
);
}
}
/// Format incoming numeric text to fit the format of (###) ###-#### ##...
class _UsNumberTextInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue, TextEditingValue newValue) {
final int newTextLength = newValue.text.length;
int selectionIndex = newValue.selection.end;
int usedSubstringIndex = 0;
final StringBuffer newText = StringBuffer();
if (newTextLength >= 1) {
newText.write('(');
if (newValue.selection.end >= 1) selectionIndex++;
}
if (newTextLength >= 4) {
newText.write(newValue.text.substring(0, usedSubstringIndex = 3) + ') ');
if (newValue.selection.end >= 3) selectionIndex += 2;
}
if (newTextLength >= 7) {
newText.write(newValue.text.substring(3, usedSubstringIndex = 6) + '-');
if (newValue.selection.end >= 6) selectionIndex++;
}
if (newTextLength >= 11) {
newText.write(newValue.text.substring(6, usedSubstringIndex = 10) + ' ');
if (newValue.selection.end >= 10) selectionIndex++;
}
// Dump the rest.
if (newTextLength >= usedSubstringIndex)
newText.write(newValue.text.substring(usedSubstringIndex));
return TextEditingValue(
text: newText.toString(),
selection: TextSelection.collapsed(offset: selectionIndex),
);
}
}

View File

@@ -0,0 +1,59 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
const String _introText =
'Tooltips are short identifying messages that briefly appear in response to '
'a long press. Tooltip messages are also used by services that make Flutter '
'apps accessible, like screen readers.';
class TooltipDemo extends StatelessWidget {
static const String routeName = '/material/tooltips';
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('Tooltips'),
actions: <Widget>[MaterialDemoDocumentationButton(routeName)],
),
body: Builder(builder: (BuildContext context) {
return SafeArea(
top: false,
bottom: false,
child: ListView(
children: <Widget>[
Text(_introText, style: theme.textTheme.subhead),
Row(children: <Widget>[
Text('Long press the ', style: theme.textTheme.subhead),
Tooltip(
message: 'call icon',
child: Icon(Icons.call,
size: 18.0, color: theme.iconTheme.color)),
Text(' icon.', style: theme.textTheme.subhead)
]),
Center(
child: IconButton(
iconSize: 48.0,
icon: const Icon(Icons.call),
color: theme.iconTheme.color,
tooltip: 'Place a phone call',
onPressed: () {
Scaffold.of(context).showSnackBar(const SnackBar(
content: Text('That was an ordinary tap.')));
}))
].map<Widget>((Widget widget) {
return Padding(
padding:
const EdgeInsets.only(top: 16.0, left: 16.0, right: 16.0),
child: widget);
}).toList()),
);
}));
}
}

View File

@@ -0,0 +1,34 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
class TwoLevelListDemo extends StatelessWidget {
static const String routeName = '/material/two-level-list';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Expand/collapse list control'),
actions: <Widget>[MaterialDemoDocumentationButton(routeName)],
),
body: ListView(children: <Widget>[
const ListTile(title: Text('Top')),
ExpansionTile(
title: const Text('Sublist'),
backgroundColor: Theme.of(context).accentColor.withOpacity(0.025),
children: const <Widget>[
ListTile(title: Text('One')),
ListTile(title: Text('Two')),
// https://en.wikipedia.org/wiki/Free_Four
ListTile(title: Text('Free')),
ListTile(title: Text('Four'))
]),
const ListTile(title: Text('Bottom'))
]));
}
}