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:
committed by
Andrew Brogdon
parent
42f2dce01b
commit
3fe927cb29
411
web/gallery/lib/demo/material/backdrop_demo.dart
Normal file
411
web/gallery/lib/demo/material/backdrop_demo.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
524
web/gallery/lib/demo/material/bottom_app_bar_demo.dart
Normal file
524
web/gallery/lib/demo/material/bottom_app_bar_demo.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
239
web/gallery/lib/demo/material/bottom_navigation_demo.dart
Normal file
239
web/gallery/lib/demo/material/bottom_navigation_demo.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
181
web/gallery/lib/demo/material/cards_demo.dart
Normal file
181
web/gallery/lib/demo/material/cards_demo.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
79
web/gallery/lib/demo/material/chip_demo.dart
Normal file
79
web/gallery/lib/demo/material/chip_demo.dart
Normal 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,
|
||||
);
|
||||
231
web/gallery/lib/demo/material/data_table_demo.dart
Normal file
231
web/gallery/lib/demo/material/data_table_demo.dart
Normal 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)
|
||||
]));
|
||||
}
|
||||
}
|
||||
230
web/gallery/lib/demo/material/date_and_time_picker_demo.dart
Normal file
230
web/gallery/lib/demo/material/date_and_time_picker_demo.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
211
web/gallery/lib/demo/material/dialog_demo.dart
Normal file
211
web/gallery/lib/demo/material/dialog_demo.dart
Normal 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()));
|
||||
}
|
||||
}
|
||||
197
web/gallery/lib/demo/material/drawer_demo.dart
Normal file
197
web/gallery/lib/demo/material/drawer_demo.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
92
web/gallery/lib/demo/material/editable_text_demo.dart
Normal file
92
web/gallery/lib/demo/material/editable_text_demo.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
69
web/gallery/lib/demo/material/elevation_demo.dart
Normal file
69
web/gallery/lib/demo/material/elevation_demo.dart
Normal 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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
335
web/gallery/lib/demo/material/expansion_panels_demo.dart
Normal file
335
web/gallery/lib/demo/material/expansion_panels_demo.dart
Normal 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()),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
232
web/gallery/lib/demo/material/full_screen_dialog_demo.dart
Normal file
232
web/gallery/lib/demo/material/full_screen_dialog_demo.dart
Normal 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())),
|
||||
);
|
||||
}
|
||||
}
|
||||
397
web/gallery/lib/demo/material/grid_list_demo.dart
Normal file
397
web/gallery/lib/demo/material/grid_list_demo.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
135
web/gallery/lib/demo/material/icons_demo.dart
Normal file
135
web/gallery/lib/demo/material/icons_demo.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
228
web/gallery/lib/demo/material/leave_behind_demo.dart
Normal file
228
web/gallery/lib/demo/material/leave_behind_demo.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
273
web/gallery/lib/demo/material/list_demo.dart
Normal file
273
web/gallery/lib/demo/material/list_demo.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
39
web/gallery/lib/demo/material/material.dart
Normal file
39
web/gallery/lib/demo/material/material.dart
Normal 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';
|
||||
103
web/gallery/lib/demo/material/material_button_demo.dart
Normal file
103
web/gallery/lib/demo/material/material_button_demo.dart
Normal 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,
|
||||
);
|
||||
181
web/gallery/lib/demo/material/menu_demo.dart
Normal file
181
web/gallery/lib/demo/material/menu_demo.dart
Normal 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))
|
||||
]))
|
||||
]));
|
||||
}
|
||||
}
|
||||
38
web/gallery/lib/demo/material/modal_bottom_sheet_demo.dart
Normal file
38
web/gallery/lib/demo/material/modal_bottom_sheet_demo.dart
Normal 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))));
|
||||
});
|
||||
})));
|
||||
}
|
||||
}
|
||||
92
web/gallery/lib/demo/material/overscroll_demo.dart
Normal file
92
web/gallery/lib/demo/material/overscroll_demo.dart
Normal 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.'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
98
web/gallery/lib/demo/material/page_selector_demo.dart
Normal file
98
web/gallery/lib/demo/material/page_selector_demo.dart
Normal 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
103
web/gallery/lib/demo/material/persistent_bottom_sheet_demo.dart
Normal file
103
web/gallery/lib/demo/material/persistent_bottom_sheet_demo.dart
Normal 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'))));
|
||||
}
|
||||
}
|
||||
132
web/gallery/lib/demo/material/progress_indicator_demo.dart
Normal file
132
web/gallery/lib/demo/material/progress_indicator_demo.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
219
web/gallery/lib/demo/material/reorderable_list_demo.dart
Normal file
219
web/gallery/lib/demo/material/reorderable_list_demo.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
195
web/gallery/lib/demo/material/scrollable_tabs_demo.dart
Normal file
195
web/gallery/lib/demo/material/scrollable_tabs_demo.dart
Normal 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()),
|
||||
);
|
||||
}
|
||||
}
|
||||
295
web/gallery/lib/demo/material/search_demo.dart
Normal file
295
web/gallery/lib/demo/material/search_demo.dart
Normal 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);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
111
web/gallery/lib/demo/material/selection_controls_demo.dart
Normal file
111
web/gallery/lib/demo/material/selection_controls_demo.dart
Normal 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)
|
||||
])
|
||||
]));
|
||||
}
|
||||
}
|
||||
240
web/gallery/lib/demo/material/slider_demo.dart
Normal file
240
web/gallery/lib/demo/material/slider_demo.dart
Normal 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'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
83
web/gallery/lib/demo/material/snack_bar_demo.dart
Normal file
83
web/gallery/lib/demo/material/snack_bar_demo.dart
Normal 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));
|
||||
}
|
||||
}
|
||||
23
web/gallery/lib/demo/material/stack_demo.dart
Normal file
23
web/gallery/lib/demo/material/stack_demo.dart
Normal 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'),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
42
web/gallery/lib/demo/material/switch_demo.dart
Normal file
42
web/gallery/lib/demo/material/switch_demo.dart
Normal 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;
|
||||
});
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
209
web/gallery/lib/demo/material/tabs_demo.dart
Normal file
209
web/gallery/lib/demo/material/tabs_demo.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
150
web/gallery/lib/demo/material/tabs_fab_demo.dart
Normal file
150
web/gallery/lib/demo/material/tabs_fab_demo.dart
Normal 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()),
|
||||
);
|
||||
}
|
||||
}
|
||||
52
web/gallery/lib/demo/material/text_demo.dart
Normal file
52
web/gallery/lib/demo/material/text_demo.dart
Normal 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);
|
||||
}
|
||||
341
web/gallery/lib/demo/material/text_form_field_demo.dart
Normal file
341
web/gallery/lib/demo/material/text_form_field_demo.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
59
web/gallery/lib/demo/material/tooltip_demo.dart
Normal file
59
web/gallery/lib/demo/material/tooltip_demo.dart
Normal 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()),
|
||||
);
|
||||
}));
|
||||
}
|
||||
}
|
||||
34
web/gallery/lib/demo/material/two_level_list_demo.dart
Normal file
34
web/gallery/lib/demo/material/two_level_list_demo.dart
Normal 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'))
|
||||
]));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user