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

Add flutter_web samples (#75)

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

View File

@@ -0,0 +1,15 @@
// 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 'animation_demo.dart';
//export 'calculator_demo.dart';
export 'colors_demo.dart';
export 'contacts_demo.dart';
//export 'cupertino/cupertino.dart';
//export 'images_demo.dart';
export 'material/material.dart';
export 'pesto_demo.dart';
export 'shrine_demo.dart';
export 'typography_demo.dart';
//export 'video_demo.dart';

View File

@@ -0,0 +1,660 @@
// 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 'package:flutter_web/rendering.dart';
import 'sections.dart';
import 'widgets.dart';
const Color _kAppBackgroundColor = Color(0xFF353662);
const Duration _kScrollDuration = Duration(milliseconds: 400);
const Curve _kScrollCurve = Curves.fastOutSlowIn;
// This app's contents start out at _kHeadingMaxHeight and they function like
// an appbar. Initially the appbar occupies most of the screen and its section
// headings are laid out in a column. By the time its height has been
// reduced to _kAppBarMidHeight, its layout is horizontal, only one section
// heading is visible, and the section's list of details is visible below the
// heading. The appbar's height can be reduced to no more than _kAppBarMinHeight.
const double _kAppBarMinHeight = 90.0;
const double _kAppBarMidHeight = 256.0;
// The AppBar's max height depends on the screen, see _AnimationDemoHomeState._buildBody()
// Initially occupies the same space as the status bar and gets smaller as
// the primary scrollable scrolls upwards.
// TODO(hansmuller): it would be worth adding something like this to the framework.
class _RenderStatusBarPaddingSliver extends RenderSliver {
_RenderStatusBarPaddingSliver({
@required double maxHeight,
@required double scrollFactor,
}) : assert(maxHeight != null && maxHeight >= 0.0),
assert(scrollFactor != null && scrollFactor >= 1.0),
_maxHeight = maxHeight,
_scrollFactor = scrollFactor;
// The height of the status bar
double get maxHeight => _maxHeight;
double _maxHeight;
set maxHeight(double value) {
assert(maxHeight != null && maxHeight >= 0.0);
if (_maxHeight == value) return;
_maxHeight = value;
markNeedsLayout();
}
// That rate at which this renderer's height shrinks when the scroll
// offset changes.
double get scrollFactor => _scrollFactor;
double _scrollFactor;
set scrollFactor(double value) {
assert(scrollFactor != null && scrollFactor >= 1.0);
if (_scrollFactor == value) return;
_scrollFactor = value;
markNeedsLayout();
}
@override
void performLayout() {
final double height = (maxHeight - constraints.scrollOffset / scrollFactor)
.clamp(0.0, maxHeight);
geometry = SliverGeometry(
paintExtent: math.min(height, constraints.remainingPaintExtent),
scrollExtent: maxHeight,
maxPaintExtent: maxHeight,
);
}
}
class _StatusBarPaddingSliver extends SingleChildRenderObjectWidget {
const _StatusBarPaddingSliver({
Key key,
@required this.maxHeight,
this.scrollFactor = 5.0,
}) : assert(maxHeight != null && maxHeight >= 0.0),
assert(scrollFactor != null && scrollFactor >= 1.0),
super(key: key);
final double maxHeight;
final double scrollFactor;
@override
_RenderStatusBarPaddingSliver createRenderObject(BuildContext context) {
return _RenderStatusBarPaddingSliver(
maxHeight: maxHeight,
scrollFactor: scrollFactor,
);
}
@override
void updateRenderObject(
BuildContext context, _RenderStatusBarPaddingSliver renderObject) {
renderObject
..maxHeight = maxHeight
..scrollFactor = scrollFactor;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(DoubleProperty('maxHeight', maxHeight));
description.add(DoubleProperty('scrollFactor', scrollFactor));
}
}
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate({
@required this.minHeight,
@required this.maxHeight,
@required this.child,
});
final double minHeight;
final double maxHeight;
final Widget child;
@override
double get minExtent => minHeight;
@override
double get maxExtent => math.max(maxHeight, minHeight);
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return SizedBox.expand(child: child);
}
@override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight ||
minHeight != oldDelegate.minHeight ||
child != oldDelegate.child;
}
@override
String toString() => '_SliverAppBarDelegate';
}
// Arrange the section titles, indicators, and cards. The cards are only included when
// the layout is transitioning between vertical and horizontal. Once the layout is
// horizontal the cards are laid out by a PageView.
//
// The layout of the section cards, titles, and indicators is defined by the
// two 0.0-1.0 "t" parameters, both of which are based on the layout's height:
// - tColumnToRow
// 0.0 when height is maxHeight and the layout is a column
// 1.0 when the height is midHeight and the layout is a row
// - tCollapsed
// 0.0 when height is midHeight and the layout is a row
// 1.0 when height is minHeight and the layout is a (still) row
//
// minHeight < midHeight < maxHeight
//
// The general approach here is to compute the column layout and row size
// and position of each element and then interpolate between them using
// tColumnToRow. Once tColumnToRow reaches 1.0, the layout changes are
// defined by tCollapsed. As tCollapsed increases the titles spread out
// until only one title is visible and the indicators cluster together
// until they're all visible.
class _AllSectionsLayout extends MultiChildLayoutDelegate {
_AllSectionsLayout({
this.translation,
this.tColumnToRow,
this.tCollapsed,
this.cardCount,
this.selectedIndex,
});
final Alignment translation;
final double tColumnToRow;
final double tCollapsed;
final int cardCount;
final double selectedIndex;
Rect _interpolateRect(Rect begin, Rect end) {
return Rect.lerp(begin, end, tColumnToRow);
}
Offset _interpolatePoint(Offset begin, Offset end) {
return Offset.lerp(begin, end, tColumnToRow);
}
@override
void performLayout(Size size) {
final double columnCardX = size.width / 5.0;
final double columnCardWidth = size.width - columnCardX;
final double columnCardHeight = size.height / cardCount;
final double rowCardWidth = size.width;
final Offset offset = translation.alongSize(size);
double columnCardY = 0.0;
double rowCardX = -(selectedIndex * rowCardWidth);
// When tCollapsed > 0 the titles spread apart
final double columnTitleX = size.width / 10.0;
final double rowTitleWidth = size.width * ((1 + tCollapsed) / 2.25);
double rowTitleX =
(size.width - rowTitleWidth) / 2.0 - selectedIndex * rowTitleWidth;
// When tCollapsed > 0, the indicators move closer together
//final double rowIndicatorWidth = 48.0 + (1.0 - tCollapsed) * (rowTitleWidth - 48.0);
const double paddedSectionIndicatorWidth = kSectionIndicatorWidth + 8.0;
final double rowIndicatorWidth = paddedSectionIndicatorWidth +
(1.0 - tCollapsed) * (rowTitleWidth - paddedSectionIndicatorWidth);
double rowIndicatorX = (size.width - rowIndicatorWidth) / 2.0 -
selectedIndex * rowIndicatorWidth;
// Compute the size and origin of each card, title, and indicator for the maxHeight
// "column" layout, and the midHeight "row" layout. The actual layout is just the
// interpolated value between the column and row layouts for t.
for (int index = 0; index < cardCount; index++) {
// Layout the card for index.
final Rect columnCardRect = Rect.fromLTWH(
columnCardX, columnCardY, columnCardWidth, columnCardHeight);
final Rect rowCardRect =
Rect.fromLTWH(rowCardX, 0.0, rowCardWidth, size.height);
final Rect cardRect =
_interpolateRect(columnCardRect, rowCardRect).shift(offset);
final String cardId = 'card$index';
if (hasChild(cardId)) {
layoutChild(cardId, BoxConstraints.tight(cardRect.size));
positionChild(cardId, cardRect.topLeft);
}
// Layout the title for index.
final Size titleSize =
layoutChild('title$index', BoxConstraints.loose(cardRect.size));
final double columnTitleY =
columnCardRect.centerLeft.dy - titleSize.height / 2.0;
final double rowTitleY =
rowCardRect.centerLeft.dy - titleSize.height / 2.0;
final double centeredRowTitleX =
rowTitleX + (rowTitleWidth - titleSize.width) / 2.0;
final Offset columnTitleOrigin = Offset(columnTitleX, columnTitleY);
final Offset rowTitleOrigin = Offset(centeredRowTitleX, rowTitleY);
final Offset titleOrigin =
_interpolatePoint(columnTitleOrigin, rowTitleOrigin);
positionChild('title$index', titleOrigin + offset);
// Layout the selection indicator for index.
final Size indicatorSize =
layoutChild('indicator$index', BoxConstraints.loose(cardRect.size));
final double columnIndicatorX =
cardRect.centerRight.dx - indicatorSize.width - 16.0;
final double columnIndicatorY =
cardRect.bottomRight.dy - indicatorSize.height - 16.0;
final Offset columnIndicatorOrigin =
Offset(columnIndicatorX, columnIndicatorY);
final Rect titleRect =
Rect.fromPoints(titleOrigin, titleSize.bottomRight(titleOrigin));
final double centeredRowIndicatorX =
rowIndicatorX + (rowIndicatorWidth - indicatorSize.width) / 2.0;
final double rowIndicatorY = titleRect.bottomCenter.dy + 16.0;
final Offset rowIndicatorOrigin =
Offset(centeredRowIndicatorX, rowIndicatorY);
final Offset indicatorOrigin =
_interpolatePoint(columnIndicatorOrigin, rowIndicatorOrigin);
positionChild('indicator$index', indicatorOrigin + offset);
columnCardY += columnCardHeight;
rowCardX += rowCardWidth;
rowTitleX += rowTitleWidth;
rowIndicatorX += rowIndicatorWidth;
}
}
@override
bool shouldRelayout(_AllSectionsLayout oldDelegate) {
return tColumnToRow != oldDelegate.tColumnToRow ||
cardCount != oldDelegate.cardCount ||
selectedIndex != oldDelegate.selectedIndex;
}
}
class _AllSectionsView extends AnimatedWidget {
_AllSectionsView({
Key key,
this.sectionIndex,
@required this.sections,
@required this.selectedIndex,
this.minHeight,
this.midHeight,
this.maxHeight,
this.sectionCards = const <Widget>[],
}) : assert(sections != null),
assert(sectionCards != null),
assert(sectionCards.length == sections.length),
assert(sectionIndex >= 0 && sectionIndex < sections.length),
assert(selectedIndex != null),
assert(selectedIndex.value >= 0.0 &&
selectedIndex.value < sections.length.toDouble()),
super(key: key, listenable: selectedIndex);
final int sectionIndex;
final List<Section> sections;
final ValueNotifier<double> selectedIndex;
final double minHeight;
final double midHeight;
final double maxHeight;
final List<Widget> sectionCards;
double _selectedIndexDelta(int index) {
return (index.toDouble() - selectedIndex.value).abs().clamp(0.0, 1.0);
}
Widget _build(BuildContext context, BoxConstraints constraints) {
final Size size = constraints.biggest;
// The layout's progress from from a column to a row. Its value is
// 0.0 when size.height equals the maxHeight, 1.0 when the size.height
// equals the midHeight.
final double tColumnToRow = 1.0 -
((size.height - midHeight) / (maxHeight - midHeight)).clamp(0.0, 1.0);
// The layout's progress from from the midHeight row layout to
// a minHeight row layout. Its value is 0.0 when size.height equals
// midHeight and 1.0 when size.height equals minHeight.
final double tCollapsed = 1.0 -
((size.height - minHeight) / (midHeight - minHeight)).clamp(0.0, 1.0);
double _indicatorOpacity(int index) {
return 1.0 - _selectedIndexDelta(index) * 0.5;
}
double _titleOpacity(int index) {
return 1.0 - _selectedIndexDelta(index) * tColumnToRow * 0.5;
}
double _titleScale(int index) {
return 1.0 - _selectedIndexDelta(index) * tColumnToRow * 0.15;
}
final List<Widget> children = List<Widget>.from(sectionCards);
for (int index = 0; index < sections.length; index++) {
final Section section = sections[index];
children.add(LayoutId(
id: 'title$index',
child: SectionTitle(
section: section,
scale: _titleScale(index),
opacity: _titleOpacity(index),
),
));
}
for (int index = 0; index < sections.length; index++) {
children.add(LayoutId(
id: 'indicator$index',
child: SectionIndicator(
opacity: _indicatorOpacity(index),
),
));
}
return CustomMultiChildLayout(
delegate: _AllSectionsLayout(
translation:
Alignment((selectedIndex.value - sectionIndex) * 2.0 - 1.0, -1.0),
tColumnToRow: tColumnToRow,
tCollapsed: tCollapsed,
cardCount: sections.length,
selectedIndex: selectedIndex.value,
),
children: children,
);
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: _build);
}
}
// Support snapping scrolls to the midScrollOffset: the point at which the
// app bar's height is _kAppBarMidHeight and only one section heading is
// visible.
class _SnappingScrollPhysics extends ClampingScrollPhysics {
const _SnappingScrollPhysics({
ScrollPhysics parent,
@required this.midScrollOffset,
}) : assert(midScrollOffset != null),
super(parent: parent);
final double midScrollOffset;
@override
_SnappingScrollPhysics applyTo(ScrollPhysics ancestor) {
return _SnappingScrollPhysics(
parent: buildParent(ancestor), midScrollOffset: midScrollOffset);
}
Simulation _toMidScrollOffsetSimulation(double offset, double dragVelocity) {
final double velocity = math.max(dragVelocity, minFlingVelocity);
return ScrollSpringSimulation(spring, offset, midScrollOffset, velocity,
tolerance: tolerance);
}
Simulation _toZeroScrollOffsetSimulation(double offset, double dragVelocity) {
final double velocity = math.max(dragVelocity, minFlingVelocity);
return ScrollSpringSimulation(spring, offset, 0.0, velocity,
tolerance: tolerance);
}
@override
Simulation createBallisticSimulation(
ScrollMetrics position, double dragVelocity) {
final Simulation simulation =
super.createBallisticSimulation(position, dragVelocity);
final double offset = position.pixels;
if (simulation != null) {
// The drag ended with sufficient velocity to trigger creating a simulation.
// If the simulation is headed up towards midScrollOffset but will not reach it,
// then snap it there. Similarly if the simulation is headed down past
// midScrollOffset but will not reach zero, then snap it to zero.
final double simulationEnd = simulation.x(double.infinity);
if (simulationEnd >= midScrollOffset) return simulation;
if (dragVelocity > 0.0)
return _toMidScrollOffsetSimulation(offset, dragVelocity);
if (dragVelocity < 0.0)
return _toZeroScrollOffsetSimulation(offset, dragVelocity);
} else {
// The user ended the drag with little or no velocity. If they
// didn't leave the offset above midScrollOffset, then
// snap to midScrollOffset if they're more than halfway there,
// otherwise snap to zero.
final double snapThreshold = midScrollOffset / 2.0;
if (offset >= snapThreshold && offset < midScrollOffset)
return _toMidScrollOffsetSimulation(offset, dragVelocity);
if (offset > 0.0 && offset < snapThreshold)
return _toZeroScrollOffsetSimulation(offset, dragVelocity);
}
return simulation;
}
}
class AnimationDemoHome extends StatefulWidget {
const AnimationDemoHome({Key key}) : super(key: key);
static const String routeName = '/animation';
@override
_AnimationDemoHomeState createState() => _AnimationDemoHomeState();
}
class _AnimationDemoHomeState extends State<AnimationDemoHome> {
final ScrollController _scrollController = ScrollController();
final PageController _headingPageController = PageController();
final PageController _detailsPageController = PageController();
ScrollPhysics _headingScrollPhysics = const NeverScrollableScrollPhysics();
ValueNotifier<double> selectedIndex = ValueNotifier<double>(0.0);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: _kAppBackgroundColor,
body: Builder(
// Insert an element so that _buildBody can find the PrimaryScrollController.
builder: _buildBody,
),
);
}
void _handleBackButton(double midScrollOffset) {
if (_scrollController.offset >= midScrollOffset)
_scrollController.animateTo(0.0,
curve: _kScrollCurve, duration: _kScrollDuration);
else
Navigator.maybePop(context);
}
// Only enable paging for the heading when the user has scrolled to midScrollOffset.
// Paging is enabled/disabled by setting the heading's PageView scroll physics.
bool _handleScrollNotification(
ScrollNotification notification, double midScrollOffset) {
if (notification.depth == 0 && notification is ScrollUpdateNotification) {
final ScrollPhysics physics =
_scrollController.position.pixels >= midScrollOffset
? const PageScrollPhysics()
: const NeverScrollableScrollPhysics();
if (physics != _headingScrollPhysics) {
setState(() {
_headingScrollPhysics = physics;
});
}
}
return false;
}
void _maybeScroll(double midScrollOffset, int pageIndex, double xOffset) {
if (_scrollController.offset < midScrollOffset) {
// Scroll the overall list to the point where only one section card shows.
// At the same time scroll the PageViews to the page at pageIndex.
_headingPageController.animateToPage(pageIndex,
curve: _kScrollCurve, duration: _kScrollDuration);
_scrollController.animateTo(midScrollOffset,
curve: _kScrollCurve, duration: _kScrollDuration);
} else {
// One one section card is showing: scroll one page forward or back.
final double centerX =
_headingPageController.position.viewportDimension / 2.0;
final int newPageIndex =
xOffset > centerX ? pageIndex + 1 : pageIndex - 1;
_headingPageController.animateToPage(newPageIndex,
curve: _kScrollCurve, duration: _kScrollDuration);
}
}
bool _handlePageNotification(ScrollNotification notification,
PageController leader, PageController follower) {
if (notification.depth == 0 && notification is ScrollUpdateNotification) {
selectedIndex.value = leader.page;
if (follower.page != leader.page)
// ignore: deprecated_member_use
follower.position.jumpToWithoutSettling(leader.position.pixels);
}
return false;
}
Iterable<Widget> _detailItemsFor(Section section) {
final Iterable<Widget> detailItems =
section.details.map<Widget>((SectionDetail detail) {
return SectionDetailView(detail: detail);
});
return ListTile.divideTiles(context: context, tiles: detailItems);
}
Iterable<Widget> _allHeadingItems(double maxHeight, double midScrollOffset) {
final List<Widget> sectionCards = <Widget>[];
for (int index = 0; index < allSections.length; index++) {
sectionCards.add(LayoutId(
id: 'card$index',
child: GestureDetector(
behavior: HitTestBehavior.opaque,
child: SectionCard(section: allSections[index]),
onTapUp: (TapUpDetails details) {
final double xOffset = details.globalPosition.dx;
setState(() {
_maybeScroll(midScrollOffset, index, xOffset);
});
}),
));
}
final List<Widget> headings = <Widget>[];
for (int index = 0; index < allSections.length; index++) {
headings.add(Container(
color: _kAppBackgroundColor,
child: ClipRect(
child: _AllSectionsView(
sectionIndex: index,
sections: allSections,
selectedIndex: selectedIndex,
minHeight: _kAppBarMinHeight,
midHeight: _kAppBarMidHeight,
maxHeight: maxHeight,
sectionCards: sectionCards,
),
),
));
}
return headings;
}
Widget _buildBody(BuildContext context) {
final MediaQueryData mediaQueryData = MediaQuery.of(context);
final double statusBarHeight = mediaQueryData.padding.top;
final double screenHeight = mediaQueryData.size.height;
final double appBarMaxHeight = screenHeight - statusBarHeight;
// The scroll offset that reveals the appBarMidHeight appbar.
final double appBarMidScrollOffset =
statusBarHeight + appBarMaxHeight - _kAppBarMidHeight;
return SizedBox.expand(
child: Stack(
children: <Widget>[
NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
return _handleScrollNotification(
notification, appBarMidScrollOffset);
},
child: CustomScrollView(
controller: _scrollController,
physics: _SnappingScrollPhysics(
midScrollOffset: appBarMidScrollOffset),
slivers: <Widget>[
// Start out below the status bar, gradually move to the top of the screen.
_StatusBarPaddingSliver(
maxHeight: statusBarHeight,
scrollFactor: 7.0,
),
// Section Headings
SliverPersistentHeader(
pinned: true,
delegate: _SliverAppBarDelegate(
minHeight: _kAppBarMinHeight,
maxHeight: appBarMaxHeight,
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
return _handlePageNotification(notification,
_headingPageController, _detailsPageController);
},
child: PageView(
physics: _headingScrollPhysics,
controller: _headingPageController,
children: _allHeadingItems(
appBarMaxHeight, appBarMidScrollOffset),
),
),
),
),
// Details
SliverToBoxAdapter(
child: SizedBox(
height: 610.0,
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
return _handlePageNotification(notification,
_detailsPageController, _headingPageController);
},
child: PageView(
controller: _detailsPageController,
children: allSections.map<Widget>((Section section) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _detailItemsFor(section).toList(),
);
}).toList(),
),
),
),
),
],
),
),
Positioned(
top: statusBarHeight,
left: 0.0,
child: IconTheme(
data: const IconThemeData(color: Colors.white),
child: SafeArea(
top: false,
bottom: false,
child: IconButton(
icon: const BackButtonIcon(),
tooltip: 'Back',
onPressed: () {
_handleBackButton(appBarMidScrollOffset);
}),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,167 @@
// 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';
const Color _mariner = Color(0xFF3B5F8F);
const Color _mediumPurple = Color(0xFF8266D4);
const Color _tomato = Color(0xFFF95B57);
const Color _mySin = Color(0xFFF3A646);
const String _kGalleryAssetsPackage = null;
class SectionDetail {
const SectionDetail({
this.title,
this.subtitle,
this.imageAsset,
this.imageAssetPackage,
});
final String title;
final String subtitle;
final String imageAsset;
final String imageAssetPackage;
}
class Section {
const Section({
this.title,
this.backgroundAsset,
this.backgroundAssetPackage,
this.leftColor,
this.rightColor,
this.details,
});
final String title;
final String backgroundAsset;
final String backgroundAssetPackage;
final Color leftColor;
final Color rightColor;
final List<SectionDetail> details;
@override
bool operator ==(Object other) {
if (other is! Section) return false;
final Section otherSection = other;
return title == otherSection.title;
}
@override
int get hashCode => title.hashCode;
}
// TODO(hansmuller): replace the SectionDetail images and text. Get rid of
// the const vars like _eyeglassesDetail and insert a variety of titles and
// image SectionDetails in the allSections list.
const SectionDetail _eyeglassesDetail = SectionDetail(
imageAsset: 'products/sunnies.png',
imageAssetPackage: _kGalleryAssetsPackage,
title: 'Flutter enables interactive animation',
subtitle: '3K views - 5 days',
);
const SectionDetail _eyeglassesImageDetail = SectionDetail(
imageAsset: 'products/sunnies.png',
imageAssetPackage: _kGalleryAssetsPackage,
);
const SectionDetail _seatingDetail = SectionDetail(
imageAsset: 'products/table.png',
imageAssetPackage: _kGalleryAssetsPackage,
title: 'Flutter enables interactive animation',
subtitle: '3K views - 5 days',
);
const SectionDetail _seatingImageDetail = SectionDetail(
imageAsset: 'products/table.png',
imageAssetPackage: _kGalleryAssetsPackage,
);
const SectionDetail _decorationDetail = SectionDetail(
imageAsset: 'products/earrings.png',
imageAssetPackage: _kGalleryAssetsPackage,
title: 'Flutter enables interactive animation',
subtitle: '3K views - 5 days',
);
const SectionDetail _decorationImageDetail = SectionDetail(
imageAsset: 'products/earrings.png',
imageAssetPackage: _kGalleryAssetsPackage,
);
const SectionDetail _protectionDetail = SectionDetail(
imageAsset: 'products/hat.png',
imageAssetPackage: _kGalleryAssetsPackage,
title: 'Flutter enables interactive animation',
subtitle: '3K views - 5 days',
);
const SectionDetail _protectionImageDetail = SectionDetail(
imageAsset: 'products/hat.png',
imageAssetPackage: _kGalleryAssetsPackage,
);
final List<Section> allSections = <Section>[
const Section(
title: 'SUNGLASSES',
leftColor: _mediumPurple,
rightColor: _mariner,
backgroundAsset: 'products/sunnies.png',
backgroundAssetPackage: _kGalleryAssetsPackage,
details: <SectionDetail>[
_eyeglassesDetail,
_eyeglassesImageDetail,
_eyeglassesDetail,
_eyeglassesDetail,
_eyeglassesDetail,
_eyeglassesDetail,
],
),
const Section(
title: 'FURNITURE',
leftColor: _tomato,
rightColor: _mediumPurple,
backgroundAsset: 'products/table.png',
backgroundAssetPackage: _kGalleryAssetsPackage,
details: <SectionDetail>[
_seatingDetail,
_seatingImageDetail,
_seatingDetail,
_seatingDetail,
_seatingDetail,
_seatingDetail,
],
),
const Section(
title: 'JEWELRY',
leftColor: _mySin,
rightColor: _tomato,
backgroundAsset: 'products/earrings.png',
backgroundAssetPackage: _kGalleryAssetsPackage,
details: <SectionDetail>[
_decorationDetail,
_decorationImageDetail,
_decorationDetail,
_decorationDetail,
_decorationDetail,
_decorationDetail,
],
),
const Section(
title: 'HEADWEAR',
leftColor: Colors.white,
rightColor: _tomato,
backgroundAsset: 'products/hat.png',
backgroundAssetPackage: _kGalleryAssetsPackage,
details: <SectionDetail>[
_protectionDetail,
_protectionImageDetail,
_protectionDetail,
_protectionDetail,
_protectionDetail,
_protectionDetail,
],
),
];

View File

@@ -0,0 +1,172 @@
// 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 'sections.dart';
const double kSectionIndicatorWidth = 32.0;
// The card for a single section. Displays the section's gradient and background image.
class SectionCard extends StatelessWidget {
const SectionCard({Key key, @required this.section})
: assert(section != null),
super(key: key);
final Section section;
@override
Widget build(BuildContext context) {
return Semantics(
label: section.title,
button: true,
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: <Color>[
section.leftColor,
section.rightColor,
],
),
),
// TODO(b:119312219): Remove Opacity layer when Image Color Filter
// is implemented in paintImage.
child: Opacity(
opacity: 0.075,
child: Image.asset(
section.backgroundAsset,
package: section.backgroundAssetPackage,
color: const Color.fromRGBO(255, 255, 255, 0.075),
colorBlendMode: BlendMode.modulate,
fit: BoxFit.cover,
),
),
),
);
}
}
// The title is rendered with two overlapping text widgets that are vertically
// offset a little. It's supposed to look sort-of 3D.
class SectionTitle extends StatelessWidget {
const SectionTitle({
Key key,
@required this.section,
@required this.scale,
@required this.opacity,
}) : assert(section != null),
assert(scale != null),
assert(opacity != null && opacity >= 0.0 && opacity <= 1.0),
super(key: key);
final Section section;
final double scale;
final double opacity;
static const TextStyle sectionTitleStyle = TextStyle(
fontFamily: 'Raleway',
inherit: false,
fontSize: 24.0,
fontWeight: FontWeight.w500,
color: Colors.white,
textBaseline: TextBaseline.alphabetic,
);
static final TextStyle sectionTitleShadowStyle = sectionTitleStyle.copyWith(
color: const Color(0x19000000),
);
@override
Widget build(BuildContext context) {
return IgnorePointer(
child: Opacity(
opacity: opacity,
child: Transform(
transform: Matrix4.identity()..scale(scale),
alignment: Alignment.center,
child: Stack(
children: <Widget>[
Positioned(
top: 4.0,
child: Text(section.title, style: sectionTitleShadowStyle),
),
Text(section.title, style: sectionTitleStyle),
],
),
),
),
);
}
}
// Small horizontal bar that indicates the selected section.
class SectionIndicator extends StatelessWidget {
const SectionIndicator({Key key, this.opacity = 1.0}) : super(key: key);
final double opacity;
@override
Widget build(BuildContext context) {
return IgnorePointer(
child: Container(
width: kSectionIndicatorWidth,
height: 3.0,
color: Colors.white.withOpacity(opacity),
),
);
}
}
// Display a single SectionDetail.
class SectionDetailView extends StatelessWidget {
SectionDetailView({Key key, @required this.detail})
: assert(detail != null && detail.imageAsset != null),
assert((detail.imageAsset ?? detail.title) != null),
super(key: key);
final SectionDetail detail;
@override
Widget build(BuildContext context) {
final Widget image = DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
image: DecorationImage(
image: AssetImage(
detail.imageAsset,
package: detail.imageAssetPackage,
),
fit: BoxFit.cover,
alignment: Alignment.center,
),
),
);
Widget item;
if (detail.title == null && detail.subtitle == null) {
item = Container(
height: 240.0,
padding: const EdgeInsets.all(16.0),
child: SafeArea(
top: false,
bottom: false,
child: image,
),
);
} else {
item = ListTile(
title: Text(detail.title),
subtitle: Text(detail.subtitle),
leading: SizedBox(width: 32.0, height: 32.0, child: image),
);
}
return DecoratedBox(
decoration: BoxDecoration(color: Colors.grey.shade200),
child: item,
);
}
}

View File

@@ -0,0 +1,16 @@
// 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 'animation/home.dart';
class AnimationDemo extends StatelessWidget {
const AnimationDemo({Key key}) : super(key: key);
static const String routeName = '/animation';
@override
Widget build(BuildContext context) => const AnimationDemoHome();
}

View File

@@ -0,0 +1,222 @@
// 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';
const double kColorItemHeight = 48.0;
class Palette {
Palette({this.name, this.primary, this.accent, this.threshold = 900});
final String name;
final MaterialColor primary;
final MaterialAccentColor accent;
final int
threshold; // titles for indices > threshold are white, otherwise black
bool get isValid => name != null && primary != null && threshold != null;
}
final List<Palette> allPalettes = <Palette>[
Palette(
name: 'RED',
primary: Colors.red,
accent: Colors.redAccent,
threshold: 300),
Palette(
name: 'PINK',
primary: Colors.pink,
accent: Colors.pinkAccent,
threshold: 200),
Palette(
name: 'PURPLE',
primary: Colors.purple,
accent: Colors.purpleAccent,
threshold: 200),
Palette(
name: 'DEEP PURPLE',
primary: Colors.deepPurple,
accent: Colors.deepPurpleAccent,
threshold: 200),
Palette(
name: 'INDIGO',
primary: Colors.indigo,
accent: Colors.indigoAccent,
threshold: 200),
Palette(
name: 'BLUE',
primary: Colors.blue,
accent: Colors.blueAccent,
threshold: 400),
Palette(
name: 'LIGHT BLUE',
primary: Colors.lightBlue,
accent: Colors.lightBlueAccent,
threshold: 500),
Palette(
name: 'CYAN',
primary: Colors.cyan,
accent: Colors.cyanAccent,
threshold: 600),
Palette(
name: 'TEAL',
primary: Colors.teal,
accent: Colors.tealAccent,
threshold: 400),
Palette(
name: 'GREEN',
primary: Colors.green,
accent: Colors.greenAccent,
threshold: 500),
Palette(
name: 'LIGHT GREEN',
primary: Colors.lightGreen,
accent: Colors.lightGreenAccent,
threshold: 600),
Palette(
name: 'LIME',
primary: Colors.lime,
accent: Colors.limeAccent,
threshold: 800),
Palette(name: 'YELLOW', primary: Colors.yellow, accent: Colors.yellowAccent),
Palette(name: 'AMBER', primary: Colors.amber, accent: Colors.amberAccent),
Palette(
name: 'ORANGE',
primary: Colors.orange,
accent: Colors.orangeAccent,
threshold: 700),
Palette(
name: 'DEEP ORANGE',
primary: Colors.deepOrange,
accent: Colors.deepOrangeAccent,
threshold: 400),
Palette(name: 'BROWN', primary: Colors.brown, threshold: 200),
Palette(name: 'GREY', primary: Colors.grey, threshold: 500),
Palette(name: 'BLUE GREY', primary: Colors.blueGrey, threshold: 500),
];
class ColorItem extends StatelessWidget {
const ColorItem({
Key key,
@required this.index,
@required this.color,
this.prefix = '',
}) : assert(index != null),
assert(color != null),
assert(prefix != null),
super(key: key);
final int index;
final Color color;
final String prefix;
String colorString() =>
"#${color.value.toRadixString(16).padLeft(8, '0').toUpperCase()}";
@override
Widget build(BuildContext context) {
return Semantics(
container: true,
child: Container(
height: kColorItemHeight,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
color: color,
child: SafeArea(
top: false,
bottom: false,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text('$prefix$index'),
Text(colorString()),
],
),
),
),
);
}
}
class PaletteTabView extends StatelessWidget {
PaletteTabView({
Key key,
@required this.colors,
}) : assert(colors != null && colors.isValid),
super(key: key);
final Palette colors;
static const List<int> primaryKeys = <int>[
50,
100,
200,
300,
400,
500,
600,
700,
800,
900
];
static const List<int> accentKeys = <int>[100, 200, 400, 700];
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
final TextStyle whiteTextStyle =
textTheme.body1.copyWith(color: Colors.white);
final TextStyle blackTextStyle =
textTheme.body1.copyWith(color: Colors.black);
final List<Widget> colorItems = primaryKeys.map<Widget>((int index) {
return DefaultTextStyle(
style: index > colors.threshold ? whiteTextStyle : blackTextStyle,
child: ColorItem(index: index, color: colors.primary[index]),
);
}).toList();
if (colors.accent != null) {
colorItems.addAll(accentKeys.map<Widget>((int index) {
return DefaultTextStyle(
style: index > colors.threshold ? whiteTextStyle : blackTextStyle,
child:
ColorItem(index: index, color: colors.accent[index], prefix: 'A'),
);
}).toList());
}
return ListView(
itemExtent: kColorItemHeight,
children: colorItems,
);
}
}
class ColorsDemo extends StatelessWidget {
static const String routeName = '/colors';
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: allPalettes.length,
child: Scaffold(
appBar: AppBar(
elevation: 0.0,
title: const Text('Colors'),
bottom: TabBar(
isScrollable: true,
tabs: allPalettes
.map<Widget>((Palette swatch) => Tab(text: swatch.name))
.toList(),
),
),
body: TabBarView(
children: allPalettes.map<Widget>((Palette colors) {
return PaletteTabView(colors: colors);
}).toList(),
),
),
);
}
}

View File

@@ -0,0 +1,340 @@
// 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/services.dart';
class _ContactCategory extends StatelessWidget {
const _ContactCategory({Key key, this.icon, this.children}) : super(key: key);
final IconData icon;
final List<Widget> children;
@override
Widget build(BuildContext context) {
final ThemeData themeData = Theme.of(context);
return Container(
padding: const EdgeInsets.symmetric(vertical: 16.0),
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: themeData.dividerColor))),
child: DefaultTextStyle(
style: Theme.of(context).textTheme.subhead,
child: SafeArea(
top: false,
bottom: false,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
padding: const EdgeInsets.symmetric(vertical: 24.0),
width: 72.0,
child: Icon(icon, color: themeData.primaryColor)),
Expanded(child: Column(children: children))
],
),
),
),
);
}
}
class _ContactItem extends StatelessWidget {
_ContactItem({Key key, this.icon, this.lines, this.tooltip, this.onPressed})
: assert(lines.length > 1),
super(key: key);
final IconData icon;
final List<String> lines;
final String tooltip;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
final ThemeData themeData = Theme.of(context);
final List<Widget> columnChildren = lines
.sublist(0, lines.length - 1)
.map<Widget>((String line) => Text(line))
.toList();
columnChildren.add(Text(lines.last, style: themeData.textTheme.caption));
final List<Widget> rowChildren = <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: columnChildren))
];
if (icon != null) {
rowChildren.add(SizedBox(
width: 72.0,
child: IconButton(
icon: Icon(icon),
color: themeData.primaryColor,
onPressed: onPressed)));
}
return MergeSemantics(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: rowChildren)),
);
}
}
class ContactsDemo extends StatefulWidget {
static const String routeName = '/contacts';
@override
ContactsDemoState createState() => ContactsDemoState();
}
enum AppBarBehavior { normal, pinned, floating, snapping }
class ContactsDemoState extends State<ContactsDemo> {
static final GlobalKey<ScaffoldState> _scaffoldKey =
GlobalKey<ScaffoldState>();
final double _appBarHeight = 256.0;
AppBarBehavior _appBarBehavior = AppBarBehavior.pinned;
@override
Widget build(BuildContext context) {
return Theme(
data: ThemeData(
brightness: Brightness.light,
primarySwatch: Colors.indigo,
platform: Theme.of(context).platform,
),
child: Scaffold(
key: _scaffoldKey,
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
expandedHeight: _appBarHeight,
pinned: _appBarBehavior == AppBarBehavior.pinned,
floating: _appBarBehavior == AppBarBehavior.floating ||
_appBarBehavior == AppBarBehavior.snapping,
snap: _appBarBehavior == AppBarBehavior.snapping,
actions: <Widget>[
IconButton(
icon: const Icon(Icons.create),
tooltip: 'Edit',
onPressed: () {
_scaffoldKey.currentState.showSnackBar(const SnackBar(
content:
Text("Editing isn't supported in this screen.")));
},
),
PopupMenuButton<AppBarBehavior>(
onSelected: (AppBarBehavior value) {
setState(() {
_appBarBehavior = value;
});
},
itemBuilder: (BuildContext context) =>
<PopupMenuItem<AppBarBehavior>>[
const PopupMenuItem<AppBarBehavior>(
value: AppBarBehavior.normal,
child: Text('App bar scrolls away')),
const PopupMenuItem<AppBarBehavior>(
value: AppBarBehavior.pinned,
child: Text('App bar stays put')),
const PopupMenuItem<AppBarBehavior>(
value: AppBarBehavior.floating,
child: Text('App bar floats')),
const PopupMenuItem<AppBarBehavior>(
value: AppBarBehavior.snapping,
child: Text('App bar snaps')),
],
),
],
flexibleSpace: FlexibleSpaceBar(
title: const Text('Ali Connors'),
background: Stack(
fit: StackFit.expand,
children: <Widget>[
Image.asset(
'people/ali_landscape.png',
// package: 'flutter_gallery_assets',
fit: BoxFit.cover,
height: _appBarHeight,
),
// This gradient ensures that the toolbar icons are distinct
// against the background image.
const DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment(0.0, -1.0),
end: Alignment(0.0, -0.4),
colors: <Color>[Color(0x60000000), Color(0x00000000)],
),
),
),
],
),
),
),
SliverList(
delegate: SliverChildListDelegate(<Widget>[
AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.dark,
child: _ContactCategory(
icon: Icons.call,
children: <Widget>[
_ContactItem(
icon: Icons.message,
tooltip: 'Send message',
onPressed: () {
_scaffoldKey.currentState.showSnackBar(const SnackBar(
content: Text(
'Pretend that this opened your SMS application.')));
},
lines: const <String>[
'(650) 555-1234',
'Mobile',
],
),
_ContactItem(
icon: Icons.message,
tooltip: 'Send message',
onPressed: () {
_scaffoldKey.currentState.showSnackBar(const SnackBar(
content: Text('A messaging app appears.')));
},
lines: const <String>[
'(323) 555-6789',
'Work',
],
),
_ContactItem(
icon: Icons.message,
tooltip: 'Send message',
onPressed: () {
_scaffoldKey.currentState.showSnackBar(const SnackBar(
content: Text(
'Imagine if you will, a messaging application.')));
},
lines: const <String>[
'(650) 555-6789',
'Home',
],
),
],
),
),
_ContactCategory(
icon: Icons.contact_mail,
children: <Widget>[
_ContactItem(
icon: Icons.email,
tooltip: 'Send personal e-mail',
onPressed: () {
_scaffoldKey.currentState.showSnackBar(const SnackBar(
content: Text(
'Here, your e-mail application would open.')));
},
lines: const <String>[
'ali_connors@example.com',
'Personal',
],
),
_ContactItem(
icon: Icons.email,
tooltip: 'Send work e-mail',
onPressed: () {
_scaffoldKey.currentState.showSnackBar(const SnackBar(
content: Text(
'Summon your favorite e-mail application here.')));
},
lines: const <String>[
'aliconnors@example.com',
'Work',
],
),
],
),
_ContactCategory(
icon: Icons.location_on,
children: <Widget>[
_ContactItem(
icon: Icons.map,
tooltip: 'Open map',
onPressed: () {
_scaffoldKey.currentState.showSnackBar(const SnackBar(
content: Text(
'This would show a map of San Francisco.')));
},
lines: const <String>[
'2000 Main Street',
'San Francisco, CA',
'Home',
],
),
_ContactItem(
icon: Icons.map,
tooltip: 'Open map',
onPressed: () {
_scaffoldKey.currentState.showSnackBar(const SnackBar(
content: Text(
'This would show a map of Mountain View.')));
},
lines: const <String>[
'1600 Amphitheater Parkway',
'Mountain View, CA',
'Work',
],
),
_ContactItem(
icon: Icons.map,
tooltip: 'Open map',
onPressed: () {
_scaffoldKey.currentState.showSnackBar(const SnackBar(
content: Text(
'This would also show a map, if this was not a demo.')));
},
lines: const <String>[
'126 Severyns Ave',
'Mountain View, CA',
'Jet Travel',
],
),
],
),
_ContactCategory(
icon: Icons.today,
children: <Widget>[
_ContactItem(
lines: const <String>[
'Birthday',
'January 9th, 1989',
],
),
_ContactItem(
lines: const <String>[
'Wedding anniversary',
'June 21st, 2014',
],
),
_ContactItem(
lines: const <String>[
'First day in office',
'January 20th, 2015',
],
),
_ContactItem(
lines: const <String>[
'Last day in office',
'August 9th, 2018',
],
),
],
),
]),
),
],
),
),
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,718 @@
// 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';
class PestoDemo extends StatelessWidget {
const PestoDemo({Key key}) : super(key: key);
static const String routeName = '/pesto';
@override
Widget build(BuildContext context) => PestoHome();
}
const String _kSmallLogoImage = 'logos/pesto/logo_small.png';
const double _kAppBarHeight = 128.0;
const double _kFabHalfSize =
28.0; // TODO(mpcomplete): needs to adapt to screen size
const double _kRecipePageMaxWidth = 500.0;
final Set<Recipe> _favoriteRecipes = Set<Recipe>();
final ThemeData _kTheme = ThemeData(
brightness: Brightness.light,
primarySwatch: Colors.teal,
accentColor: Colors.redAccent,
);
class PestoHome extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const RecipeGridPage(recipes: kPestoRecipes);
}
}
class PestoFavorites extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RecipeGridPage(recipes: _favoriteRecipes.toList());
}
}
class PestoStyle extends TextStyle {
const PestoStyle({
double fontSize = 12.0,
FontWeight fontWeight,
Color color = Colors.black87,
double letterSpacing,
double height,
}) : super(
inherit: false,
color: color,
fontFamily: 'Raleway',
fontSize: fontSize,
fontWeight: fontWeight,
textBaseline: TextBaseline.alphabetic,
letterSpacing: letterSpacing,
height: height,
);
}
// Displays a grid of recipe cards.
class RecipeGridPage extends StatefulWidget {
const RecipeGridPage({Key key, this.recipes}) : super(key: key);
final List<Recipe> recipes;
@override
_RecipeGridPageState createState() => _RecipeGridPageState();
}
class _RecipeGridPageState extends State<RecipeGridPage> {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
@override
Widget build(BuildContext context) {
final double statusBarHeight = MediaQuery.of(context).padding.top;
return Theme(
data: _kTheme.copyWith(platform: Theme.of(context).platform),
child: Scaffold(
key: scaffoldKey,
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.edit),
onPressed: () {
scaffoldKey.currentState.showSnackBar(const SnackBar(
content: Text('Not supported.'),
));
},
),
body: CustomScrollView(
semanticChildCount: widget.recipes.length,
slivers: <Widget>[
_buildAppBar(context, statusBarHeight),
_buildBody(context, statusBarHeight),
],
),
),
);
}
Widget _buildAppBar(BuildContext context, double statusBarHeight) {
return SliverAppBar(
pinned: true,
expandedHeight: _kAppBarHeight,
actions: <Widget>[
IconButton(
icon: const Icon(Icons.search),
tooltip: 'Search',
onPressed: () {
scaffoldKey.currentState.showSnackBar(const SnackBar(
content: Text('Not supported.'),
));
},
),
],
flexibleSpace: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final Size size = constraints.biggest;
final double appBarHeight = size.height - statusBarHeight;
final double t = (appBarHeight - kToolbarHeight) /
(_kAppBarHeight - kToolbarHeight);
final double extraPadding =
Tween<double>(begin: 10.0, end: 24.0).transform(t);
final double logoHeight = appBarHeight - 1.5 * extraPadding;
return Padding(
padding: EdgeInsets.only(
top: statusBarHeight + 0.5 * extraPadding,
bottom: extraPadding,
),
child: Center(
child: PestoLogo(height: logoHeight, t: t.clamp(0.0, 1.0))),
);
},
),
);
}
Widget _buildBody(BuildContext context, double statusBarHeight) {
final EdgeInsets mediaPadding = MediaQuery.of(context).padding;
final EdgeInsets padding = EdgeInsets.only(
top: 8.0,
left: 8.0 + mediaPadding.left,
right: 8.0 + mediaPadding.right,
bottom: 8.0);
return SliverPadding(
padding: padding,
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: _kRecipePageMaxWidth,
crossAxisSpacing: 8.0,
mainAxisSpacing: 8.0,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
final Recipe recipe = widget.recipes[index];
return RecipeCard(
recipe: recipe,
onTap: () {
showRecipePage(context, recipe);
},
);
},
childCount: widget.recipes.length,
),
),
);
}
void showFavoritesPage(BuildContext context) {
Navigator.push(
context,
MaterialPageRoute<void>(
settings: const RouteSettings(name: '/pesto/favorites'),
builder: (BuildContext context) => PestoFavorites(),
));
}
void showRecipePage(BuildContext context, Recipe recipe) {
Navigator.push(
context,
MaterialPageRoute<void>(
settings: const RouteSettings(name: '/pesto/recipe'),
builder: (BuildContext context) {
return Theme(
data: _kTheme.copyWith(platform: Theme.of(context).platform),
child: RecipePage(recipe: recipe),
);
},
));
}
}
class PestoLogo extends StatefulWidget {
const PestoLogo({this.height, this.t});
final double height;
final double t;
@override
_PestoLogoState createState() => _PestoLogoState();
}
class _PestoLogoState extends State<PestoLogo> {
// Native sizes for logo and its image/text components.
static const double kLogoHeight = 162.0;
static const double kLogoWidth = 220.0;
static const double kImageHeight = 108.0;
static const double kTextHeight = 48.0;
final TextStyle titleStyle = const PestoStyle(
fontSize: kTextHeight,
fontWeight: FontWeight.w900,
color: Colors.white,
letterSpacing: 3.0);
final RectTween _textRectTween = RectTween(
begin: Rect.fromLTWH(0.0, kLogoHeight, kLogoWidth, kTextHeight),
end: Rect.fromLTWH(0.0, kImageHeight, kLogoWidth, kTextHeight));
final Curve _textOpacity = const Interval(0.4, 1.0, curve: Curves.easeInOut);
final RectTween _imageRectTween = RectTween(
begin: Rect.fromLTWH(0.0, 0.0, kLogoWidth, kLogoHeight),
end: Rect.fromLTWH(0.0, 0.0, kLogoWidth, kImageHeight),
);
@override
Widget build(BuildContext context) {
return Semantics(
namesRoute: true,
child: Transform(
transform: Matrix4.identity()..scale(widget.height / kLogoHeight),
alignment: Alignment.topCenter,
child: SizedBox(
width: kLogoWidth,
child: Stack(
overflow: Overflow.visible,
children: <Widget>[
Positioned.fromRect(
rect: _imageRectTween.lerp(widget.t),
child: Image.asset(
'$_kSmallLogoImage',
fit: BoxFit.contain,
),
),
Positioned.fromRect(
rect: _textRectTween.lerp(widget.t),
child: Opacity(
opacity: _textOpacity.transform(widget.t),
child: Text('PESTO',
style: titleStyle, textAlign: TextAlign.center),
),
),
],
),
),
),
);
}
}
// A card with the recipe's image, author, and title.
class RecipeCard extends StatelessWidget {
const RecipeCard({Key key, this.recipe, this.onTap}) : super(key: key);
final Recipe recipe;
final VoidCallback onTap;
TextStyle get titleStyle =>
const PestoStyle(fontSize: 24.0, fontWeight: FontWeight.w600);
TextStyle get authorStyle =>
const PestoStyle(fontWeight: FontWeight.w500, color: Colors.black54);
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Hero(
tag: '${recipe.imagePath}',
child: AspectRatio(
aspectRatio: 4.0 / 3.0,
child: Image.asset(
'${recipe.imagePath}',
package: recipe.imagePackage,
fit: BoxFit.cover,
semanticLabel: recipe.name,
),
),
),
Expanded(
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16.0),
child: Image.asset(
'${recipe.ingredientsImagePath}',
package: recipe.ingredientsImagePackage,
width: 48.0,
height: 48.0,
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(recipe.name,
style: titleStyle,
softWrap: false,
overflow: TextOverflow.ellipsis),
Text(recipe.author, style: authorStyle),
],
),
),
],
),
),
],
),
),
);
}
}
// Displays one recipe. Includes the recipe sheet with a background image.
class RecipePage extends StatefulWidget {
const RecipePage({Key key, this.recipe}) : super(key: key);
final Recipe recipe;
@override
_RecipePageState createState() => _RecipePageState();
}
class _RecipePageState extends State<RecipePage> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final TextStyle menuItemStyle = const PestoStyle(
fontSize: 15.0, color: Colors.black54, height: 24.0 / 15.0);
double _getAppBarHeight(BuildContext context) =>
MediaQuery.of(context).size.height * 0.3;
@override
Widget build(BuildContext context) {
// The full page content with the recipe's image behind it. This
// adjusts based on the size of the screen. If the recipe sheet touches
// the edge of the screen, use a slightly different layout.
final double appBarHeight = _getAppBarHeight(context);
final Size screenSize = MediaQuery.of(context).size;
final bool fullWidth = screenSize.width < _kRecipePageMaxWidth;
final bool isFavorite = _favoriteRecipes.contains(widget.recipe);
return Scaffold(
key: _scaffoldKey,
body: Stack(
children: <Widget>[
Positioned(
top: 0.0,
left: 0.0,
right: 0.0,
height: appBarHeight + _kFabHalfSize,
child: Hero(
tag: '${widget.recipe.imagePath}',
child: Image.asset(
'${widget.recipe.imagePath}',
package: widget.recipe.imagePackage,
fit: fullWidth ? BoxFit.fitWidth : BoxFit.cover,
),
),
),
CustomScrollView(
slivers: <Widget>[
SliverAppBar(
expandedHeight: appBarHeight - _kFabHalfSize,
backgroundColor: Colors.transparent,
actions: <Widget>[
PopupMenuButton<String>(
onSelected: (String item) {},
itemBuilder: (BuildContext context) =>
<PopupMenuItem<String>>[
_buildMenuItem(Icons.share, 'Tweet recipe'),
_buildMenuItem(Icons.email, 'Email recipe'),
_buildMenuItem(Icons.message, 'Message recipe'),
_buildMenuItem(Icons.people, 'Share on Facebook'),
],
),
],
flexibleSpace: const FlexibleSpaceBar(
background: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment(0.0, -1.0),
end: Alignment(0.0, -0.2),
colors: <Color>[Color(0x60000000), Color(0x00000000)],
),
),
),
),
),
SliverToBoxAdapter(
child: Stack(
children: <Widget>[
Container(
padding: const EdgeInsets.only(top: _kFabHalfSize),
width: fullWidth ? null : _kRecipePageMaxWidth,
child: RecipeSheet(recipe: widget.recipe),
),
Positioned(
right: 16.0,
child: FloatingActionButton(
child: Icon(
isFavorite ? Icons.favorite : Icons.favorite_border),
onPressed: _toggleFavorite,
),
),
],
)),
],
),
],
),
);
}
PopupMenuItem<String> _buildMenuItem(IconData icon, String label) {
return PopupMenuItem<String>(
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(right: 24.0),
child: Icon(icon, color: Colors.black54)),
Text(label, style: menuItemStyle),
],
),
);
}
void _toggleFavorite() {
setState(() {
if (_favoriteRecipes.contains(widget.recipe))
_favoriteRecipes.remove(widget.recipe);
else
_favoriteRecipes.add(widget.recipe);
});
}
}
/// Displays the recipe's name and instructions.
class RecipeSheet extends StatelessWidget {
RecipeSheet({Key key, this.recipe}) : super(key: key);
final TextStyle titleStyle = const PestoStyle(fontSize: 34.0);
final TextStyle descriptionStyle = const PestoStyle(
fontSize: 15.0, color: Colors.black54, height: 24.0 / 15.0);
final TextStyle itemStyle =
const PestoStyle(fontSize: 15.0, height: 24.0 / 15.0);
final TextStyle itemAmountStyle = PestoStyle(
fontSize: 15.0, color: _kTheme.primaryColor, height: 24.0 / 15.0);
final TextStyle headingStyle = const PestoStyle(
fontSize: 16.0, fontWeight: FontWeight.bold, height: 24.0 / 15.0);
final Recipe recipe;
@override
Widget build(BuildContext context) {
return Material(
child: SafeArea(
top: false,
bottom: false,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 40.0),
child: Table(
columnWidths: const <int, TableColumnWidth>{
0: FixedColumnWidth(64.0)
},
children: <TableRow>[
TableRow(children: <Widget>[
TableCell(
verticalAlignment: TableCellVerticalAlignment.middle,
child: Image.asset('${recipe.ingredientsImagePath}',
package: recipe.ingredientsImagePackage,
width: 32.0,
height: 32.0,
alignment: Alignment.centerLeft,
fit: BoxFit.scaleDown)),
TableCell(
verticalAlignment: TableCellVerticalAlignment.middle,
child: Text(recipe.name, style: titleStyle)),
]),
TableRow(children: <Widget>[
const SizedBox(),
Padding(
padding: const EdgeInsets.only(top: 8.0, bottom: 4.0),
child: Text(recipe.description, style: descriptionStyle)),
]),
TableRow(children: <Widget>[
const SizedBox(),
Padding(
padding: const EdgeInsets.only(top: 24.0, bottom: 4.0),
child: Text('Ingredients', style: headingStyle)),
]),
]
..addAll(recipe.ingredients
.map<TableRow>((RecipeIngredient ingredient) {
return _buildItemRow(ingredient.amount, ingredient.description);
}))
..add(TableRow(children: <Widget>[
const SizedBox(),
Padding(
padding: const EdgeInsets.only(top: 24.0, bottom: 4.0),
child: Text('Steps', style: headingStyle)),
]))
..addAll(recipe.steps.map<TableRow>((RecipeStep step) {
return _buildItemRow(step.duration ?? '', step.description);
})),
),
),
),
);
}
TableRow _buildItemRow(String left, String right) {
return TableRow(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Text(left, style: itemAmountStyle),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Text(right, style: itemStyle),
),
],
);
}
}
class Recipe {
const Recipe(
{this.name,
this.author,
this.description,
this.imagePath,
this.imagePackage,
this.ingredientsImagePath,
this.ingredientsImagePackage,
this.ingredients,
this.steps});
final String name;
final String author;
final String description;
final String imagePath;
final String imagePackage;
final String ingredientsImagePath;
final String ingredientsImagePackage;
final List<RecipeIngredient> ingredients;
final List<RecipeStep> steps;
}
class RecipeIngredient {
const RecipeIngredient({this.amount, this.description});
final String amount;
final String description;
}
class RecipeStep {
const RecipeStep({this.duration, this.description});
final String duration;
final String description;
}
const List<Recipe> kPestoRecipes = <Recipe>[
Recipe(
name: 'Roasted Chicken',
author: 'Peter Carlsson',
ingredientsImagePath: 'food/icons/main.png',
description:
'The perfect dish to welcome your family and friends with on a crisp autumn night. Pair with roasted veggies to truly impress them.',
imagePath: 'food/roasted_chicken.png',
ingredients: <RecipeIngredient>[
RecipeIngredient(amount: '1 whole', description: 'Chicken'),
RecipeIngredient(amount: '1/2 cup', description: 'Butter'),
RecipeIngredient(amount: '1 tbsp', description: 'Onion powder'),
RecipeIngredient(amount: '1 tbsp', description: 'Freshly ground pepper'),
RecipeIngredient(amount: '1 tsp', description: 'Salt'),
],
steps: <RecipeStep>[
RecipeStep(duration: '1 min', description: 'Put in oven'),
RecipeStep(duration: '1hr 45 min', description: 'Cook'),
],
),
Recipe(
name: 'Chopped Beet Leaves',
author: 'Trevor Hansen',
ingredientsImagePath: 'food/icons/veggie.png',
description:
'This vegetable has more to offer than just its root. Beet greens can be tossed into a salad to add some variety or sauteed on its own with some oil and garlic.',
imagePath: 'food/chopped_beet_leaves.png',
ingredients: <RecipeIngredient>[
RecipeIngredient(amount: '3 cups', description: 'Beet greens'),
],
steps: <RecipeStep>[
RecipeStep(duration: '5 min', description: 'Chop'),
],
),
Recipe(
name: 'Pesto Pasta',
author: 'Ali Connors',
ingredientsImagePath: 'food/icons/main.png',
description:
'With this pesto recipe, you can quickly whip up a meal to satisfy your savory needs. And if you\'re feeling festive, you can add bacon to taste.',
imagePath: 'food/pesto_pasta.png',
ingredients: <RecipeIngredient>[
RecipeIngredient(amount: '1/4 cup ', description: 'Pasta'),
RecipeIngredient(amount: '2 cups', description: 'Fresh basil leaves'),
RecipeIngredient(amount: '1/2 cup', description: 'Parmesan cheese'),
RecipeIngredient(
amount: '1/2 cup', description: 'Extra virgin olive oil'),
RecipeIngredient(amount: '1/3 cup', description: 'Pine nuts'),
RecipeIngredient(amount: '1/4 cup', description: 'Lemon juice'),
RecipeIngredient(amount: '3 cloves', description: 'Garlic'),
RecipeIngredient(amount: '1/4 tsp', description: 'Salt'),
RecipeIngredient(amount: '1/8 tsp', description: 'Pepper'),
RecipeIngredient(amount: '3 lbs', description: 'Bacon'),
],
steps: <RecipeStep>[
RecipeStep(duration: '15 min', description: 'Blend'),
],
),
Recipe(
name: 'Cherry Pie',
author: 'Sandra Adams',
ingredientsImagePath: 'food/icons/main.png',
description:
'Sometimes when you\'re craving some cheer in your life you can jumpstart your day with some cherry pie. Dessert for breakfast is perfectly acceptable.',
imagePath: 'food/cherry_pie.png',
ingredients: <RecipeIngredient>[
RecipeIngredient(amount: '1', description: 'Pie crust'),
RecipeIngredient(
amount: '4 cups', description: 'Fresh or frozen cherries'),
RecipeIngredient(amount: '1 cup', description: 'Granulated sugar'),
RecipeIngredient(amount: '4 tbsp', description: 'Cornstarch'),
RecipeIngredient(amount: '1½ tbsp', description: 'Butter'),
],
steps: <RecipeStep>[
RecipeStep(duration: '15 min', description: 'Mix'),
RecipeStep(duration: '1hr 30 min', description: 'Bake'),
],
),
Recipe(
name: 'Spinach Salad',
author: 'Peter Carlsson',
ingredientsImagePath: 'food/icons/spicy.png',
description:
'Everyone\'s favorite leafy green is back. Paired with fresh sliced onion, it\'s ready to tackle any dish, whether it be a salad or an egg scramble.',
imagePath: 'food/spinach_onion_salad.png',
ingredients: <RecipeIngredient>[
RecipeIngredient(amount: '4 cups', description: 'Spinach'),
RecipeIngredient(amount: '1 cup', description: 'Sliced onion'),
],
steps: <RecipeStep>[
RecipeStep(duration: '5 min', description: 'Mix'),
],
),
Recipe(
name: 'Butternut Squash Soup',
author: 'Ali Connors',
ingredientsImagePath: 'food/icons/healthy.png',
description:
'This creamy butternut squash soup will warm you on the chilliest of winter nights and bring a delightful pop of orange to the dinner table.',
imagePath: 'food/butternut_squash_soup.png',
ingredients: <RecipeIngredient>[
RecipeIngredient(amount: '1', description: 'Butternut squash'),
RecipeIngredient(amount: '4 cups', description: 'Chicken stock'),
RecipeIngredient(amount: '2', description: 'Potatoes'),
RecipeIngredient(amount: '1', description: 'Onion'),
RecipeIngredient(amount: '1', description: 'Carrot'),
RecipeIngredient(amount: '1', description: 'Celery'),
RecipeIngredient(amount: '1 tsp', description: 'Salt'),
RecipeIngredient(amount: '1 tsp', description: 'Pepper'),
],
steps: <RecipeStep>[
RecipeStep(duration: '10 min', description: 'Prep vegetables'),
RecipeStep(duration: '5 min', description: 'Stir'),
RecipeStep(duration: '1 hr 10 min', description: 'Cook')
],
),
Recipe(
name: 'Spanakopita',
author: 'Trevor Hansen',
ingredientsImagePath: 'food/icons/quick.png',
description:
'You \'feta\' believe this is a crowd-pleaser! Flaky phyllo pastry surrounds a delicious mixture of spinach and cheeses to create the perfect appetizer.',
imagePath: 'food/spanakopita.png',
ingredients: <RecipeIngredient>[
RecipeIngredient(amount: '1 lb', description: 'Spinach'),
RecipeIngredient(amount: '½ cup', description: 'Feta cheese'),
RecipeIngredient(amount: '½ cup', description: 'Cottage cheese'),
RecipeIngredient(amount: '2', description: 'Eggs'),
RecipeIngredient(amount: '1', description: 'Onion'),
RecipeIngredient(amount: '½ lb', description: 'Phyllo dough'),
],
steps: <RecipeStep>[
RecipeStep(duration: '5 min', description: 'Sauté vegetables'),
RecipeStep(
duration: '3 min',
description: 'Stir vegetables and other filling ingredients'),
RecipeStep(
duration: '10 min',
description: 'Fill phyllo squares half-full with filling and fold.'),
RecipeStep(duration: '40 min', description: 'Bake')
],
),
];

View File

@@ -0,0 +1,254 @@
// 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 'shrine_types.dart';
const String _kGalleryAssetsPackage = null;
const Vendor _ali = Vendor(
name: 'Alis shop',
avatarAsset: 'people/square/ali.png',
avatarAssetPackage: _kGalleryAssetsPackage,
description:
'Ali Connors makes custom goods for folks of all shapes and sizes '
'made by hand and sometimes by machine, but always with love and care. '
'Custom orders are available upon request if you need something extra special.');
const Vendor _peter = Vendor(
name: 'Peters shop',
avatarAsset: 'people/square/peter.png',
avatarAssetPackage: _kGalleryAssetsPackage,
description:
'Peter makes great stuff for awesome people like you. Super cool and extra '
'awesome all of his shops goods are handmade with love. Custom orders are '
'available upon request if you need something extra special.');
const Vendor _sandra = Vendor(
name: 'Sandras shop',
avatarAsset: 'people/square/sandra.png',
avatarAssetPackage: _kGalleryAssetsPackage,
description:
'Sandra specializes in furniture, beauty and travel products with a classic vibe. '
'Custom orders are available if youre looking for a certain color or material.');
const Vendor _stella = Vendor(
name: 'Stellas shop',
avatarAsset: 'people/square/stella.png',
avatarAssetPackage: _kGalleryAssetsPackage,
description:
'Stella sells awesome stuff at lovely prices. made by hand and sometimes by '
'machine, but always with love and care. Custom orders are available upon request '
'if you need something extra special.');
const Vendor _trevor = Vendor(
name: 'Trevors shop',
avatarAsset: 'people/square/trevor.png',
avatarAssetPackage: _kGalleryAssetsPackage,
description:
'Trevor makes great stuff for awesome people like you. Super cool and extra '
'awesome all of his shops goods are handmade with love. Custom orders are '
'available upon request if you need something extra special.');
const List<Product> _allProducts = <Product>[
Product(
name: 'Vintage Brown Belt',
imageAsset: 'products/belt.png',
imageAssetPackage: _kGalleryAssetsPackage,
categories: <String>['fashion', 'latest'],
price: 300.00,
vendor: _sandra,
description:
'Isnt it cool when things look old, but they\'re not. Looks Old But Not makes '
'awesome vintage goods that are super smart. This ol belt just got an upgrade. '),
Product(
name: 'Sunglasses',
imageAsset: 'products/sunnies.png',
imageAssetPackage: _kGalleryAssetsPackage,
categories: <String>['travel', 'fashion', 'beauty'],
price: 20.00,
vendor: _trevor,
description:
'Be an optimist. Carry Sunglasses with you at all times. All Tints and '
'Shades products come with polarized lenses and super duper UV protection '
'so you can look at the sun for however long you want. Sunglasses make you '
'look cool, wear them.'),
Product(
name: 'Flatwear',
imageAsset: 'products/flatwear.png',
imageAssetPackage: _kGalleryAssetsPackage,
categories: <String>['furniture'],
price: 30.00,
vendor: _trevor,
description:
'Leave the tunnel and the rain is fallin amazing things happen when you wait'),
Product(
name: 'Salmon Sweater',
imageAsset: 'products/sweater.png',
imageAssetPackage: _kGalleryAssetsPackage,
categories: <String>['fashion'],
price: 300.00,
vendor: _stella,
description:
'Looks can be deceiving. This sweater comes in a wide variety of '
'flavors, including salmon, that pop as soon as they hit your eyes. '
'Sweaters heat quickly, so savor the warmth.'),
Product(
name: 'Pine Table',
imageAsset: 'products/table.png',
imageAssetPackage: _kGalleryAssetsPackage,
categories: <String>['furniture'],
price: 63.00,
vendor: _stella,
description:
'Leave the tunnel and the rain is fallin amazing things happen when you wait'),
Product(
name: 'Green Comfort Jacket',
imageAsset: 'products/jacket.png',
imageAssetPackage: _kGalleryAssetsPackage,
categories: <String>['fashion'],
price: 36.00,
vendor: _ali,
description:
'Leave the tunnel and the rain is fallin amazing things happen when you wait'),
Product(
name: 'Chambray Top',
imageAsset: 'products/top.png',
imageAssetPackage: _kGalleryAssetsPackage,
categories: <String>['fashion'],
price: 125.00,
vendor: _peter,
description:
'Leave the tunnel and the rain is fallin amazing things happen when you wait'),
Product(
name: 'Blue Cup',
imageAsset: 'products/cup.png',
imageAssetPackage: _kGalleryAssetsPackage,
categories: <String>['travel', 'furniture'],
price: 75.00,
vendor: _sandra,
description:
'Drinksy has been making extraordinary mugs for decades. With each '
'cup purchased Drinksy donates a cup to those in need. Buy yourself a mug, '
'buy someone else a mug.'),
Product(
name: 'Tea Set',
imageAsset: 'products/teaset.png',
imageAssetPackage: _kGalleryAssetsPackage,
categories: <String>['furniture', 'fashion'],
price: 70.00,
vendor: _trevor,
featureTitle: 'Beautiful glass teapot',
featureDescription:
'Teapot holds extremely hot liquids and pours them from the spout.',
description:
'Impress your guests with Tea Set by Kitchen Stuff. Teapot holds extremely '
'hot liquids and pours them from the spout. Use the handle, shown on the right, '
'so your fingers dont get burnt while pouring.'),
Product(
name: 'Blue linen napkins',
imageAsset: 'products/napkins.png',
imageAssetPackage: _kGalleryAssetsPackage,
categories: <String>['furniture', 'fashion'],
price: 89.00,
vendor: _trevor,
description:
'Blue linen napkins were meant to go with friends, so you may want to pick '
'up a bunch of these. These things are absorbant.'),
Product(
name: 'Dipped Earrings',
imageAsset: 'products/earrings.png',
imageAssetPackage: _kGalleryAssetsPackage,
categories: <String>['fashion', 'beauty'],
price: 25.00,
vendor: _stella,
description:
'WeDipIt does it again. These hand-dipped 4 inch earrings are perfect for '
'the office or the beach. Just be sure you dont drop it in a bucket of '
'red paint, then they wont look dipped anymore.'),
Product(
name: 'Perfect Planters',
imageAsset: 'products/planters.png',
imageAssetPackage: _kGalleryAssetsPackage,
categories: <String>['latest', 'furniture'],
price: 30.00,
vendor: _ali,
description:
'The Perfect Planter Co makes the best vessels for just about anything you '
'can pot. This set of Perfect Planters holds succulents and cuttings perfectly. '
'Looks great in any room. Keep out of reach from cats.'),
Product(
name: 'Cloud-White Dress',
imageAsset: 'products/dress.png',
imageAssetPackage: _kGalleryAssetsPackage,
categories: <String>['fashion'],
price: 54.00,
vendor: _sandra,
description:
'Trying to find the perfect outift to match your mood? Try no longer. '
'This Cloud-White Dress has you covered for those nights when you need '
'to get out, or even if youre just headed to work.'),
Product(
name: 'Backpack',
imageAsset: 'products/backpack.png',
imageAssetPackage: _kGalleryAssetsPackage,
categories: <String>['travel', 'fashion'],
price: 25.00,
vendor: _peter,
description:
'This backpack by Bags n stuff can hold just about anything: a laptop, '
'a pen, a protractor, notebooks, small animals, plugs for your devices, '
'sunglasses, gym clothes, shoes, gloves, two kittens, and even lunch!'),
Product(
name: 'Charcoal Straw Hat',
imageAsset: 'products/hat.png',
imageAssetPackage: _kGalleryAssetsPackage,
categories: <String>['travel', 'fashion', 'latest'],
price: 25.00,
vendor: _ali,
description:
'This is the helmet for those warm summer days on the road. '
'Jetset approved, these hats have been rigorously tested. Keep that face '
'protected from the sun.'),
Product(
name: 'Ginger Scarf',
imageAsset: 'products/scarf.png',
imageAssetPackage: _kGalleryAssetsPackage,
categories: <String>['latest', 'fashion'],
price: 17.00,
vendor: _peter,
description:
'Leave the tunnel and the rain is fallin amazing things happen when you wait'),
Product(
name: 'Blush Sweats',
imageAsset: 'products/sweats.png',
imageAssetPackage: _kGalleryAssetsPackage,
categories: <String>['travel', 'fashion', 'latest'],
price: 25.00,
vendor: _stella,
description:
'Leave the tunnel and the rain is fallin amazing things happen when you wait'),
Product(
name: 'Mint Jumper',
imageAsset: 'products/jumper.png',
imageAssetPackage: _kGalleryAssetsPackage,
categories: <String>['travel', 'fashion', 'beauty'],
price: 25.00,
vendor: _peter,
description:
'Leave the tunnel and the rain is fallin amazing things happen when you wait'),
Product(
name: 'Ochre Shirt',
imageAsset: 'products/shirt.png',
imageAssetPackage: _kGalleryAssetsPackage,
categories: <String>['fashion', 'latest'],
price: 120.00,
vendor: _stella,
description:
'Leave the tunnel and the rain is fallin amazing things happen when you wait')
];
List<Product> allProducts() {
assert(_allProducts.every((Product product) => product.isValid()));
return List<Product>.unmodifiable(_allProducts);
}

View File

@@ -0,0 +1,434 @@
// 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/rendering.dart';
import 'package:meta/meta.dart';
import 'shrine_data.dart';
import 'shrine_order.dart';
import 'shrine_page.dart';
import 'shrine_theme.dart';
import 'shrine_types.dart';
const double unitSize = kToolbarHeight;
final List<Product> _products = List<Product>.from(allProducts());
final Map<Product, Order> _shoppingCart = <Product, Order>{};
const int _childrenPerBlock = 8;
const int _rowsPerBlock = 5;
int _minIndexInRow(int rowIndex) {
final int blockIndex = rowIndex ~/ _rowsPerBlock;
return const <int>[0, 2, 4, 6, 7][rowIndex % _rowsPerBlock] +
blockIndex * _childrenPerBlock;
}
int _maxIndexInRow(int rowIndex) {
final int blockIndex = rowIndex ~/ _rowsPerBlock;
return const <int>[1, 3, 5, 6, 7][rowIndex % _rowsPerBlock] +
blockIndex * _childrenPerBlock;
}
int _rowAtIndex(int index) {
final int blockCount = index ~/ _childrenPerBlock;
return const <int>[
0,
0,
1,
1,
2,
2,
3,
4
][index - blockCount * _childrenPerBlock] +
blockCount * _rowsPerBlock;
}
int _columnAtIndex(int index) {
return const <int>[0, 1, 0, 1, 0, 1, 0, 0][index % _childrenPerBlock];
}
int _columnSpanAtIndex(int index) {
return const <int>[1, 1, 1, 1, 1, 1, 2, 2][index % _childrenPerBlock];
}
// The Shrine home page arranges the product cards into two columns. The card
// on every 4th and 5th row spans two columns.
class _ShrineGridLayout extends SliverGridLayout {
const _ShrineGridLayout({
@required this.rowStride,
@required this.columnStride,
@required this.tileHeight,
@required this.tileWidth,
});
final double rowStride;
final double columnStride;
final double tileHeight;
final double tileWidth;
@override
int getMinChildIndexForScrollOffset(double scrollOffset) {
return _minIndexInRow(scrollOffset ~/ rowStride);
}
@override
int getMaxChildIndexForScrollOffset(double scrollOffset) {
return _maxIndexInRow(scrollOffset ~/ rowStride);
}
@override
SliverGridGeometry getGeometryForChildIndex(int index) {
final int row = _rowAtIndex(index);
final int column = _columnAtIndex(index);
final int columnSpan = _columnSpanAtIndex(index);
return SliverGridGeometry(
scrollOffset: row * rowStride,
crossAxisOffset: column * columnStride,
mainAxisExtent: tileHeight,
crossAxisExtent: tileWidth + (columnSpan - 1) * columnStride,
);
}
@override
double computeMaxScrollOffset(int childCount) {
if (childCount == 0) return 0.0;
final int rowCount = _rowAtIndex(childCount - 1) + 1;
final double rowSpacing = rowStride - tileHeight;
return rowStride * rowCount - rowSpacing;
}
}
class _ShrineGridDelegate extends SliverGridDelegate {
static const double _spacing = 8.0;
@override
SliverGridLayout getLayout(SliverConstraints constraints) {
final double tileWidth = (constraints.crossAxisExtent - _spacing) / 2.0;
const double tileHeight = 40.0 + 144.0 + 40.0;
return _ShrineGridLayout(
tileWidth: tileWidth,
tileHeight: tileHeight,
rowStride: tileHeight + _spacing,
columnStride: tileWidth + _spacing,
);
}
@override
bool shouldRelayout(covariant SliverGridDelegate oldDelegate) => false;
}
// Displays the Vendor's name and avatar.
class _VendorItem extends StatelessWidget {
const _VendorItem({Key key, @required this.vendor})
: assert(vendor != null),
super(key: key);
final Vendor vendor;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 24.0,
child: Row(
children: <Widget>[
SizedBox(
width: 24.0,
child: ClipRRect(
borderRadius: BorderRadius.circular(12.0),
child: Image.asset(
vendor.avatarAsset,
package: vendor.avatarAssetPackage,
fit: BoxFit.cover,
),
),
),
const SizedBox(width: 8.0),
Expanded(
child: Text(vendor.name,
style: ShrineTheme.of(context).vendorItemStyle),
),
],
),
);
}
}
// Displays the product's price. If the product is in the shopping cart then the
// background is highlighted.
abstract class _PriceItem extends StatelessWidget {
const _PriceItem({Key key, @required this.product})
: assert(product != null),
super(key: key);
final Product product;
Widget buildItem(BuildContext context, TextStyle style, EdgeInsets padding) {
BoxDecoration decoration;
if (_shoppingCart[product] != null)
decoration =
BoxDecoration(color: ShrineTheme.of(context).priceHighlightColor);
return Container(
padding: padding,
decoration: decoration,
child: Text(product.priceString, style: style),
);
}
}
class _ProductPriceItem extends _PriceItem {
const _ProductPriceItem({Key key, Product product})
: super(key: key, product: product);
@override
Widget build(BuildContext context) {
return buildItem(
context,
ShrineTheme.of(context).priceStyle,
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
);
}
}
class _FeaturePriceItem extends _PriceItem {
const _FeaturePriceItem({Key key, Product product})
: super(key: key, product: product);
@override
Widget build(BuildContext context) {
return buildItem(
context,
ShrineTheme.of(context).featurePriceStyle,
const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
);
}
}
class _HeadingLayout extends MultiChildLayoutDelegate {
_HeadingLayout();
static const String price = 'price';
static const String image = 'image';
static const String title = 'title';
static const String description = 'description';
static const String vendor = 'vendor';
@override
void performLayout(Size size) {
final Size priceSize = layoutChild(price, BoxConstraints.loose(size));
positionChild(price, Offset(size.width - priceSize.width, 0.0));
final double halfWidth = size.width / 2.0;
final double halfHeight = size.height / 2.0;
const double halfUnit = unitSize / 2.0;
const double margin = 16.0;
final Size imageSize = layoutChild(image, BoxConstraints.loose(size));
final double imageX = imageSize.width < halfWidth - halfUnit
? halfWidth / 2.0 - imageSize.width / 2.0 - halfUnit
: halfWidth - imageSize.width;
positionChild(image, Offset(imageX, halfHeight - imageSize.height / 2.0));
final double maxTitleWidth = halfWidth + unitSize - margin;
final BoxConstraints titleBoxConstraints =
BoxConstraints(maxWidth: maxTitleWidth);
final Size titleSize = layoutChild(title, titleBoxConstraints);
final double titleX = halfWidth - unitSize;
final double titleY = halfHeight - titleSize.height;
positionChild(title, Offset(titleX, titleY));
final Size descriptionSize = layoutChild(description, titleBoxConstraints);
final double descriptionY = titleY + titleSize.height + margin;
positionChild(description, Offset(titleX, descriptionY));
layoutChild(vendor, titleBoxConstraints);
final double vendorY = descriptionY + descriptionSize.height + margin;
positionChild(vendor, Offset(titleX, vendorY));
}
@override
bool shouldRelayout(_HeadingLayout oldDelegate) => false;
}
// A card that highlights the "featured" catalog item.
class _Heading extends StatelessWidget {
_Heading({Key key, @required this.product})
: assert(product != null),
assert(product.featureTitle != null),
assert(product.featureDescription != null),
super(key: key);
final Product product;
@override
Widget build(BuildContext context) {
final Size screenSize = MediaQuery.of(context).size;
final ShrineTheme theme = ShrineTheme.of(context);
return MergeSemantics(
child: SizedBox(
height: screenSize.width > screenSize.height
? (screenSize.height - kToolbarHeight) * 0.85
: (screenSize.height - kToolbarHeight) * 0.70,
child: Container(
decoration: BoxDecoration(
color: theme.cardBackgroundColor,
border: Border(bottom: BorderSide(color: theme.dividerColor)),
),
child: CustomMultiChildLayout(
delegate: _HeadingLayout(),
children: <Widget>[
LayoutId(
id: _HeadingLayout.price,
child: _FeaturePriceItem(product: product),
),
LayoutId(
id: _HeadingLayout.image,
child: Image.asset(
product.imageAsset,
package: product.imageAssetPackage,
fit: BoxFit.cover,
),
),
LayoutId(
id: _HeadingLayout.title,
child:
Text(product.featureTitle, style: theme.featureTitleStyle),
),
LayoutId(
id: _HeadingLayout.description,
child:
Text(product.featureDescription, style: theme.featureStyle),
),
LayoutId(
id: _HeadingLayout.vendor,
child: _VendorItem(vendor: product.vendor),
),
],
),
),
),
);
}
}
// A card that displays a product's image, price, and vendor. The _ProductItem
// cards appear in a grid below the heading.
class _ProductItem extends StatelessWidget {
const _ProductItem({Key key, @required this.product, this.onPressed})
: assert(product != null),
super(key: key);
final Product product;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return MergeSemantics(
child: Card(
child: Stack(
children: <Widget>[
Column(
children: <Widget>[
Align(
alignment: Alignment.centerRight,
child: _ProductPriceItem(product: product),
),
Container(
width: 144.0,
height: 144.0,
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Hero(
tag: product.tag,
child: Image.asset(
product.imageAsset,
package: product.imageAssetPackage,
fit: BoxFit.contain,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: _VendorItem(vendor: product.vendor),
),
],
),
Material(
type: MaterialType.transparency,
child: InkWell(onTap: onPressed),
),
],
),
),
);
}
}
// The Shrine app's home page. Displays the featured item above a grid
// of the product items.
class ShrineHome extends StatefulWidget {
@override
_ShrineHomeState createState() => _ShrineHomeState();
}
class _ShrineHomeState extends State<ShrineHome> {
static final GlobalKey<ScaffoldState> _scaffoldKey =
GlobalKey<ScaffoldState>(debugLabel: 'Shrine Home');
static final _ShrineGridDelegate gridDelegate = _ShrineGridDelegate();
Future<void> _showOrderPage(Product product) async {
final Order order = _shoppingCart[product] ?? Order(product: product);
final Order completedOrder = await Navigator.push(
context,
ShrineOrderRoute(
order: order,
builder: (BuildContext context) {
return OrderPage(
order: order,
products: _products,
shoppingCart: _shoppingCart,
);
}));
assert(completedOrder.product != null);
if (completedOrder.quantity == 0)
_shoppingCart.remove(completedOrder.product);
}
@override
Widget build(BuildContext context) {
final Product featured = _products
.firstWhere((Product product) => product.featureDescription != null);
return ShrinePage(
scaffoldKey: _scaffoldKey,
products: _products,
shoppingCart: _shoppingCart,
body: CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(child: _Heading(product: featured)),
SliverSafeArea(
top: false,
minimum: const EdgeInsets.all(16.0),
sliver: SliverGrid(
gridDelegate: gridDelegate,
delegate: SliverChildListDelegate(
_products.map<Widget>((Product product) {
return _ProductItem(
product: product,
onPressed: () {
_showOrderPage(product);
},
);
}).toList(),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,353 @@
// 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 '../shrine_demo.dart' show ShrinePageRoute;
import 'shrine_page.dart';
import 'shrine_theme.dart';
import 'shrine_types.dart';
// Displays the product title's, description, and order quantity dropdown.
class _ProductItem extends StatelessWidget {
const _ProductItem({
Key key,
@required this.product,
@required this.quantity,
@required this.onChanged,
}) : assert(product != null),
assert(quantity != null),
assert(onChanged != null),
super(key: key);
final Product product;
final int quantity;
final ValueChanged<int> onChanged;
@override
Widget build(BuildContext context) {
final ShrineTheme theme = ShrineTheme.of(context);
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(product.name, style: theme.featureTitleStyle),
const SizedBox(height: 24.0),
Text(product.description, style: theme.featureStyle),
const SizedBox(height: 16.0),
Padding(
padding: const EdgeInsets.only(top: 8.0, bottom: 8.0, right: 88.0),
child: DropdownButtonHideUnderline(
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: const Color(0xFFD9D9D9),
),
),
child: DropdownButton<int>(
items: <int>[0, 1, 2, 3, 4, 5]
.map<DropdownMenuItem<int>>((int value) {
return DropdownMenuItem<int>(
value: value,
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text('Quantity $value',
style: theme.quantityMenuStyle),
),
);
}).toList(),
value: quantity,
onChanged: onChanged,
),
),
),
),
],
);
}
}
// Vendor name and description
class _VendorItem extends StatelessWidget {
const _VendorItem({Key key, @required this.vendor})
: assert(vendor != null),
super(key: key);
final Vendor vendor;
@override
Widget build(BuildContext context) {
final ShrineTheme theme = ShrineTheme.of(context);
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
SizedBox(
height: 24.0,
child: Align(
alignment: Alignment.bottomLeft,
child: Text(vendor.name, style: theme.vendorTitleStyle),
),
),
const SizedBox(height: 16.0),
Text(vendor.description, style: theme.vendorStyle),
],
);
}
}
// Layout the order page's heading: the product's image, the
// title/description/dropdown product item, and the vendor item.
class _HeadingLayout extends MultiChildLayoutDelegate {
_HeadingLayout();
static const String image = 'image';
static const String icon = 'icon';
static const String product = 'product';
static const String vendor = 'vendor';
@override
void performLayout(Size size) {
const double margin = 56.0;
final bool landscape = size.width > size.height;
final double imageWidth =
(landscape ? size.width / 2.0 : size.width) - margin * 2.0;
final BoxConstraints imageConstraints =
BoxConstraints(maxHeight: 224.0, maxWidth: imageWidth);
final Size imageSize = layoutChild(image, imageConstraints);
const double imageY = 0.0;
positionChild(image, const Offset(margin, imageY));
final double productWidth =
landscape ? size.width / 2.0 : size.width - margin;
final BoxConstraints productConstraints =
BoxConstraints(maxWidth: productWidth);
final Size productSize = layoutChild(product, productConstraints);
final double productX = landscape ? size.width / 2.0 : margin;
final double productY = landscape ? 0.0 : imageY + imageSize.height + 16.0;
positionChild(product, Offset(productX, productY));
final Size iconSize = layoutChild(icon, BoxConstraints.loose(size));
positionChild(
icon, Offset(productX - iconSize.width - 16.0, productY + 8.0));
final double vendorWidth = landscape ? size.width - margin : productWidth;
layoutChild(vendor, BoxConstraints(maxWidth: vendorWidth));
final double vendorX = landscape ? margin : productX;
final double vendorY = productY + productSize.height + 16.0;
positionChild(vendor, Offset(vendorX, vendorY));
}
@override
bool shouldRelayout(_HeadingLayout oldDelegate) => true;
}
// Describes a product and vendor in detail, supports specifying
// a order quantity (0-5). Appears at the top of the OrderPage.
class _Heading extends StatelessWidget {
const _Heading({
Key key,
@required this.product,
@required this.quantity,
this.quantityChanged,
}) : assert(product != null),
assert(quantity != null && quantity >= 0 && quantity <= 5),
super(key: key);
final Product product;
final int quantity;
final ValueChanged<int> quantityChanged;
@override
Widget build(BuildContext context) {
final Size screenSize = MediaQuery.of(context).size;
return SizedBox(
height: (screenSize.height - kToolbarHeight) * 1.35,
child: Material(
type: MaterialType.card,
elevation: 0.0,
child: Padding(
padding: const EdgeInsets.only(
left: 16.0, top: 18.0, right: 16.0, bottom: 24.0),
child: CustomMultiChildLayout(
delegate: _HeadingLayout(),
children: <Widget>[
LayoutId(
id: _HeadingLayout.image,
child: Hero(
tag: product.tag,
child: Image.asset(
product.imageAsset,
package: product.imageAssetPackage,
fit: BoxFit.contain,
alignment: Alignment.center,
),
),
),
LayoutId(
id: _HeadingLayout.icon,
child: const Icon(
Icons.info_outline,
size: 24.0,
color: Color(0xFFFFE0E0),
),
),
LayoutId(
id: _HeadingLayout.product,
child: _ProductItem(
product: product,
quantity: quantity,
onChanged: quantityChanged,
),
),
LayoutId(
id: _HeadingLayout.vendor,
child: _VendorItem(vendor: product.vendor),
),
],
),
),
),
);
}
}
class OrderPage extends StatefulWidget {
OrderPage({
Key key,
@required this.order,
@required this.products,
@required this.shoppingCart,
}) : assert(order != null),
assert(products != null && products.isNotEmpty),
assert(shoppingCart != null),
super(key: key);
final Order order;
final List<Product> products;
final Map<Product, Order> shoppingCart;
@override
_OrderPageState createState() => _OrderPageState();
}
// Displays a product's heading above photos of all of the other products
// arranged in two columns. Enables the user to specify a quantity and add an
// order to the shopping cart.
class _OrderPageState extends State<OrderPage> {
GlobalKey<ScaffoldState> scaffoldKey;
@override
void initState() {
super.initState();
scaffoldKey =
GlobalKey<ScaffoldState>(debugLabel: 'Shrine Order ${widget.order}');
}
Order get currentOrder => ShrineOrderRoute.of(context).order;
set currentOrder(Order value) {
ShrineOrderRoute.of(context).order = value;
}
void updateOrder({int quantity, bool inCart}) {
final Order newOrder =
currentOrder.copyWith(quantity: quantity, inCart: inCart);
if (currentOrder != newOrder) {
setState(() {
widget.shoppingCart[newOrder.product] = newOrder;
currentOrder = newOrder;
});
}
}
void showSnackBarMessage(String message) {
scaffoldKey.currentState.showSnackBar(SnackBar(content: Text(message)));
}
@override
Widget build(BuildContext context) {
return ShrinePage(
scaffoldKey: scaffoldKey,
products: widget.products,
shoppingCart: widget.shoppingCart,
floatingActionButton: FloatingActionButton(
onPressed: () {
updateOrder(inCart: true);
final int n = currentOrder.quantity;
final String item = currentOrder.product.name;
showSnackBarMessage(
'There ${n == 1 ? "is one $item item" : "are $n $item items"} in the shopping cart.');
},
backgroundColor: const Color(0xFF16F0F0),
tooltip: 'Add to cart',
child: const Icon(
Icons.add_shopping_cart,
color: Colors.black,
),
),
body: CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: _Heading(
product: widget.order.product,
quantity: currentOrder.quantity,
quantityChanged: (int value) {
updateOrder(quantity: value);
},
),
),
SliverSafeArea(
top: false,
minimum: const EdgeInsets.fromLTRB(8.0, 32.0, 8.0, 8.0),
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 248.0,
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
),
delegate: SliverChildListDelegate(
widget.products
.where((Product product) => product != widget.order.product)
.map((Product product) {
return Card(
elevation: 1.0,
child: Image.asset(
product.imageAsset,
package: product.imageAssetPackage,
fit: BoxFit.contain,
),
);
}).toList(),
),
),
),
],
),
);
}
}
// Displays a full-screen modal OrderPage.
//
// The order field will be replaced each time the user reconfigures the order.
// When the user backs out of this route the completer's value will be the
// final value of the order field.
class ShrineOrderRoute extends ShrinePageRoute<Order> {
ShrineOrderRoute({
@required this.order,
WidgetBuilder builder,
RouteSettings settings,
}) : assert(order != null),
super(builder: builder, settings: settings);
Order order;
@override
Order get currentResult => order;
static ShrineOrderRoute of(BuildContext context) =>
ModalRoute.of<Order>(context);
}

View File

@@ -0,0 +1,137 @@
// 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 'shrine_theme.dart';
import 'shrine_types.dart';
enum ShrineAction { sortByPrice, sortByProduct, emptyCart }
class ShrinePage extends StatefulWidget {
const ShrinePage(
{Key key,
@required this.scaffoldKey,
@required this.body,
this.floatingActionButton,
this.products,
this.shoppingCart})
: assert(body != null),
assert(scaffoldKey != null),
super(key: key);
final GlobalKey<ScaffoldState> scaffoldKey;
final Widget body;
final Widget floatingActionButton;
final List<Product> products;
final Map<Product, Order> shoppingCart;
@override
ShrinePageState createState() => ShrinePageState();
}
/// Defines the Scaffold, AppBar, etc that the demo pages have in common.
class ShrinePageState extends State<ShrinePage> {
double _appBarElevation = 0.0;
bool _handleScrollNotification(ScrollNotification notification) {
final double elevation =
notification.metrics.extentBefore <= 0.0 ? 0.0 : 1.0;
if (elevation != _appBarElevation) {
setState(() {
_appBarElevation = elevation;
});
}
return false;
}
void _showShoppingCart() {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
if (widget.shoppingCart.isEmpty) {
return const Padding(
padding: EdgeInsets.all(24.0),
child: Text('The shopping cart is empty'));
}
return ListView(
padding: kMaterialListPadding,
children: widget.shoppingCart.values.map((Order order) {
return ListTile(
title: Text(order.product.name),
leading: Text('${order.quantity}'),
subtitle: Text(order.product.vendor.name));
}).toList(),
);
});
}
void _sortByPrice() {
widget.products.sort((Product a, Product b) => a.price.compareTo(b.price));
}
void _sortByProduct() {
widget.products.sort((Product a, Product b) => a.name.compareTo(b.name));
}
void _emptyCart() {
widget.shoppingCart.clear();
widget.scaffoldKey.currentState
.showSnackBar(const SnackBar(content: Text('Shopping cart is empty')));
}
@override
Widget build(BuildContext context) {
final ShrineTheme theme = ShrineTheme.of(context);
return Scaffold(
key: widget.scaffoldKey,
appBar: AppBar(
elevation: _appBarElevation,
backgroundColor: theme.appBarBackgroundColor,
iconTheme: Theme.of(context).iconTheme,
brightness: Brightness.light,
flexibleSpace: Container(
decoration: BoxDecoration(
border:
Border(bottom: BorderSide(color: theme.dividerColor)))),
title:
Text('SHRINE', style: ShrineTheme.of(context).appBarTitleStyle),
centerTitle: true,
actions: <Widget>[
IconButton(
icon: const Icon(Icons.shopping_cart),
tooltip: 'Shopping cart',
onPressed: _showShoppingCart),
PopupMenuButton<ShrineAction>(
itemBuilder: (BuildContext context) =>
<PopupMenuItem<ShrineAction>>[
const PopupMenuItem<ShrineAction>(
value: ShrineAction.sortByPrice,
child: Text('Sort by price')),
const PopupMenuItem<ShrineAction>(
value: ShrineAction.sortByProduct,
child: Text('Sort by product')),
const PopupMenuItem<ShrineAction>(
value: ShrineAction.emptyCart,
child: Text('Empty shopping cart'))
],
onSelected: (ShrineAction action) {
switch (action) {
case ShrineAction.sortByPrice:
setState(_sortByPrice);
break;
case ShrineAction.sortByProduct:
setState(_sortByProduct);
break;
case ShrineAction.emptyCart:
setState(_emptyCart);
break;
}
})
]),
floatingActionButton: widget.floatingActionButton,
body: NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification, child: widget.body));
}
}

View File

@@ -0,0 +1,76 @@
// 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 ShrineStyle extends TextStyle {
const ShrineStyle.roboto(double size, FontWeight weight, Color color)
: super(
inherit: false,
color: color,
fontSize: size,
fontWeight: weight,
textBaseline: TextBaseline.alphabetic);
const ShrineStyle.abrilFatface(double size, FontWeight weight, Color color)
: super(
inherit: false,
color: color,
fontFamily: 'AbrilFatface',
fontSize: size,
fontWeight: weight,
textBaseline: TextBaseline.alphabetic);
}
TextStyle robotoRegular12(Color color) =>
ShrineStyle.roboto(12.0, FontWeight.w500, color);
TextStyle robotoLight12(Color color) =>
ShrineStyle.roboto(12.0, FontWeight.w300, color);
TextStyle robotoRegular14(Color color) =>
ShrineStyle.roboto(14.0, FontWeight.w500, color);
TextStyle robotoMedium14(Color color) =>
ShrineStyle.roboto(14.0, FontWeight.w600, color);
TextStyle robotoLight14(Color color) =>
ShrineStyle.roboto(14.0, FontWeight.w300, color);
TextStyle robotoRegular16(Color color) =>
ShrineStyle.roboto(16.0, FontWeight.w500, color);
TextStyle robotoRegular20(Color color) =>
ShrineStyle.roboto(20.0, FontWeight.w500, color);
TextStyle abrilFatfaceRegular24(Color color) =>
ShrineStyle.abrilFatface(24.0, FontWeight.w500, color);
TextStyle abrilFatfaceRegular34(Color color) =>
ShrineStyle.abrilFatface(34.0, FontWeight.w500, color);
/// The TextStyles and Colors used for titles, labels, and descriptions. This
/// InheritedWidget is shared by all of the routes and widgets created for
/// the Shrine app.
class ShrineTheme extends InheritedWidget {
ShrineTheme({Key key, @required Widget child})
: assert(child != null),
super(key: key, child: child);
final Color cardBackgroundColor = Colors.white;
final Color appBarBackgroundColor = Colors.white;
final Color dividerColor = const Color(0xFFD9D9D9);
final Color priceHighlightColor = const Color(0xFFFFE0E0);
final TextStyle appBarTitleStyle = robotoRegular20(Colors.black87);
final TextStyle vendorItemStyle = robotoRegular12(const Color(0xFF81959D));
final TextStyle priceStyle = robotoRegular14(Colors.black87);
final TextStyle featureTitleStyle =
abrilFatfaceRegular34(const Color(0xFF0A3142));
final TextStyle featurePriceStyle = robotoRegular16(Colors.black87);
final TextStyle featureStyle = robotoLight14(Colors.black54);
final TextStyle orderTitleStyle = abrilFatfaceRegular24(Colors.black87);
final TextStyle orderStyle = robotoLight14(Colors.black54);
final TextStyle vendorTitleStyle = robotoMedium14(Colors.black87);
final TextStyle vendorStyle = robotoLight14(Colors.black54);
final TextStyle quantityMenuStyle = robotoLight14(Colors.black54);
static ShrineTheme of(BuildContext context) =>
context.inheritFromWidgetOfExactType(ShrineTheme);
@override
bool updateShouldNotify(ShrineTheme oldWidget) => false;
}

View File

@@ -0,0 +1,100 @@
// 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_ui/ui.dart' show hashValues;
class Vendor {
const Vendor({
this.name,
this.description,
this.avatarAsset,
this.avatarAssetPackage,
});
final String name;
final String description;
final String avatarAsset;
final String avatarAssetPackage;
bool isValid() {
return name != null && description != null && avatarAsset != null;
}
@override
String toString() => 'Vendor($name)';
}
class Product {
const Product(
{this.name,
this.description,
this.featureTitle,
this.featureDescription,
this.imageAsset,
this.imageAssetPackage,
this.categories,
this.price,
this.vendor});
final String name;
final String description;
final String featureTitle;
final String featureDescription;
final String imageAsset;
final String imageAssetPackage;
final List<String> categories;
final double price;
final Vendor vendor;
String get tag => name; // Unique value for Heroes
String get priceString => '\$${price.floor()}';
bool isValid() {
return name != null &&
description != null &&
imageAsset != null &&
categories != null &&
categories.isNotEmpty &&
price != null &&
vendor.isValid();
}
@override
String toString() => 'Product($name)';
}
class Order {
Order({@required this.product, this.quantity = 1, this.inCart = false})
: assert(product != null),
assert(quantity != null && quantity >= 0),
assert(inCart != null);
final Product product;
final int quantity;
final bool inCart;
Order copyWith({Product product, int quantity, bool inCart}) {
return Order(
product: product ?? this.product,
quantity: quantity ?? this.quantity,
inCart: inCart ?? this.inCart);
}
@override
bool operator ==(dynamic other) {
if (identical(this, other)) return true;
if (other.runtimeType != runtimeType) return false;
final Order typedOther = other;
return product == typedOther.product &&
quantity == typedOther.quantity &&
inCart == typedOther.inCart;
}
@override
int get hashCode => hashValues(product, quantity, inCart);
@override
String toString() => 'Order($product, quantity=$quantity, inCart=$inCart)';
}

View File

@@ -0,0 +1,43 @@
// 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 'shrine/shrine_home.dart' show ShrineHome;
import 'shrine/shrine_theme.dart' show ShrineTheme;
// This code would ordinarily be part of the MaterialApp's home. It's being
// used by the ShrineDemo and by each route pushed from there because this
// isn't a standalone app with its own main() and MaterialApp.
Widget buildShrine(BuildContext context, Widget child) {
return Theme(
data: ThemeData(
primarySwatch: Colors.grey,
iconTheme: const IconThemeData(color: Color(0xFF707070)),
platform: Theme.of(context).platform,
),
child: ShrineTheme(child: child));
}
// In a standalone version of this app, MaterialPageRoute<T> could be used directly.
class ShrinePageRoute<T> extends MaterialPageRoute<T> {
ShrinePageRoute({
WidgetBuilder builder,
RouteSettings settings,
}) : super(builder: builder, settings: settings);
@override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return buildShrine(
context, super.buildPage(context, animation, secondaryAnimation));
}
}
class ShrineDemo extends StatelessWidget {
static const String routeName = '/shrine'; // Used by the Gallery app.
@override
Widget build(BuildContext context) => buildShrine(context, ShrineHome());
}

View File

@@ -0,0 +1,86 @@
// 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 TextStyleItem extends StatelessWidget {
const TextStyleItem({
Key key,
@required this.name,
@required this.style,
@required this.text,
}) : assert(name != null),
assert(style != null),
assert(text != null),
super(key: key);
final String name;
final TextStyle style;
final String text;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final TextStyle nameStyle =
theme.textTheme.caption.copyWith(color: theme.textTheme.caption.color);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SizedBox(width: 72.0, child: Text(name, style: nameStyle)),
Expanded(child: Text(text, style: style.copyWith(height: 1.0)))
]));
}
}
class TypographyDemo extends StatelessWidget {
static const String routeName = '/typography';
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
final List<Widget> styleItems = <Widget>[
TextStyleItem(
name: 'Display 3', style: textTheme.display3, text: 'Regular 56sp'),
TextStyleItem(
name: 'Display 2', style: textTheme.display2, text: 'Regular 45sp'),
TextStyleItem(
name: 'Display 1', style: textTheme.display1, text: 'Regular 34sp'),
TextStyleItem(
name: 'Headline', style: textTheme.headline, text: 'Regular 24sp'),
TextStyleItem(name: 'Title', style: textTheme.title, text: 'Medium 20sp'),
TextStyleItem(
name: 'Subheading', style: textTheme.subhead, text: 'Regular 16sp'),
TextStyleItem(
name: 'Body 2', style: textTheme.body2, text: 'Medium 14sp'),
TextStyleItem(
name: 'Body 1', style: textTheme.body1, text: 'Regular 14sp'),
TextStyleItem(
name: 'Caption', style: textTheme.caption, text: 'Regular 12sp'),
TextStyleItem(
name: 'Button',
style: textTheme.button,
text: 'MEDIUM (ALL CAPS) 14sp'),
];
if (MediaQuery.of(context).size.width > 500.0) {
styleItems.insert(
0,
TextStyleItem(
name: 'Display 4',
style: textTheme.display4,
text: 'Light 112sp'));
}
return Scaffold(
appBar: AppBar(title: const Text('Typography')),
body: SafeArea(
top: false,
bottom: false,
child: ListView(children: styleItems),
),
);
}
}