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:
committed by
Andrew Brogdon
parent
42f2dce01b
commit
3fe927cb29
15
web/gallery/lib/demo/all.dart
Normal file
15
web/gallery/lib/demo/all.dart
Normal 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';
|
||||
660
web/gallery/lib/demo/animation/home.dart
Normal file
660
web/gallery/lib/demo/animation/home.dart
Normal 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);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
167
web/gallery/lib/demo/animation/sections.dart
Normal file
167
web/gallery/lib/demo/animation/sections.dart
Normal 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,
|
||||
],
|
||||
),
|
||||
];
|
||||
172
web/gallery/lib/demo/animation/widgets.dart
Normal file
172
web/gallery/lib/demo/animation/widgets.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
16
web/gallery/lib/demo/animation_demo.dart
Normal file
16
web/gallery/lib/demo/animation_demo.dart
Normal 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();
|
||||
}
|
||||
222
web/gallery/lib/demo/colors_demo.dart
Normal file
222
web/gallery/lib/demo/colors_demo.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
340
web/gallery/lib/demo/contacts_demo.dart
Normal file
340
web/gallery/lib/demo/contacts_demo.dart
Normal 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',
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
411
web/gallery/lib/demo/material/backdrop_demo.dart
Normal file
411
web/gallery/lib/demo/material/backdrop_demo.dart
Normal file
@@ -0,0 +1,411 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
// This demo displays one Category at a time. The backdrop show a list
|
||||
// of all of the categories and the selected category is displayed
|
||||
// (CategoryView) on top of the backdrop.
|
||||
|
||||
class Category {
|
||||
const Category({this.title, this.assets});
|
||||
final String title;
|
||||
final List<String> assets;
|
||||
@override
|
||||
String toString() => '$runtimeType("$title")';
|
||||
}
|
||||
|
||||
const List<Category> allCategories = <Category>[
|
||||
Category(
|
||||
title: 'Accessories',
|
||||
assets: <String>[
|
||||
'products/belt.png',
|
||||
'products/earrings.png',
|
||||
'products/backpack.png',
|
||||
'products/hat.png',
|
||||
'products/scarf.png',
|
||||
'products/sunnies.png',
|
||||
],
|
||||
),
|
||||
Category(
|
||||
title: 'Blue',
|
||||
assets: <String>[
|
||||
'products/backpack.png',
|
||||
'products/cup.png',
|
||||
'products/napkins.png',
|
||||
'products/top.png',
|
||||
],
|
||||
),
|
||||
Category(
|
||||
title: 'Cold Weather',
|
||||
assets: <String>[
|
||||
'products/jacket.png',
|
||||
'products/jumper.png',
|
||||
'products/scarf.png',
|
||||
'products/sweater.png',
|
||||
'products/sweats.png',
|
||||
],
|
||||
),
|
||||
Category(
|
||||
title: 'Home',
|
||||
assets: <String>[
|
||||
'products/cup.png',
|
||||
'products/napkins.png',
|
||||
'products/planters.png',
|
||||
'products/table.png',
|
||||
'products/teaset.png',
|
||||
],
|
||||
),
|
||||
Category(
|
||||
title: 'Tops',
|
||||
assets: <String>[
|
||||
'products/jumper.png',
|
||||
'products/shirt.png',
|
||||
'products/sweater.png',
|
||||
'products/top.png',
|
||||
],
|
||||
),
|
||||
Category(
|
||||
title: 'Everything',
|
||||
assets: <String>[
|
||||
'products/backpack.png',
|
||||
'products/belt.png',
|
||||
'products/cup.png',
|
||||
'products/dress.png',
|
||||
'products/earrings.png',
|
||||
'products/flatwear.png',
|
||||
'products/hat.png',
|
||||
'products/jacket.png',
|
||||
'products/jumper.png',
|
||||
'products/napkins.png',
|
||||
'products/planters.png',
|
||||
'products/scarf.png',
|
||||
'products/shirt.png',
|
||||
'products/sunnies.png',
|
||||
'products/sweater.png',
|
||||
'products/sweats.png',
|
||||
'products/table.png',
|
||||
'products/teaset.png',
|
||||
'products/top.png',
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
class CategoryView extends StatelessWidget {
|
||||
const CategoryView({Key key, this.category}) : super(key: key);
|
||||
|
||||
final Category category;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return ListView(
|
||||
key: PageStorageKey<Category>(category),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16.0,
|
||||
horizontal: 64.0,
|
||||
),
|
||||
children: category.assets.map<Widget>((String asset) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Card(
|
||||
child: Container(
|
||||
width: 144.0,
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Image.asset(
|
||||
'$asset',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
alignment: AlignmentDirectional.center,
|
||||
child: Text(
|
||||
asset,
|
||||
style: theme.textTheme.caption,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// One BackdropPanel is visible at a time. It's stacked on top of the
|
||||
// the BackdropDemo.
|
||||
class BackdropPanel extends StatelessWidget {
|
||||
const BackdropPanel({
|
||||
Key key,
|
||||
this.onTap,
|
||||
this.onVerticalDragUpdate,
|
||||
this.onVerticalDragEnd,
|
||||
this.title,
|
||||
this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
final VoidCallback onTap;
|
||||
final GestureDragUpdateCallback onVerticalDragUpdate;
|
||||
final GestureDragEndCallback onVerticalDragEnd;
|
||||
final Widget title;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return Material(
|
||||
elevation: 2.0,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16.0),
|
||||
topRight: Radius.circular(16.0),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onVerticalDragUpdate: onVerticalDragUpdate,
|
||||
onVerticalDragEnd: onVerticalDragEnd,
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 48.0,
|
||||
padding: const EdgeInsetsDirectional.only(start: 16.0),
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: DefaultTextStyle(
|
||||
style: theme.textTheme.subhead,
|
||||
child: Tooltip(
|
||||
message: 'Tap to dismiss',
|
||||
child: title,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(height: 1.0),
|
||||
Expanded(child: child),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Cross fades between 'Select a Category' and 'Asset Viewer'.
|
||||
class BackdropTitle extends AnimatedWidget {
|
||||
const BackdropTitle({
|
||||
Key key,
|
||||
Listenable listenable,
|
||||
}) : super(key: key, listenable: listenable);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Animation<double> animation = listenable;
|
||||
return DefaultTextStyle(
|
||||
style: Theme.of(context).primaryTextTheme.title,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Opacity(
|
||||
opacity: CurvedAnimation(
|
||||
parent: ReverseAnimation(animation),
|
||||
curve: const Interval(0.5, 1.0),
|
||||
).value,
|
||||
child: const Text('Select a Category'),
|
||||
),
|
||||
Opacity(
|
||||
opacity: CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: const Interval(0.5, 1.0),
|
||||
).value,
|
||||
child: const Text('Asset Viewer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// This widget is essentially the backdrop itself.
|
||||
class BackdropDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/backdrop';
|
||||
|
||||
@override
|
||||
_BackdropDemoState createState() => _BackdropDemoState();
|
||||
}
|
||||
|
||||
class _BackdropDemoState extends State<BackdropDemo>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop');
|
||||
AnimationController _controller;
|
||||
Category _category = allCategories[0];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
value: 1.0,
|
||||
vsync: this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _changeCategory(Category category) {
|
||||
setState(() {
|
||||
_category = category;
|
||||
_controller.fling(velocity: 2.0);
|
||||
});
|
||||
}
|
||||
|
||||
bool get _backdropPanelVisible {
|
||||
final AnimationStatus status = _controller.status;
|
||||
return status == AnimationStatus.completed ||
|
||||
status == AnimationStatus.forward;
|
||||
}
|
||||
|
||||
void _toggleBackdropPanelVisibility() {
|
||||
_controller.fling(velocity: _backdropPanelVisible ? -2.0 : 2.0);
|
||||
}
|
||||
|
||||
double get _backdropHeight {
|
||||
final RenderBox renderBox = _backdropKey.currentContext.findRenderObject();
|
||||
return renderBox.size.height;
|
||||
}
|
||||
|
||||
// By design: the panel can only be opened with a swipe. To close the panel
|
||||
// the user must either tap its heading or the backdrop's menu icon.
|
||||
|
||||
void _handleDragUpdate(DragUpdateDetails details) {
|
||||
if (_controller.isAnimating ||
|
||||
_controller.status == AnimationStatus.completed) return;
|
||||
|
||||
_controller.value -=
|
||||
details.primaryDelta / (_backdropHeight ?? details.primaryDelta);
|
||||
}
|
||||
|
||||
void _handleDragEnd(DragEndDetails details) {
|
||||
if (_controller.isAnimating ||
|
||||
_controller.status == AnimationStatus.completed) return;
|
||||
|
||||
final double flingVelocity =
|
||||
details.velocity.pixelsPerSecond.dy / _backdropHeight;
|
||||
if (flingVelocity < 0.0)
|
||||
_controller.fling(velocity: math.max(2.0, -flingVelocity));
|
||||
else if (flingVelocity > 0.0)
|
||||
_controller.fling(velocity: math.min(-2.0, -flingVelocity));
|
||||
else
|
||||
_controller.fling(velocity: _controller.value < 0.5 ? -2.0 : 2.0);
|
||||
}
|
||||
|
||||
// Stacks a BackdropPanel, which displays the selected category, on top
|
||||
// of the backdrop. The categories are displayed with ListTiles. Just one
|
||||
// can be selected at a time. This is a LayoutWidgetBuild function because
|
||||
// we need to know how big the BackdropPanel will be to set up its
|
||||
// animation.
|
||||
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
|
||||
const double panelTitleHeight = 48.0;
|
||||
final Size panelSize = constraints.biggest;
|
||||
final double panelTop = panelSize.height - panelTitleHeight;
|
||||
|
||||
final Animation<RelativeRect> panelAnimation = _controller.drive(
|
||||
RelativeRectTween(
|
||||
begin: RelativeRect.fromLTRB(
|
||||
0.0,
|
||||
panelTop - MediaQuery.of(context).padding.bottom,
|
||||
0.0,
|
||||
panelTop - panelSize.height,
|
||||
),
|
||||
end: const RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
|
||||
),
|
||||
);
|
||||
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final List<Widget> backdropItems =
|
||||
allCategories.map<Widget>((Category category) {
|
||||
final bool selected = category == _category;
|
||||
return Material(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(4.0)),
|
||||
),
|
||||
color: selected ? Colors.white.withOpacity(0.25) : Colors.transparent,
|
||||
child: ListTile(
|
||||
title: Text(category.title),
|
||||
selected: selected,
|
||||
onTap: () {
|
||||
_changeCategory(category);
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return Container(
|
||||
key: _backdropKey,
|
||||
color: theme.primaryColor,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
ListTileTheme(
|
||||
iconColor: theme.primaryIconTheme.color,
|
||||
textColor: theme.primaryTextTheme.title.color.withOpacity(0.6),
|
||||
selectedColor: theme.primaryTextTheme.title.color,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: backdropItems,
|
||||
),
|
||||
),
|
||||
),
|
||||
PositionedTransition(
|
||||
rect: panelAnimation,
|
||||
child: BackdropPanel(
|
||||
onTap: _toggleBackdropPanelVisibility,
|
||||
onVerticalDragUpdate: _handleDragUpdate,
|
||||
onVerticalDragEnd: _handleDragEnd,
|
||||
title: Text(_category.title),
|
||||
child: CategoryView(category: _category),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: 0.0,
|
||||
title: BackdropTitle(
|
||||
listenable: _controller.view,
|
||||
),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
onPressed: _toggleBackdropPanelVisibility,
|
||||
icon: AnimatedIcon(
|
||||
icon: AnimatedIcons.close_menu,
|
||||
semanticLabel: 'close',
|
||||
progress: _controller.view,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: LayoutBuilder(
|
||||
builder: _buildStack,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
524
web/gallery/lib/demo/material/bottom_app_bar_demo.dart
Normal file
524
web/gallery/lib/demo/material/bottom_app_bar_demo.dart
Normal file
@@ -0,0 +1,524 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class BottomAppBarDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/bottom_app_bar';
|
||||
|
||||
@override
|
||||
State createState() => _BottomAppBarDemoState();
|
||||
}
|
||||
|
||||
// Flutter generally frowns upon abbrevation however this class uses two
|
||||
// abbrevations extensively: "fab" for floating action button, and "bab"
|
||||
// for bottom application bar.
|
||||
|
||||
class _BottomAppBarDemoState extends State<BottomAppBarDemo> {
|
||||
static final GlobalKey<ScaffoldState> _scaffoldKey =
|
||||
GlobalKey<ScaffoldState>();
|
||||
|
||||
// FAB shape
|
||||
|
||||
static const _ChoiceValue<Widget> kNoFab = _ChoiceValue<Widget>(
|
||||
title: 'None',
|
||||
label: 'do not show a floating action button',
|
||||
value: null,
|
||||
);
|
||||
|
||||
static const _ChoiceValue<Widget> kCircularFab = _ChoiceValue<Widget>(
|
||||
title: 'Circular',
|
||||
label: 'circular floating action button',
|
||||
value: FloatingActionButton(
|
||||
onPressed: _showSnackbar,
|
||||
child: Icon(Icons.add, semanticLabel: 'Action'),
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
);
|
||||
|
||||
static const _ChoiceValue<Widget> kDiamondFab = _ChoiceValue<Widget>(
|
||||
title: 'Diamond',
|
||||
label: 'diamond shape floating action button',
|
||||
value: _DiamondFab(
|
||||
onPressed: _showSnackbar,
|
||||
child: Icon(Icons.add, semanticLabel: 'Action'),
|
||||
),
|
||||
);
|
||||
|
||||
// Notch
|
||||
|
||||
static const _ChoiceValue<bool> kShowNotchTrue = _ChoiceValue<bool>(
|
||||
title: 'On',
|
||||
label: 'show bottom appbar notch',
|
||||
value: true,
|
||||
);
|
||||
|
||||
static const _ChoiceValue<bool> kShowNotchFalse = _ChoiceValue<bool>(
|
||||
title: 'Off',
|
||||
label: 'do not show bottom appbar notch',
|
||||
value: false,
|
||||
);
|
||||
|
||||
// FAB Position
|
||||
|
||||
static const _ChoiceValue<FloatingActionButtonLocation> kFabEndDocked =
|
||||
_ChoiceValue<FloatingActionButtonLocation>(
|
||||
title: 'Attached - End',
|
||||
label: 'floating action button is docked at the end of the bottom app bar',
|
||||
value: FloatingActionButtonLocation.endDocked,
|
||||
);
|
||||
|
||||
static const _ChoiceValue<FloatingActionButtonLocation> kFabCenterDocked =
|
||||
_ChoiceValue<FloatingActionButtonLocation>(
|
||||
title: 'Attached - Center',
|
||||
label:
|
||||
'floating action button is docked at the center of the bottom app bar',
|
||||
value: FloatingActionButtonLocation.centerDocked,
|
||||
);
|
||||
|
||||
static const _ChoiceValue<FloatingActionButtonLocation> kFabEndFloat =
|
||||
_ChoiceValue<FloatingActionButtonLocation>(
|
||||
title: 'Free - End',
|
||||
label: 'floating action button floats above the end of the bottom app bar',
|
||||
value: FloatingActionButtonLocation.endFloat,
|
||||
);
|
||||
|
||||
static const _ChoiceValue<FloatingActionButtonLocation> kFabCenterFloat =
|
||||
_ChoiceValue<FloatingActionButtonLocation>(
|
||||
title: 'Free - Center',
|
||||
label:
|
||||
'floating action button is floats above the center of the bottom app bar',
|
||||
value: FloatingActionButtonLocation.centerFloat,
|
||||
);
|
||||
|
||||
static void _showSnackbar() {
|
||||
const String text =
|
||||
"When the Scaffold's floating action button location changes, "
|
||||
'the floating action button animates to its new position.'
|
||||
'The BottomAppBar adapts its shape appropriately.';
|
||||
_scaffoldKey.currentState.showSnackBar(
|
||||
const SnackBar(content: Text(text)),
|
||||
);
|
||||
}
|
||||
|
||||
// App bar color
|
||||
|
||||
static const List<_NamedColor> kBabColors = <_NamedColor>[
|
||||
_NamedColor(null, 'Clear'),
|
||||
_NamedColor(Color(0xFFFFC100), 'Orange'),
|
||||
_NamedColor(Color(0xFF91FAFF), 'Light Blue'),
|
||||
_NamedColor(Color(0xFF00D1FF), 'Cyan'),
|
||||
_NamedColor(Color(0xFF00BCFF), 'Cerulean'),
|
||||
_NamedColor(Color(0xFF009BEE), 'Blue'),
|
||||
];
|
||||
|
||||
_ChoiceValue<Widget> _fabShape = kCircularFab;
|
||||
_ChoiceValue<bool> _showNotch = kShowNotchTrue;
|
||||
_ChoiceValue<FloatingActionButtonLocation> _fabLocation = kFabEndDocked;
|
||||
Color _babColor = kBabColors.first.color;
|
||||
|
||||
void _onShowNotchChanged(_ChoiceValue<bool> value) {
|
||||
setState(() {
|
||||
_showNotch = value;
|
||||
});
|
||||
}
|
||||
|
||||
void _onFabShapeChanged(_ChoiceValue<Widget> value) {
|
||||
setState(() {
|
||||
_fabShape = value;
|
||||
});
|
||||
}
|
||||
|
||||
void _onFabLocationChanged(_ChoiceValue<FloatingActionButtonLocation> value) {
|
||||
setState(() {
|
||||
_fabLocation = value;
|
||||
});
|
||||
}
|
||||
|
||||
void _onBabColorChanged(Color value) {
|
||||
setState(() {
|
||||
_babColor = value;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: const Text('Bottom app bar'),
|
||||
elevation: 0.0,
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(BottomAppBarDemo.routeName),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.sentiment_very_satisfied,
|
||||
semanticLabel: 'Update shape'),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_fabShape =
|
||||
_fabShape == kCircularFab ? kDiamondFab : kCircularFab;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.only(bottom: 88.0),
|
||||
children: <Widget>[
|
||||
const _Heading('FAB Shape'),
|
||||
_RadioItem<Widget>(kCircularFab, _fabShape, _onFabShapeChanged),
|
||||
_RadioItem<Widget>(kDiamondFab, _fabShape, _onFabShapeChanged),
|
||||
_RadioItem<Widget>(kNoFab, _fabShape, _onFabShapeChanged),
|
||||
const Divider(),
|
||||
const _Heading('Notch'),
|
||||
_RadioItem<bool>(kShowNotchTrue, _showNotch, _onShowNotchChanged),
|
||||
_RadioItem<bool>(kShowNotchFalse, _showNotch, _onShowNotchChanged),
|
||||
const Divider(),
|
||||
const _Heading('FAB Position'),
|
||||
_RadioItem<FloatingActionButtonLocation>(
|
||||
kFabEndDocked, _fabLocation, _onFabLocationChanged),
|
||||
_RadioItem<FloatingActionButtonLocation>(
|
||||
kFabCenterDocked, _fabLocation, _onFabLocationChanged),
|
||||
_RadioItem<FloatingActionButtonLocation>(
|
||||
kFabEndFloat, _fabLocation, _onFabLocationChanged),
|
||||
_RadioItem<FloatingActionButtonLocation>(
|
||||
kFabCenterFloat, _fabLocation, _onFabLocationChanged),
|
||||
const Divider(),
|
||||
const _Heading('App bar color'),
|
||||
_ColorsItem(kBabColors, _babColor, _onBabColorChanged),
|
||||
],
|
||||
),
|
||||
floatingActionButton: _fabShape.value,
|
||||
floatingActionButtonLocation: _fabLocation.value,
|
||||
bottomNavigationBar: _DemoBottomAppBar(
|
||||
color: _babColor,
|
||||
fabLocation: _fabLocation.value,
|
||||
shape: _selectNotch(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
NotchedShape _selectNotch() {
|
||||
if (!_showNotch.value) return null;
|
||||
if (_fabShape == kCircularFab) return const CircularNotchedRectangle();
|
||||
if (_fabShape == kDiamondFab) return const _DiamondNotchedRectangle();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class _ChoiceValue<T> {
|
||||
const _ChoiceValue({this.value, this.title, this.label});
|
||||
|
||||
final T value;
|
||||
final String title;
|
||||
final String label; // For the Semantics widget that contains title
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType("$title")';
|
||||
}
|
||||
|
||||
class _RadioItem<T> extends StatelessWidget {
|
||||
const _RadioItem(this.value, this.groupValue, this.onChanged);
|
||||
|
||||
final _ChoiceValue<T> value;
|
||||
final _ChoiceValue<T> groupValue;
|
||||
final ValueChanged<_ChoiceValue<T>> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return Container(
|
||||
height: 56.0,
|
||||
padding: const EdgeInsetsDirectional.only(start: 16.0),
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: MergeSemantics(
|
||||
child: Row(children: <Widget>[
|
||||
Radio<_ChoiceValue<T>>(
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
Expanded(
|
||||
child: Semantics(
|
||||
container: true,
|
||||
button: true,
|
||||
label: value.label,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
onChanged(value);
|
||||
},
|
||||
child: Text(
|
||||
value.title,
|
||||
style: theme.textTheme.subhead,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NamedColor {
|
||||
const _NamedColor(this.color, this.name);
|
||||
|
||||
final Color color;
|
||||
final String name;
|
||||
}
|
||||
|
||||
class _ColorsItem extends StatelessWidget {
|
||||
const _ColorsItem(this.colors, this.selectedColor, this.onChanged);
|
||||
|
||||
final List<_NamedColor> colors;
|
||||
final Color selectedColor;
|
||||
final ValueChanged<Color> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: colors.map<Widget>((_NamedColor namedColor) {
|
||||
return RawMaterialButton(
|
||||
onPressed: () {
|
||||
onChanged(namedColor.color);
|
||||
},
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
width: 32.0,
|
||||
height: 32.0,
|
||||
),
|
||||
fillColor: namedColor.color,
|
||||
shape: CircleBorder(
|
||||
side: BorderSide(
|
||||
color: namedColor.color == selectedColor
|
||||
? Colors.black
|
||||
: const Color(0xFFD5D7DA),
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
child: Semantics(
|
||||
value: namedColor.name,
|
||||
selected: namedColor.color == selectedColor,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Heading extends StatelessWidget {
|
||||
const _Heading(this.text);
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return Container(
|
||||
height: 48.0,
|
||||
padding: const EdgeInsetsDirectional.only(start: 56.0),
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(
|
||||
text,
|
||||
style: theme.textTheme.body1.copyWith(
|
||||
color: theme.primaryColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DemoBottomAppBar extends StatelessWidget {
|
||||
const _DemoBottomAppBar({this.color, this.fabLocation, this.shape});
|
||||
|
||||
final Color color;
|
||||
final FloatingActionButtonLocation fabLocation;
|
||||
final NotchedShape shape;
|
||||
|
||||
static final List<FloatingActionButtonLocation> kCenterLocations =
|
||||
<FloatingActionButtonLocation>[
|
||||
FloatingActionButtonLocation.centerDocked,
|
||||
FloatingActionButtonLocation.centerFloat,
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<Widget> rowContents = <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.menu, semanticLabel: 'Show bottom sheet'),
|
||||
onPressed: () {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => const _DemoDrawer(),
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
if (kCenterLocations.contains(fabLocation)) {
|
||||
rowContents.add(
|
||||
const Expanded(child: SizedBox()),
|
||||
);
|
||||
}
|
||||
|
||||
rowContents.addAll(<Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.search,
|
||||
semanticLabel: 'show search action',
|
||||
),
|
||||
onPressed: () {
|
||||
Scaffold.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('This is a dummy search action.')),
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Theme.of(context).platform == TargetPlatform.iOS
|
||||
? Icons.more_horiz
|
||||
: Icons.more_vert,
|
||||
semanticLabel: 'Show menu actions',
|
||||
),
|
||||
onPressed: () {
|
||||
Scaffold.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('This is a dummy menu action.')),
|
||||
);
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
||||
return BottomAppBar(
|
||||
color: color,
|
||||
child: Row(children: rowContents),
|
||||
shape: shape,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// A drawer that pops up from the bottom of the screen.
|
||||
class _DemoDrawer extends StatelessWidget {
|
||||
const _DemoDrawer();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Drawer(
|
||||
child: Column(
|
||||
children: const <Widget>[
|
||||
ListTile(
|
||||
leading: Icon(Icons.search),
|
||||
title: Text('Search'),
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(Icons.threed_rotation),
|
||||
title: Text('3D'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// A diamond-shaped floating action button.
|
||||
class _DiamondFab extends StatelessWidget {
|
||||
const _DiamondFab({
|
||||
this.child,
|
||||
this.onPressed,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
shape: const _DiamondBorder(),
|
||||
color: Colors.orange,
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
child: Container(
|
||||
width: 56.0,
|
||||
height: 56.0,
|
||||
child: IconTheme.merge(
|
||||
data: IconThemeData(color: Theme.of(context).accentIconTheme.color),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
elevation: 6.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DiamondNotchedRectangle implements NotchedShape {
|
||||
const _DiamondNotchedRectangle();
|
||||
|
||||
@override
|
||||
Path getOuterPath(Rect host, Rect guest) {
|
||||
if (!host.overlaps(guest)) return Path()..addRect(host);
|
||||
assert(guest.width > 0.0);
|
||||
|
||||
final Rect intersection = guest.intersect(host);
|
||||
// We are computing a "V" shaped notch, as in this diagram:
|
||||
// -----\**** /-----
|
||||
// \ /
|
||||
// \ /
|
||||
// \ /
|
||||
//
|
||||
// "-" marks the top edge of the bottom app bar.
|
||||
// "\" and "/" marks the notch outline
|
||||
//
|
||||
// notchToCenter is the horizontal distance between the guest's center and
|
||||
// the host's top edge where the notch starts (marked with "*").
|
||||
// We compute notchToCenter by similar triangles:
|
||||
final double notchToCenter =
|
||||
intersection.height * (guest.height / 2.0) / (guest.width / 2.0);
|
||||
|
||||
return Path()
|
||||
..moveTo(host.left, host.top)
|
||||
..lineTo(guest.center.dx - notchToCenter, host.top)
|
||||
..lineTo(guest.left + guest.width / 2.0, guest.bottom)
|
||||
..lineTo(guest.center.dx + notchToCenter, host.top)
|
||||
..lineTo(host.right, host.top)
|
||||
..lineTo(host.right, host.bottom)
|
||||
..lineTo(host.left, host.bottom)
|
||||
..close();
|
||||
}
|
||||
}
|
||||
|
||||
class _DiamondBorder extends ShapeBorder {
|
||||
const _DiamondBorder();
|
||||
|
||||
@override
|
||||
EdgeInsetsGeometry get dimensions {
|
||||
return const EdgeInsets.only();
|
||||
}
|
||||
|
||||
@override
|
||||
Path getInnerPath(Rect rect, {TextDirection textDirection}) {
|
||||
return getOuterPath(rect, textDirection: textDirection);
|
||||
}
|
||||
|
||||
@override
|
||||
Path getOuterPath(Rect rect, {TextDirection textDirection}) {
|
||||
return Path()
|
||||
..moveTo(rect.left + rect.width / 2.0, rect.top)
|
||||
..lineTo(rect.right, rect.top + rect.height / 2.0)
|
||||
..lineTo(rect.left + rect.width / 2.0, rect.bottom)
|
||||
..lineTo(rect.left, rect.top + rect.height / 2.0)
|
||||
..close();
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) {}
|
||||
|
||||
// This border doesn't support scaling.
|
||||
@override
|
||||
ShapeBorder scale(double t) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
239
web/gallery/lib/demo/material/bottom_navigation_demo.dart
Normal file
239
web/gallery/lib/demo/material/bottom_navigation_demo.dart
Normal file
@@ -0,0 +1,239 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class NavigationIconView {
|
||||
NavigationIconView({
|
||||
Widget icon,
|
||||
Widget activeIcon,
|
||||
String title,
|
||||
Color color,
|
||||
TickerProvider vsync,
|
||||
}) : _icon = icon,
|
||||
_color = color,
|
||||
_title = title,
|
||||
item = BottomNavigationBarItem(
|
||||
icon: icon,
|
||||
activeIcon: activeIcon,
|
||||
title: Text(title),
|
||||
backgroundColor: color,
|
||||
),
|
||||
controller = AnimationController(
|
||||
duration: kThemeAnimationDuration,
|
||||
vsync: vsync,
|
||||
) {
|
||||
_animation = controller.drive(CurveTween(
|
||||
curve: const Interval(0.5, 1.0, curve: Curves.fastOutSlowIn),
|
||||
));
|
||||
}
|
||||
|
||||
final Widget _icon;
|
||||
final Color _color;
|
||||
final String _title;
|
||||
final BottomNavigationBarItem item;
|
||||
final AnimationController controller;
|
||||
Animation<double> _animation;
|
||||
|
||||
FadeTransition transition(
|
||||
BottomNavigationBarType type, BuildContext context) {
|
||||
Color iconColor;
|
||||
if (type == BottomNavigationBarType.shifting) {
|
||||
iconColor = _color;
|
||||
} else {
|
||||
final ThemeData themeData = Theme.of(context);
|
||||
iconColor = themeData.brightness == Brightness.light
|
||||
? themeData.primaryColor
|
||||
: themeData.accentColor;
|
||||
}
|
||||
|
||||
return FadeTransition(
|
||||
opacity: _animation,
|
||||
child: SlideTransition(
|
||||
position: _animation.drive(
|
||||
Tween<Offset>(
|
||||
begin: const Offset(0.0, 0.02), // Slightly down.
|
||||
end: Offset.zero,
|
||||
),
|
||||
),
|
||||
child: IconTheme(
|
||||
data: IconThemeData(
|
||||
color: iconColor,
|
||||
size: 120.0,
|
||||
),
|
||||
child: Semantics(
|
||||
label: 'Placeholder for $_title tab',
|
||||
child: _icon,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CustomIcon extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final IconThemeData iconTheme = IconTheme.of(context);
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(4.0),
|
||||
width: iconTheme.size - 8.0,
|
||||
height: iconTheme.size - 8.0,
|
||||
color: iconTheme.color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CustomInactiveIcon extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final IconThemeData iconTheme = IconTheme.of(context);
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(4.0),
|
||||
width: iconTheme.size - 8.0,
|
||||
height: iconTheme.size - 8.0,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: iconTheme.color, width: 2.0),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class BottomNavigationDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/bottom_navigation';
|
||||
|
||||
@override
|
||||
_BottomNavigationDemoState createState() => _BottomNavigationDemoState();
|
||||
}
|
||||
|
||||
class _BottomNavigationDemoState extends State<BottomNavigationDemo>
|
||||
with TickerProviderStateMixin {
|
||||
int _currentIndex = 0;
|
||||
BottomNavigationBarType _type = BottomNavigationBarType.shifting;
|
||||
List<NavigationIconView> _navigationViews;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_navigationViews = <NavigationIconView>[
|
||||
NavigationIconView(
|
||||
icon: const Icon(Icons.access_alarm),
|
||||
title: 'Alarm',
|
||||
color: Colors.deepPurple,
|
||||
vsync: this,
|
||||
),
|
||||
NavigationIconView(
|
||||
activeIcon: CustomIcon(),
|
||||
icon: CustomInactiveIcon(),
|
||||
title: 'Box',
|
||||
color: Colors.deepOrange,
|
||||
vsync: this,
|
||||
),
|
||||
NavigationIconView(
|
||||
activeIcon: const Icon(Icons.cloud),
|
||||
icon: const Icon(Icons.cloud_queue),
|
||||
title: 'Cloud',
|
||||
color: Colors.teal,
|
||||
vsync: this,
|
||||
),
|
||||
NavigationIconView(
|
||||
activeIcon: const Icon(Icons.favorite),
|
||||
icon: const Icon(Icons.favorite_border),
|
||||
title: 'Favorites',
|
||||
color: Colors.indigo,
|
||||
vsync: this,
|
||||
),
|
||||
NavigationIconView(
|
||||
icon: const Icon(Icons.event_available),
|
||||
title: 'Event',
|
||||
color: Colors.pink,
|
||||
vsync: this,
|
||||
)
|
||||
];
|
||||
|
||||
for (NavigationIconView view in _navigationViews)
|
||||
view.controller.addListener(_rebuild);
|
||||
|
||||
_navigationViews[_currentIndex].controller.value = 1.0;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (NavigationIconView view in _navigationViews) view.controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _rebuild() {
|
||||
setState(() {
|
||||
// Rebuild in order to animate views.
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildTransitionsStack() {
|
||||
final List<FadeTransition> transitions = <FadeTransition>[];
|
||||
|
||||
for (NavigationIconView view in _navigationViews)
|
||||
transitions.add(view.transition(_type, context));
|
||||
|
||||
// We want to have the newly animating (fading in) views on top.
|
||||
transitions.sort((FadeTransition a, FadeTransition b) {
|
||||
final Animation<double> aAnimation = a.opacity;
|
||||
final Animation<double> bAnimation = b.opacity;
|
||||
final double aValue = aAnimation.value;
|
||||
final double bValue = bAnimation.value;
|
||||
return aValue.compareTo(bValue);
|
||||
});
|
||||
|
||||
return Stack(children: transitions);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final BottomNavigationBar botNavBar = BottomNavigationBar(
|
||||
items: _navigationViews
|
||||
.map<BottomNavigationBarItem>(
|
||||
(NavigationIconView navigationView) => navigationView.item)
|
||||
.toList(),
|
||||
currentIndex: _currentIndex,
|
||||
type: _type,
|
||||
onTap: (int index) {
|
||||
setState(() {
|
||||
_navigationViews[_currentIndex].controller.reverse();
|
||||
_currentIndex = index;
|
||||
_navigationViews[_currentIndex].controller.forward();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Bottom navigation'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(BottomNavigationDemo.routeName),
|
||||
PopupMenuButton<BottomNavigationBarType>(
|
||||
onSelected: (BottomNavigationBarType value) {
|
||||
setState(() {
|
||||
_type = value;
|
||||
});
|
||||
},
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuItem<BottomNavigationBarType>>[
|
||||
const PopupMenuItem<BottomNavigationBarType>(
|
||||
value: BottomNavigationBarType.fixed,
|
||||
child: Text('Fixed'),
|
||||
),
|
||||
const PopupMenuItem<BottomNavigationBarType>(
|
||||
value: BottomNavigationBarType.shifting,
|
||||
child: Text('Shifting'),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
body: Center(child: _buildTransitionsStack()),
|
||||
bottomNavigationBar: botNavBar,
|
||||
);
|
||||
}
|
||||
}
|
||||
181
web/gallery/lib/demo/material/cards_demo.dart
Normal file
181
web/gallery/lib/demo/material/cards_demo.dart
Normal file
@@ -0,0 +1,181 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/foundation.dart';
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
|
||||
|
||||
class TravelDestination {
|
||||
const TravelDestination({
|
||||
this.assetName,
|
||||
this.assetPackage,
|
||||
this.title,
|
||||
this.description,
|
||||
});
|
||||
|
||||
final String assetName;
|
||||
final String assetPackage;
|
||||
final String title;
|
||||
final List<String> description;
|
||||
|
||||
bool get isValid =>
|
||||
assetName != null && title != null && description?.length == 3;
|
||||
}
|
||||
|
||||
final List<TravelDestination> destinations = <TravelDestination>[
|
||||
const TravelDestination(
|
||||
assetName: 'places/india_thanjavur_market.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Top 10 Cities to Visit in Tamil Nadu',
|
||||
description: <String>[
|
||||
'Number 10',
|
||||
'Thanjavur',
|
||||
'Thanjavur, Tamil Nadu',
|
||||
],
|
||||
),
|
||||
const TravelDestination(
|
||||
assetName: 'places/india_chettinad_silk_maker.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Artisans of Southern India',
|
||||
description: <String>[
|
||||
'Silk Spinners',
|
||||
'Chettinad',
|
||||
'Sivaganga, Tamil Nadu',
|
||||
],
|
||||
)
|
||||
];
|
||||
|
||||
class TravelDestinationItem extends StatelessWidget {
|
||||
TravelDestinationItem({Key key, @required this.destination, this.shape})
|
||||
: assert(destination != null && destination.isValid),
|
||||
super(key: key);
|
||||
|
||||
static const double height = 366.0;
|
||||
final TravelDestination destination;
|
||||
final ShapeBorder shape;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final TextStyle titleStyle =
|
||||
theme.textTheme.headline.copyWith(color: Colors.white);
|
||||
final TextStyle descriptionStyle = theme.textTheme.subhead;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
height: height,
|
||||
child: Card(
|
||||
shape: shape,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// photo and title
|
||||
SizedBox(
|
||||
height: 184.0,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Positioned(
|
||||
bottom: 16.0,
|
||||
left: 16.0,
|
||||
right: 16.0,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
destination.title,
|
||||
style: titleStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// description and share/explore buttons
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0.0),
|
||||
child: DefaultTextStyle(
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: descriptionStyle,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// three line description
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
destination.description[0],
|
||||
style:
|
||||
descriptionStyle.copyWith(color: Colors.black54),
|
||||
),
|
||||
),
|
||||
Text(destination.description[1]),
|
||||
Text(destination.description[2]),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// share, explore buttons
|
||||
ButtonTheme.bar(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
FlatButton(
|
||||
child: const Text('SHARE'),
|
||||
textColor: Colors.amber.shade500,
|
||||
onPressed: () {/* do nothing */},
|
||||
),
|
||||
FlatButton(
|
||||
child: const Text('EXPLORE'),
|
||||
textColor: Colors.amber.shade500,
|
||||
onPressed: () {/* do nothing */},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CardsDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/cards';
|
||||
|
||||
@override
|
||||
_CardsDemoState createState() => _CardsDemoState();
|
||||
}
|
||||
|
||||
class _CardsDemoState extends State<CardsDemo> {
|
||||
ShapeBorder _shape;
|
||||
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return wrapScaffold('Cards Demo', context, _scaffoldKey,
|
||||
_buildContents(context), CardsDemo.routeName);
|
||||
}
|
||||
|
||||
Widget _buildContents(BuildContext context) {
|
||||
return ListView(
|
||||
itemExtent: TravelDestinationItem.height,
|
||||
padding: const EdgeInsets.only(top: 8.0, left: 8.0, right: 8.0),
|
||||
children: destinations.map((TravelDestination destination) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8.0),
|
||||
child: TravelDestinationItem(
|
||||
destination: destination,
|
||||
shape: _shape,
|
||||
),
|
||||
);
|
||||
}).toList());
|
||||
}
|
||||
}
|
||||
79
web/gallery/lib/demo/material/chip_demo.dart
Normal file
79
web/gallery/lib/demo/material/chip_demo.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class ChipDemo extends StatefulWidget {
|
||||
static const routeName = '/material/chip';
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ChipDemoState();
|
||||
}
|
||||
|
||||
class _ChipDemoState extends State<ChipDemo> {
|
||||
bool _filterChipSelected = false;
|
||||
bool _hasAvatar = true;
|
||||
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return wrapScaffold('Chip Demo', context, _scaffoldKey, _buildContents(),
|
||||
ChipDemo.routeName);
|
||||
}
|
||||
|
||||
Widget _buildContents() {
|
||||
return Material(
|
||||
child: Column(
|
||||
children: [
|
||||
addPadding(Chip(
|
||||
label: Text('Chip'),
|
||||
)),
|
||||
addPadding(InputChip(
|
||||
label: Text('InputChip'),
|
||||
)),
|
||||
addPadding(ChoiceChip(
|
||||
label: Text('Selected ChoiceChip'),
|
||||
selected: true,
|
||||
)),
|
||||
addPadding(ChoiceChip(
|
||||
label: Text('Deselected ChoiceChip'),
|
||||
selected: false,
|
||||
)),
|
||||
addPadding(FilterChip(
|
||||
label: Text('FilterChip'),
|
||||
selected: _filterChipSelected,
|
||||
onSelected: (bool newValue) {
|
||||
setState(() {
|
||||
_filterChipSelected = newValue;
|
||||
});
|
||||
},
|
||||
)),
|
||||
addPadding(ActionChip(
|
||||
label: Text('ActionChip'),
|
||||
onPressed: () {},
|
||||
)),
|
||||
addPadding(ActionChip(
|
||||
label: Text('Chip with avatar'),
|
||||
avatar: _hasAvatar
|
||||
? CircleAvatar(
|
||||
backgroundColor: Colors.amber,
|
||||
child: Text('Z'),
|
||||
)
|
||||
: null,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_hasAvatar = !_hasAvatar;
|
||||
});
|
||||
},
|
||||
)),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Padding addPadding(Widget widget) => Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: widget,
|
||||
);
|
||||
231
web/gallery/lib/demo/material/data_table_demo.dart
Normal file
231
web/gallery/lib/demo/material/data_table_demo.dart
Normal file
@@ -0,0 +1,231 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
import 'package:flutter_web/rendering.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class Dessert {
|
||||
Dessert(this.name, this.calories, this.fat, this.carbs, this.protein,
|
||||
this.sodium, this.calcium, this.iron);
|
||||
final String name;
|
||||
final int calories;
|
||||
final double fat;
|
||||
final int carbs;
|
||||
final double protein;
|
||||
final int sodium;
|
||||
final int calcium;
|
||||
final int iron;
|
||||
|
||||
bool selected = false;
|
||||
}
|
||||
|
||||
class DessertDataSource extends DataTableSource {
|
||||
final List<Dessert> _desserts = <Dessert>[
|
||||
Dessert('Frozen yogurt', 159, 6.0, 24, 4.0, 87, 14, 1),
|
||||
Dessert('Ice cream sandwich', 237, 9.0, 37, 4.3, 129, 8, 1),
|
||||
Dessert('Eclair', 262, 16.0, 24, 6.0, 337, 6, 7),
|
||||
Dessert('Cupcake', 305, 3.7, 67, 4.3, 413, 3, 8),
|
||||
Dessert('Gingerbread', 356, 16.0, 49, 3.9, 327, 7, 16),
|
||||
Dessert('Jelly bean', 375, 0.0, 94, 0.0, 50, 0, 0),
|
||||
Dessert('Lollipop', 392, 0.2, 98, 0.0, 38, 0, 2),
|
||||
Dessert('Honeycomb', 408, 3.2, 87, 6.5, 562, 0, 45),
|
||||
Dessert('Donut', 452, 25.0, 51, 4.9, 326, 2, 22),
|
||||
Dessert('KitKat', 518, 26.0, 65, 7.0, 54, 12, 6),
|
||||
Dessert('Frozen yogurt with sugar', 168, 6.0, 26, 4.0, 87, 14, 1),
|
||||
Dessert('Ice cream sandwich with sugar', 246, 9.0, 39, 4.3, 129, 8, 1),
|
||||
Dessert('Eclair with sugar', 271, 16.0, 26, 6.0, 337, 6, 7),
|
||||
Dessert('Cupcake with sugar', 314, 3.7, 69, 4.3, 413, 3, 8),
|
||||
Dessert('Gingerbread with sugar', 345, 16.0, 51, 3.9, 327, 7, 16),
|
||||
Dessert('Jelly bean with sugar', 364, 0.0, 96, 0.0, 50, 0, 0),
|
||||
Dessert('Lollipop with sugar', 401, 0.2, 100, 0.0, 38, 0, 2),
|
||||
Dessert('Honeycomb with sugar', 417, 3.2, 89, 6.5, 562, 0, 45),
|
||||
Dessert('Donut with sugar', 461, 25.0, 53, 4.9, 326, 2, 22),
|
||||
Dessert('KitKat with sugar', 527, 26.0, 67, 7.0, 54, 12, 6),
|
||||
Dessert('Frozen yogurt with honey', 223, 6.0, 36, 4.0, 87, 14, 1),
|
||||
Dessert('Ice cream sandwich with honey', 301, 9.0, 49, 4.3, 129, 8, 1),
|
||||
Dessert('Eclair with honey', 326, 16.0, 36, 6.0, 337, 6, 7),
|
||||
Dessert('Cupcake with honey', 369, 3.7, 79, 4.3, 413, 3, 8),
|
||||
Dessert('Gingerbread with honey', 420, 16.0, 61, 3.9, 327, 7, 16),
|
||||
Dessert('Jelly bean with honey', 439, 0.0, 106, 0.0, 50, 0, 0),
|
||||
Dessert('Lollipop with honey', 456, 0.2, 110, 0.0, 38, 0, 2),
|
||||
Dessert('Honeycomb with honey', 472, 3.2, 99, 6.5, 562, 0, 45),
|
||||
Dessert('Donut with honey', 516, 25.0, 63, 4.9, 326, 2, 22),
|
||||
Dessert('KitKat with honey', 582, 26.0, 77, 7.0, 54, 12, 6),
|
||||
Dessert('Frozen yogurt with milk', 262, 8.4, 36, 12.0, 194, 44, 1),
|
||||
Dessert('Ice cream sandwich with milk', 339, 11.4, 49, 12.3, 236, 38, 1),
|
||||
Dessert('Eclair with milk', 365, 18.4, 36, 14.0, 444, 36, 7),
|
||||
Dessert('Cupcake with milk', 408, 6.1, 79, 12.3, 520, 33, 8),
|
||||
Dessert('Gingerbread with milk', 459, 18.4, 61, 11.9, 434, 37, 16),
|
||||
Dessert('Jelly bean with milk', 478, 2.4, 106, 8.0, 157, 30, 0),
|
||||
Dessert('Lollipop with milk', 495, 2.6, 110, 8.0, 145, 30, 2),
|
||||
Dessert('Honeycomb with milk', 511, 5.6, 99, 14.5, 669, 30, 45),
|
||||
Dessert('Donut with milk', 555, 27.4, 63, 12.9, 433, 32, 22),
|
||||
Dessert('KitKat with milk', 621, 28.4, 77, 15.0, 161, 42, 6),
|
||||
Dessert('Coconut slice and frozen yogurt', 318, 21.0, 31, 5.5, 96, 14, 7),
|
||||
Dessert(
|
||||
'Coconut slice and ice cream sandwich', 396, 24.0, 44, 5.8, 138, 8, 7),
|
||||
Dessert('Coconut slice and eclair', 421, 31.0, 31, 7.5, 346, 6, 13),
|
||||
Dessert('Coconut slice and cupcake', 464, 18.7, 74, 5.8, 422, 3, 14),
|
||||
Dessert('Coconut slice and gingerbread', 515, 31.0, 56, 5.4, 316, 7, 22),
|
||||
Dessert('Coconut slice and jelly bean', 534, 15.0, 101, 1.5, 59, 0, 6),
|
||||
Dessert('Coconut slice and lollipop', 551, 15.2, 105, 1.5, 47, 0, 8),
|
||||
Dessert('Coconut slice and honeycomb', 567, 18.2, 94, 8.0, 571, 0, 51),
|
||||
Dessert('Coconut slice and donut', 611, 40.0, 58, 6.4, 335, 2, 28),
|
||||
Dessert('Coconut slice and KitKat', 677, 41.0, 72, 8.5, 63, 12, 12),
|
||||
];
|
||||
|
||||
void _sort<T>(Comparable<T> getField(Dessert d), bool ascending) {
|
||||
_desserts.sort((Dessert a, Dessert b) {
|
||||
if (!ascending) {
|
||||
final Dessert c = a;
|
||||
a = b;
|
||||
b = c;
|
||||
}
|
||||
final Comparable<T> aValue = getField(a);
|
||||
final Comparable<T> bValue = getField(b);
|
||||
return Comparable.compare(aValue, bValue);
|
||||
});
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
int _selectedCount = 0;
|
||||
|
||||
@override
|
||||
DataRow getRow(int index) {
|
||||
assert(index >= 0);
|
||||
if (index >= _desserts.length) return null;
|
||||
final Dessert dessert = _desserts[index];
|
||||
return DataRow.byIndex(
|
||||
index: index,
|
||||
selected: dessert.selected,
|
||||
onSelectChanged: (bool value) {
|
||||
if (dessert.selected != value) {
|
||||
_selectedCount += value ? 1 : -1;
|
||||
assert(_selectedCount >= 0);
|
||||
dessert.selected = value;
|
||||
notifyListeners();
|
||||
}
|
||||
},
|
||||
cells: <DataCell>[
|
||||
DataCell(Text('${dessert.name}')),
|
||||
DataCell(Text('${dessert.calories}')),
|
||||
DataCell(Text('${dessert.fat.toStringAsFixed(1)}')),
|
||||
DataCell(Text('${dessert.carbs}')),
|
||||
DataCell(Text('${dessert.protein.toStringAsFixed(1)}')),
|
||||
DataCell(Text('${dessert.sodium}')),
|
||||
DataCell(Text('${dessert.calcium}%')),
|
||||
DataCell(Text('${dessert.iron}%')),
|
||||
]);
|
||||
}
|
||||
|
||||
@override
|
||||
int get rowCount => _desserts.length;
|
||||
|
||||
@override
|
||||
bool get isRowCountApproximate => false;
|
||||
|
||||
@override
|
||||
int get selectedRowCount => _selectedCount;
|
||||
|
||||
void _selectAll(bool checked) {
|
||||
for (Dessert dessert in _desserts) dessert.selected = checked;
|
||||
_selectedCount = checked ? _desserts.length : 0;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class DataTableDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/data-table';
|
||||
|
||||
@override
|
||||
_DataTableDemoState createState() => _DataTableDemoState();
|
||||
}
|
||||
|
||||
class _DataTableDemoState extends State<DataTableDemo> {
|
||||
int _rowsPerPage = PaginatedDataTable.defaultRowsPerPage;
|
||||
int _sortColumnIndex;
|
||||
bool _sortAscending = true;
|
||||
final DessertDataSource _dessertsDataSource = DessertDataSource();
|
||||
|
||||
void _sort<T>(
|
||||
Comparable<T> getField(Dessert d), int columnIndex, bool ascending) {
|
||||
_dessertsDataSource._sort<T>(getField, ascending);
|
||||
setState(() {
|
||||
_sortColumnIndex = columnIndex;
|
||||
_sortAscending = ascending;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Data tables'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(DataTableDemo.routeName),
|
||||
],
|
||||
),
|
||||
body: ListView(padding: const EdgeInsets.all(20.0), children: <Widget>[
|
||||
PaginatedDataTable(
|
||||
header: const Text('Nutrition'),
|
||||
rowsPerPage: _rowsPerPage,
|
||||
onRowsPerPageChanged: (int value) {
|
||||
setState(() {
|
||||
_rowsPerPage = value;
|
||||
});
|
||||
},
|
||||
sortColumnIndex: _sortColumnIndex,
|
||||
sortAscending: _sortAscending,
|
||||
onSelectAll: _dessertsDataSource._selectAll,
|
||||
columns: <DataColumn>[
|
||||
DataColumn(
|
||||
label: const Text('Dessert (100g serving)'),
|
||||
onSort: (int columnIndex, bool ascending) => _sort<String>(
|
||||
(Dessert d) => d.name, columnIndex, ascending)),
|
||||
DataColumn(
|
||||
label: const Text('Calories'),
|
||||
tooltip:
|
||||
'The total amount of food energy in the given serving size.',
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort<num>(
|
||||
(Dessert d) => d.calories, columnIndex, ascending)),
|
||||
DataColumn(
|
||||
label: const Text('Fat (g)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort<num>(
|
||||
(Dessert d) => d.fat, columnIndex, ascending)),
|
||||
DataColumn(
|
||||
label: const Text('Carbs (g)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort<num>(
|
||||
(Dessert d) => d.carbs, columnIndex, ascending)),
|
||||
DataColumn(
|
||||
label: const Text('Protein (g)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort<num>(
|
||||
(Dessert d) => d.protein, columnIndex, ascending)),
|
||||
DataColumn(
|
||||
label: const Text('Sodium (mg)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort<num>(
|
||||
(Dessert d) => d.sodium, columnIndex, ascending)),
|
||||
DataColumn(
|
||||
label: const Text('Calcium (%)'),
|
||||
tooltip:
|
||||
'The amount of calcium as a percentage of the recommended daily amount.',
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort<num>(
|
||||
(Dessert d) => d.calcium, columnIndex, ascending)),
|
||||
DataColumn(
|
||||
label: const Text('Iron (%)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort<num>(
|
||||
(Dessert d) => d.iron, columnIndex, ascending)),
|
||||
],
|
||||
source: _dessertsDataSource)
|
||||
]));
|
||||
}
|
||||
}
|
||||
230
web/gallery/lib/demo/material/date_and_time_picker_demo.dart
Normal file
230
web/gallery/lib/demo/material/date_and_time_picker_demo.dart
Normal file
@@ -0,0 +1,230 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class _InputDropdown extends StatelessWidget {
|
||||
const _InputDropdown(
|
||||
{Key key,
|
||||
this.child,
|
||||
this.labelText,
|
||||
this.valueText,
|
||||
this.valueStyle,
|
||||
this.onPressed})
|
||||
: super(key: key);
|
||||
|
||||
final String labelText;
|
||||
final String valueText;
|
||||
final TextStyle valueStyle;
|
||||
final VoidCallback onPressed;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onPressed,
|
||||
child: InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: labelText,
|
||||
),
|
||||
baseStyle: valueStyle,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(valueText, style: valueStyle),
|
||||
Icon(Icons.arrow_drop_down,
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? Colors.grey.shade700
|
||||
: Colors.white70),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DateTimePicker extends StatelessWidget {
|
||||
const _DateTimePicker(
|
||||
{Key key,
|
||||
this.labelText,
|
||||
this.selectedDate,
|
||||
this.selectedTime,
|
||||
this.selectDate,
|
||||
this.selectTime})
|
||||
: super(key: key);
|
||||
|
||||
final String labelText;
|
||||
final DateTime selectedDate;
|
||||
final TimeOfDay selectedTime;
|
||||
final ValueChanged<DateTime> selectDate;
|
||||
final ValueChanged<TimeOfDay> selectTime;
|
||||
|
||||
Future<void> _selectDate(BuildContext context) async {
|
||||
final DateTime picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: selectedDate,
|
||||
firstDate: DateTime(2015, 8),
|
||||
lastDate: DateTime(2101));
|
||||
if (picked != null && picked != selectedDate) selectDate(picked);
|
||||
}
|
||||
|
||||
Future<void> _selectTime(BuildContext context) async {
|
||||
final TimeOfDay picked =
|
||||
await showTimePicker(context: context, initialTime: selectedTime);
|
||||
if (picked != null && picked != selectedTime) selectTime(picked);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TextStyle valueStyle = Theme.of(context).textTheme.title;
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: _InputDropdown(
|
||||
labelText: labelText,
|
||||
valueText: DateFormat.yMMMd().format(selectedDate),
|
||||
valueStyle: valueStyle,
|
||||
onPressed: () {
|
||||
_selectDate(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12.0),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: _InputDropdown(
|
||||
valueText: selectedTime.format(context),
|
||||
valueStyle: valueStyle,
|
||||
onPressed: () {
|
||||
_selectTime(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DateAndTimePickerDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/date-and-time-pickers';
|
||||
|
||||
@override
|
||||
_DateAndTimePickerDemoState createState() => _DateAndTimePickerDemoState();
|
||||
}
|
||||
|
||||
class _DateAndTimePickerDemoState extends State<DateAndTimePickerDemo> {
|
||||
DateTime _fromDate = DateTime.now();
|
||||
TimeOfDay _fromTime = const TimeOfDay(hour: 7, minute: 28);
|
||||
DateTime _toDate = DateTime.now();
|
||||
TimeOfDay _toTime = const TimeOfDay(hour: 7, minute: 28);
|
||||
final List<String> _allActivities = <String>[
|
||||
'hiking',
|
||||
'swimming',
|
||||
'boating',
|
||||
'fishing'
|
||||
];
|
||||
String _activity = 'fishing';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Date and time pickers'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(DateAndTimePickerDemo.routeName)
|
||||
],
|
||||
),
|
||||
body: DropdownButtonHideUnderline(
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: <Widget>[
|
||||
TextField(
|
||||
enabled: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Event name',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
style: Theme.of(context).textTheme.display1,
|
||||
),
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Location',
|
||||
),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.display1
|
||||
.copyWith(fontSize: 20.0),
|
||||
),
|
||||
_DateTimePicker(
|
||||
labelText: 'From',
|
||||
selectedDate: _fromDate,
|
||||
selectedTime: _fromTime,
|
||||
selectDate: (DateTime date) {
|
||||
setState(() {
|
||||
_fromDate = date;
|
||||
});
|
||||
},
|
||||
selectTime: (TimeOfDay time) {
|
||||
setState(() {
|
||||
_fromTime = time;
|
||||
});
|
||||
},
|
||||
),
|
||||
_DateTimePicker(
|
||||
labelText: 'To',
|
||||
selectedDate: _toDate,
|
||||
selectedTime: _toTime,
|
||||
selectDate: (DateTime date) {
|
||||
setState(() {
|
||||
_toDate = date;
|
||||
});
|
||||
},
|
||||
selectTime: (TimeOfDay time) {
|
||||
setState(() {
|
||||
_toTime = time;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
InputDecorator(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Activity',
|
||||
hintText: 'Choose an activity',
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
isEmpty: _activity == null,
|
||||
child: DropdownButton<String>(
|
||||
value: _activity,
|
||||
onChanged: (String newValue) {
|
||||
setState(() {
|
||||
_activity = newValue;
|
||||
});
|
||||
},
|
||||
items: _allActivities
|
||||
.map<DropdownMenuItem<String>>((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
211
web/gallery/lib/demo/material/dialog_demo.dart
Normal file
211
web/gallery/lib/demo/material/dialog_demo.dart
Normal file
@@ -0,0 +1,211 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
import 'full_screen_dialog_demo.dart';
|
||||
|
||||
enum DialogDemoAction {
|
||||
cancel,
|
||||
discard,
|
||||
disagree,
|
||||
agree,
|
||||
}
|
||||
|
||||
const String _alertWithoutTitleText = 'Discard draft?';
|
||||
|
||||
const String _alertWithTitleText =
|
||||
'Let Google help apps determine location. This means sending anonymous location '
|
||||
'data to Google, even when no apps are running.';
|
||||
|
||||
class DialogDemoItem extends StatelessWidget {
|
||||
const DialogDemoItem(
|
||||
{Key key, this.icon, this.color, this.text, this.onPressed})
|
||||
: super(key: key);
|
||||
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final String text;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SimpleDialogOption(
|
||||
onPressed: onPressed,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Icon(icon, size: 36.0, color: color),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: Text(text),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DialogDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/dialog';
|
||||
|
||||
@override
|
||||
DialogDemoState createState() => DialogDemoState();
|
||||
}
|
||||
|
||||
class DialogDemoState extends State<DialogDemo> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
TimeOfDay _selectedTime;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final DateTime now = DateTime.now();
|
||||
_selectedTime = TimeOfDay(hour: now.hour, minute: now.minute);
|
||||
}
|
||||
|
||||
void showDemoDialog<T>({BuildContext context, Widget child}) {
|
||||
showDialog<T>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => child,
|
||||
).then<void>((T value) {
|
||||
// The value passed to Navigator.pop() or null.
|
||||
if (value != null) {
|
||||
_scaffoldKey.currentState
|
||||
.showSnackBar(SnackBar(content: Text('You selected: $value')));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final TextStyle dialogTextStyle =
|
||||
theme.textTheme.subhead.copyWith(color: theme.textTheme.caption.color);
|
||||
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: const Text('Dialogs'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(DialogDemo.routeName)
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 24.0, horizontal: 72.0),
|
||||
children: <Widget>[
|
||||
RaisedButton(
|
||||
child: const Text('ALERT'),
|
||||
onPressed: () {
|
||||
showDemoDialog<DialogDemoAction>(
|
||||
context: context,
|
||||
child: AlertDialog(
|
||||
content: Text(_alertWithoutTitleText,
|
||||
style: dialogTextStyle),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
child: const Text('CANCEL'),
|
||||
onPressed: () {
|
||||
Navigator.pop(
|
||||
context, DialogDemoAction.cancel);
|
||||
}),
|
||||
FlatButton(
|
||||
child: const Text('DISCARD'),
|
||||
onPressed: () {
|
||||
Navigator.pop(
|
||||
context, DialogDemoAction.discard);
|
||||
})
|
||||
]));
|
||||
}),
|
||||
RaisedButton(
|
||||
child: const Text('ALERT WITH TITLE'),
|
||||
onPressed: () {
|
||||
showDemoDialog<DialogDemoAction>(
|
||||
context: context,
|
||||
child: AlertDialog(
|
||||
title:
|
||||
const Text('Use Google\'s location service?'),
|
||||
content: Text(_alertWithTitleText,
|
||||
style: dialogTextStyle),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
child: const Text('DISAGREE'),
|
||||
onPressed: () {
|
||||
Navigator.pop(
|
||||
context, DialogDemoAction.disagree);
|
||||
}),
|
||||
FlatButton(
|
||||
child: const Text('AGREE'),
|
||||
onPressed: () {
|
||||
Navigator.pop(
|
||||
context, DialogDemoAction.agree);
|
||||
})
|
||||
]));
|
||||
}),
|
||||
RaisedButton(
|
||||
child: const Text('SIMPLE'),
|
||||
onPressed: () {
|
||||
showDemoDialog<String>(
|
||||
context: context,
|
||||
child: SimpleDialog(
|
||||
title: const Text('Set backup account'),
|
||||
children: <Widget>[
|
||||
DialogDemoItem(
|
||||
icon: Icons.account_circle,
|
||||
color: theme.primaryColor,
|
||||
text: 'username@gmail.com',
|
||||
onPressed: () {
|
||||
Navigator.pop(
|
||||
context, 'username@gmail.com');
|
||||
}),
|
||||
DialogDemoItem(
|
||||
icon: Icons.account_circle,
|
||||
color: theme.primaryColor,
|
||||
text: 'user02@gmail.com',
|
||||
onPressed: () {
|
||||
Navigator.pop(context, 'user02@gmail.com');
|
||||
}),
|
||||
DialogDemoItem(
|
||||
icon: Icons.add_circle,
|
||||
text: 'add account',
|
||||
color: theme.disabledColor)
|
||||
]));
|
||||
}),
|
||||
RaisedButton(
|
||||
child: const Text('CONFIRMATION'),
|
||||
onPressed: () {
|
||||
showTimePicker(context: context, initialTime: _selectedTime)
|
||||
.then<void>((TimeOfDay value) {
|
||||
if (value != null && value != _selectedTime) {
|
||||
_selectedTime = value;
|
||||
_scaffoldKey.currentState.showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
'You selected: ${value.format(context)}')));
|
||||
}
|
||||
});
|
||||
}),
|
||||
RaisedButton(
|
||||
child: const Text('FULLSCREEN'),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<DismissDialogAction>(
|
||||
builder: (BuildContext context) =>
|
||||
FullScreenDialogDemo(),
|
||||
fullscreenDialog: true,
|
||||
));
|
||||
}),
|
||||
]
|
||||
// Add a little space between the buttons
|
||||
.map<Widget>((Widget button) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: button);
|
||||
}).toList()));
|
||||
}
|
||||
}
|
||||
197
web/gallery/lib/demo/material/drawer_demo.dart
Normal file
197
web/gallery/lib/demo/material/drawer_demo.dart
Normal file
@@ -0,0 +1,197 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class DrawerDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/drawer';
|
||||
|
||||
@override
|
||||
_DrawerDemoState createState() => _DrawerDemoState();
|
||||
}
|
||||
|
||||
class _DrawerDemoState extends State<DrawerDemo> with TickerProviderStateMixin {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
static const List<String> _drawerContents = <String>[
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
];
|
||||
|
||||
static final Animatable<Offset> _drawerDetailsTween = Tween<Offset>(
|
||||
begin: const Offset(0.0, -1.0),
|
||||
end: Offset.zero,
|
||||
).chain(CurveTween(
|
||||
curve: Curves.fastOutSlowIn,
|
||||
));
|
||||
|
||||
AnimationController _controller;
|
||||
Animation<double> _drawerContentsOpacity;
|
||||
Animation<Offset> _drawerDetailsPosition;
|
||||
bool _showDrawerContents = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
_drawerContentsOpacity = CurvedAnimation(
|
||||
parent: ReverseAnimation(_controller),
|
||||
curve: Curves.fastOutSlowIn,
|
||||
);
|
||||
_drawerDetailsPosition = _controller.drive(_drawerDetailsTween);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
IconData _backIcon() {
|
||||
switch (Theme.of(context).platform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
return Icons.arrow_back;
|
||||
case TargetPlatform.iOS:
|
||||
return Icons.arrow_back_ios;
|
||||
}
|
||||
assert(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
void _showNotImplementedMessage() {
|
||||
Navigator.pop(context); // Dismiss the drawer.
|
||||
_scaffoldKey.currentState.showSnackBar(
|
||||
const SnackBar(content: Text("The drawer's items don't do anything")));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: Icon(_backIcon()),
|
||||
alignment: Alignment.centerLeft,
|
||||
tooltip: 'Back',
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
title: const Text('Navigation drawer'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(DrawerDemo.routeName)
|
||||
],
|
||||
),
|
||||
drawer: Drawer(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
UserAccountsDrawerHeader(
|
||||
accountName: const Text('Trevor Widget'),
|
||||
accountEmail: const Text('trevor.widget@example.com'),
|
||||
margin: EdgeInsets.zero,
|
||||
onDetailsPressed: () {
|
||||
_showDrawerContents = !_showDrawerContents;
|
||||
if (_showDrawerContents)
|
||||
_controller.reverse();
|
||||
else
|
||||
_controller.forward();
|
||||
},
|
||||
),
|
||||
MediaQuery.removePadding(
|
||||
context: context,
|
||||
// DrawerHeader consumes top MediaQuery padding.
|
||||
removeTop: true,
|
||||
child: Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
children: <Widget>[
|
||||
Stack(
|
||||
children: <Widget>[
|
||||
// The initial contents of the drawer.
|
||||
FadeTransition(
|
||||
opacity: _drawerContentsOpacity,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: _drawerContents.map<Widget>((String id) {
|
||||
return ListTile(
|
||||
leading: CircleAvatar(child: Text(id)),
|
||||
title: Text('Drawer item $id'),
|
||||
onTap: _showNotImplementedMessage,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
// The drawer's "details" view.
|
||||
SlideTransition(
|
||||
position: _drawerDetailsPosition,
|
||||
child: FadeTransition(
|
||||
opacity: ReverseAnimation(_drawerContentsOpacity),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
leading: const Icon(Icons.add),
|
||||
title: const Text('Add account'),
|
||||
onTap: _showNotImplementedMessage,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
title: const Text('Manage accounts'),
|
||||
onTap: _showNotImplementedMessage,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: Center(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
_scaffoldKey.currentState.openDrawer();
|
||||
},
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: 'Open drawer',
|
||||
excludeSemantics: true,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 100.0,
|
||||
height: 100.0,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
'Tap here to open the drawer',
|
||||
style: Theme.of(context).textTheme.subhead,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
92
web/gallery/lib/demo/material/editable_text_demo.dart
Normal file
92
web/gallery/lib/demo/material/editable_text_demo.dart
Normal file
@@ -0,0 +1,92 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
class EditableTextDemo extends StatefulWidget {
|
||||
static String routeName = '/material/editable_text';
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => EditableTextDemoState();
|
||||
}
|
||||
|
||||
class EditableTextDemoState extends State<EditableTextDemo> {
|
||||
final cyanController = TextEditingController(text: 'Cyan');
|
||||
final orangeController = TextEditingController(text: 'Orange');
|
||||
final thickController = TextEditingController(text: 'Thick Rounded Cursor');
|
||||
final multiController =
|
||||
TextEditingController(text: 'First line\nSecond line');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Text Editing'),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
field(
|
||||
cyanController,
|
||||
color: Colors.cyan.shade50,
|
||||
selection: Colors.cyan.shade200,
|
||||
cursor: Colors.cyan.shade900,
|
||||
),
|
||||
field(
|
||||
orangeController,
|
||||
color: Colors.orange.shade50,
|
||||
selection: Colors.orange.shade200,
|
||||
cursor: Colors.orange.shade900,
|
||||
center: true,
|
||||
),
|
||||
field(
|
||||
thickController,
|
||||
color: Colors.white,
|
||||
selection: Colors.grey.shade200,
|
||||
cursor: Colors.red.shade900,
|
||||
radius: const Radius.circular(2),
|
||||
cursorWidth: 8,
|
||||
),
|
||||
Banner(
|
||||
child: TextField(
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
||||
controller: multiController,
|
||||
maxLines: 3,
|
||||
),
|
||||
message: 'W.I.P',
|
||||
textDirection: TextDirection.ltr,
|
||||
location: BannerLocation.bottomEnd,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget field(
|
||||
TextEditingController controller, {
|
||||
Color color,
|
||||
Color selection,
|
||||
Color cursor,
|
||||
Radius radius = null,
|
||||
double cursorWidth = 2,
|
||||
bool center = false,
|
||||
}) {
|
||||
return Theme(
|
||||
data: ThemeData(textSelectionColor: selection),
|
||||
child: Container(
|
||||
color: color,
|
||||
child: TextField(
|
||||
textAlign: center ? TextAlign.center : TextAlign.start,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.fromLTRB(8, 16, 8, 16),
|
||||
),
|
||||
controller: controller,
|
||||
cursorColor: cursor,
|
||||
cursorRadius: radius,
|
||||
cursorWidth: cursorWidth,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
69
web/gallery/lib/demo/material/elevation_demo.dart
Normal file
69
web/gallery/lib/demo/material/elevation_demo.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class ElevationDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/elevation';
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ElevationDemoState();
|
||||
}
|
||||
|
||||
class _ElevationDemoState extends State<ElevationDemo> {
|
||||
bool _showElevation = true;
|
||||
|
||||
List<Widget> buildCards() {
|
||||
const List<double> elevations = <double>[
|
||||
0.0,
|
||||
1.0,
|
||||
2.0,
|
||||
3.0,
|
||||
4.0,
|
||||
5.0,
|
||||
8.0,
|
||||
16.0,
|
||||
24.0,
|
||||
];
|
||||
|
||||
return elevations.map<Widget>((double elevation) {
|
||||
return Center(
|
||||
child: Card(
|
||||
margin: const EdgeInsets.all(20.0),
|
||||
elevation: _showElevation ? elevation : 0.0,
|
||||
child: SizedBox(
|
||||
height: 100.0,
|
||||
width: 100.0,
|
||||
child: Center(
|
||||
child: Text('${elevation.toStringAsFixed(0)} pt'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Elevation'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(ElevationDemo.routeName),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.sentiment_very_satisfied),
|
||||
onPressed: () {
|
||||
setState(() => _showElevation = !_showElevation);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
children: buildCards(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
335
web/gallery/lib/demo/material/expansion_panels_demo.dart
Normal file
335
web/gallery/lib/demo/material/expansion_panels_demo.dart
Normal file
@@ -0,0 +1,335 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
@visibleForTesting
|
||||
enum Location { Barbados, Bahamas, Bermuda }
|
||||
|
||||
typedef DemoItemBodyBuilder<T> = Widget Function(DemoItem<T> item);
|
||||
typedef ValueToString<T> = String Function(T value);
|
||||
|
||||
class DualHeaderWithHint extends StatelessWidget {
|
||||
const DualHeaderWithHint({this.name, this.value, this.hint, this.showHint});
|
||||
|
||||
final String name;
|
||||
final String value;
|
||||
final String hint;
|
||||
final bool showHint;
|
||||
|
||||
Widget _crossFade(Widget first, Widget second, bool isExpanded) {
|
||||
return AnimatedCrossFade(
|
||||
firstChild: first,
|
||||
secondChild: second,
|
||||
firstCurve: const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn),
|
||||
secondCurve: const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn),
|
||||
sizeCurve: Curves.fastOutSlowIn,
|
||||
crossFadeState:
|
||||
isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final TextTheme textTheme = theme.textTheme;
|
||||
|
||||
return Row(children: <Widget>[
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 24.0),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
name,
|
||||
style: textTheme.body1.copyWith(fontSize: 15.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 24.0),
|
||||
child: _crossFade(
|
||||
Text(value,
|
||||
style: textTheme.caption.copyWith(fontSize: 15.0)),
|
||||
Text(hint, style: textTheme.caption.copyWith(fontSize: 15.0)),
|
||||
showHint)))
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class CollapsibleBody extends StatelessWidget {
|
||||
const CollapsibleBody(
|
||||
{this.margin = EdgeInsets.zero, this.child, this.onSave, this.onCancel});
|
||||
|
||||
final EdgeInsets margin;
|
||||
final Widget child;
|
||||
final VoidCallback onSave;
|
||||
final VoidCallback onCancel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final TextTheme textTheme = theme.textTheme;
|
||||
|
||||
return Column(children: <Widget>[
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 24.0, right: 24.0, bottom: 24.0) -
|
||||
margin,
|
||||
child: Center(
|
||||
child: DefaultTextStyle(
|
||||
style: textTheme.caption.copyWith(fontSize: 15.0),
|
||||
child: child))),
|
||||
const Divider(height: 1.0),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child:
|
||||
Row(mainAxisAlignment: MainAxisAlignment.end, children: <Widget>[
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 8.0),
|
||||
child: FlatButton(
|
||||
onPressed: onCancel,
|
||||
child: const Text('CANCEL',
|
||||
style: TextStyle(
|
||||
color: Colors.black54,
|
||||
fontSize: 15.0,
|
||||
fontWeight: FontWeight.w500)))),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 8.0),
|
||||
child: FlatButton(
|
||||
onPressed: onSave,
|
||||
textTheme: ButtonTextTheme.accent,
|
||||
child: const Text('SAVE')))
|
||||
]))
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class DemoItem<T> {
|
||||
DemoItem({this.name, this.value, this.hint, this.builder, this.valueToString})
|
||||
: textController = TextEditingController(text: valueToString(value));
|
||||
|
||||
final String name;
|
||||
final String hint;
|
||||
final TextEditingController textController;
|
||||
final DemoItemBodyBuilder<T> builder;
|
||||
final ValueToString<T> valueToString;
|
||||
T value;
|
||||
bool isExpanded = false;
|
||||
|
||||
ExpansionPanelHeaderBuilder get headerBuilder {
|
||||
return (BuildContext context, bool isExpanded) {
|
||||
return DualHeaderWithHint(
|
||||
name: name,
|
||||
value: valueToString(value),
|
||||
hint: hint,
|
||||
showHint: isExpanded);
|
||||
};
|
||||
}
|
||||
|
||||
Widget build() => builder(this);
|
||||
}
|
||||
|
||||
class ExpansionPanelsDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/expansion_panels';
|
||||
|
||||
@override
|
||||
_ExpansionPanelsDemoState createState() => _ExpansionPanelsDemoState();
|
||||
}
|
||||
|
||||
class _ExpansionPanelsDemoState extends State<ExpansionPanelsDemo> {
|
||||
List<DemoItem<dynamic>> _demoItems;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_demoItems = <DemoItem<dynamic>>[
|
||||
DemoItem<String>(
|
||||
name: 'Trip',
|
||||
value: 'Caribbean cruise',
|
||||
hint: 'Change trip name',
|
||||
valueToString: (String value) => value,
|
||||
builder: (DemoItem<String> item) {
|
||||
void close() {
|
||||
setState(() {
|
||||
item.isExpanded = false;
|
||||
});
|
||||
}
|
||||
|
||||
return Form(
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return CollapsibleBody(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
onSave: () {
|
||||
Form.of(context).save();
|
||||
close();
|
||||
},
|
||||
onCancel: () {
|
||||
Form.of(context).reset();
|
||||
close();
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: TextFormField(
|
||||
controller: item.textController,
|
||||
decoration: InputDecoration(
|
||||
hintText: item.hint,
|
||||
labelText: item.name,
|
||||
),
|
||||
onSaved: (String value) {
|
||||
item.value = value;
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
DemoItem<Location>(
|
||||
name: 'Location',
|
||||
value: Location.Bahamas,
|
||||
hint: 'Select location',
|
||||
valueToString: (Location location) =>
|
||||
location.toString().split('.')[1],
|
||||
builder: (DemoItem<Location> item) {
|
||||
void close() {
|
||||
setState(() {
|
||||
item.isExpanded = false;
|
||||
});
|
||||
}
|
||||
|
||||
return Form(child: Builder(builder: (BuildContext context) {
|
||||
return CollapsibleBody(
|
||||
onSave: () {
|
||||
Form.of(context).save();
|
||||
close();
|
||||
},
|
||||
onCancel: () {
|
||||
Form.of(context).reset();
|
||||
close();
|
||||
},
|
||||
child: FormField<Location>(
|
||||
initialValue: item.value,
|
||||
onSaved: (Location result) {
|
||||
item.value = result;
|
||||
},
|
||||
builder: (FormFieldState<Location> field) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
RadioListTile<Location>(
|
||||
value: Location.Bahamas,
|
||||
title: const Text('Bahamas'),
|
||||
groupValue: field.value,
|
||||
onChanged: field.didChange,
|
||||
),
|
||||
RadioListTile<Location>(
|
||||
value: Location.Barbados,
|
||||
title: const Text('Barbados'),
|
||||
groupValue: field.value,
|
||||
onChanged: field.didChange,
|
||||
),
|
||||
RadioListTile<Location>(
|
||||
value: Location.Bermuda,
|
||||
title: const Text('Bermuda'),
|
||||
groupValue: field.value,
|
||||
onChanged: field.didChange,
|
||||
),
|
||||
]);
|
||||
}),
|
||||
);
|
||||
}));
|
||||
}),
|
||||
DemoItem<double>(
|
||||
name: 'Sun',
|
||||
value: 80.0,
|
||||
hint: 'Select sun level',
|
||||
valueToString: (double amount) => '${amount.round()}',
|
||||
builder: (DemoItem<double> item) {
|
||||
void close() {
|
||||
setState(() {
|
||||
item.isExpanded = false;
|
||||
});
|
||||
}
|
||||
|
||||
return Form(child: Builder(builder: (BuildContext context) {
|
||||
return CollapsibleBody(
|
||||
onSave: () {
|
||||
Form.of(context).save();
|
||||
close();
|
||||
},
|
||||
onCancel: () {
|
||||
Form.of(context).reset();
|
||||
close();
|
||||
},
|
||||
child: FormField<double>(
|
||||
initialValue: item.value,
|
||||
onSaved: (double value) {
|
||||
item.value = value;
|
||||
},
|
||||
builder: (FormFieldState<double> field) {
|
||||
return Slider(
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
divisions: 5,
|
||||
activeColor:
|
||||
Colors.orange[100 + (field.value * 5.0).round()],
|
||||
label: '${field.value.round()}',
|
||||
value: field.value,
|
||||
onChanged: field.didChange,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}));
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Expansion panels'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(ExpansionPanelsDemo.routeName),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(24.0),
|
||||
child: ExpansionPanelList(
|
||||
expansionCallback: (int index, bool isExpanded) {
|
||||
setState(() {
|
||||
_demoItems[index].isExpanded = !isExpanded;
|
||||
});
|
||||
},
|
||||
children:
|
||||
_demoItems.map<ExpansionPanel>((DemoItem<dynamic> item) {
|
||||
return ExpansionPanel(
|
||||
isExpanded: item.isExpanded,
|
||||
headerBuilder: item.headerBuilder,
|
||||
body: item.build());
|
||||
}).toList()),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
232
web/gallery/lib/demo/material/full_screen_dialog_demo.dart
Normal file
232
web/gallery/lib/demo/material/full_screen_dialog_demo.dart
Normal file
@@ -0,0 +1,232 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
// This demo is based on
|
||||
// https://material.google.com/components/dialogs.html#dialogs-full-screen-dialogs
|
||||
|
||||
enum DismissDialogAction {
|
||||
cancel,
|
||||
discard,
|
||||
save,
|
||||
}
|
||||
|
||||
class DateTimeItem extends StatelessWidget {
|
||||
DateTimeItem({Key key, DateTime dateTime, @required this.onChanged})
|
||||
: assert(onChanged != null),
|
||||
date = DateTime(dateTime.year, dateTime.month, dateTime.day),
|
||||
time = TimeOfDay(hour: dateTime.hour, minute: dateTime.minute),
|
||||
super(key: key);
|
||||
|
||||
final DateTime date;
|
||||
final TimeOfDay time;
|
||||
final ValueChanged<DateTime> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
|
||||
return DefaultTextStyle(
|
||||
style: theme.textTheme.subhead,
|
||||
child: Row(children: <Widget>[
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: theme.dividerColor))),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
showDatePicker(
|
||||
context: context,
|
||||
initialDate: date,
|
||||
firstDate:
|
||||
date.subtract(const Duration(days: 30)),
|
||||
lastDate: date.add(const Duration(days: 30)))
|
||||
.then<void>((DateTime value) {
|
||||
if (value != null)
|
||||
onChanged(DateTime(value.year, value.month,
|
||||
value.day, time.hour, time.minute));
|
||||
});
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(DateFormat('EEE, MMM d yyyy').format(date)),
|
||||
const Icon(Icons.arrow_drop_down,
|
||||
color: Colors.black54),
|
||||
])))),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 8.0),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
decoration: BoxDecoration(
|
||||
border:
|
||||
Border(bottom: BorderSide(color: theme.dividerColor))),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
showTimePicker(context: context, initialTime: time)
|
||||
.then<void>((TimeOfDay value) {
|
||||
if (value != null)
|
||||
onChanged(DateTime(date.year, date.month, date.day,
|
||||
value.hour, value.minute));
|
||||
});
|
||||
},
|
||||
child: Row(children: <Widget>[
|
||||
Text('${time.format(context)}'),
|
||||
const Icon(Icons.arrow_drop_down, color: Colors.black54),
|
||||
])))
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
class FullScreenDialogDemo extends StatefulWidget {
|
||||
@override
|
||||
FullScreenDialogDemoState createState() => FullScreenDialogDemoState();
|
||||
}
|
||||
|
||||
class FullScreenDialogDemoState extends State<FullScreenDialogDemo> {
|
||||
DateTime _fromDateTime = DateTime.now();
|
||||
DateTime _toDateTime = DateTime.now();
|
||||
bool _allDayValue = false;
|
||||
bool _saveNeeded = false;
|
||||
bool _hasLocation = false;
|
||||
bool _hasName = false;
|
||||
String _eventName;
|
||||
|
||||
Future<bool> _onWillPop() async {
|
||||
_saveNeeded = _hasLocation || _hasName || _saveNeeded;
|
||||
if (!_saveNeeded) return true;
|
||||
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final TextStyle dialogTextStyle =
|
||||
theme.textTheme.subhead.copyWith(color: theme.textTheme.caption.color);
|
||||
|
||||
return await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
content: Text('Discard new event?', style: dialogTextStyle),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
child: const Text('CANCEL'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(
|
||||
false); // Pops the confirmation dialog but not the page.
|
||||
}),
|
||||
FlatButton(
|
||||
child: const Text('DISCARD'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(
|
||||
true); // Returning true to _onWillPop will pop again.
|
||||
})
|
||||
],
|
||||
);
|
||||
},
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_hasName ? _eventName : 'Event Name TBD'),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
child: Text('SAVE',
|
||||
style: theme.textTheme.body1.copyWith(color: Colors.white)),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, DismissDialogAction.save);
|
||||
})
|
||||
]),
|
||||
body: Form(
|
||||
onWillPop: _onWillPop,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Event name', filled: true),
|
||||
style: theme.textTheme.headline,
|
||||
onChanged: (String value) {
|
||||
setState(() {
|
||||
_hasName = value.isNotEmpty;
|
||||
if (_hasName) {
|
||||
_eventName = value;
|
||||
}
|
||||
});
|
||||
})),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Location',
|
||||
hintText: 'Where is the event?',
|
||||
filled: true),
|
||||
onChanged: (String value) {
|
||||
setState(() {
|
||||
_hasLocation = value.isNotEmpty;
|
||||
});
|
||||
})),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text('From', style: theme.textTheme.caption),
|
||||
DateTimeItem(
|
||||
dateTime: _fromDateTime,
|
||||
onChanged: (DateTime value) {
|
||||
setState(() {
|
||||
_fromDateTime = value;
|
||||
_saveNeeded = true;
|
||||
});
|
||||
})
|
||||
]),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text('To', style: theme.textTheme.caption),
|
||||
DateTimeItem(
|
||||
dateTime: _toDateTime,
|
||||
onChanged: (DateTime value) {
|
||||
setState(() {
|
||||
_toDateTime = value;
|
||||
_saveNeeded = true;
|
||||
});
|
||||
}),
|
||||
const Text('All-day'),
|
||||
]),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: theme.dividerColor))),
|
||||
child: Row(children: <Widget>[
|
||||
Checkbox(
|
||||
value: _allDayValue,
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
_allDayValue = value;
|
||||
_saveNeeded = true;
|
||||
});
|
||||
}),
|
||||
const Text('All-day'),
|
||||
]))
|
||||
].map<Widget>((Widget child) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
height: 96.0,
|
||||
child: child);
|
||||
}).toList())),
|
||||
);
|
||||
}
|
||||
}
|
||||
397
web/gallery/lib/demo/material/grid_list_demo.dart
Normal file
397
web/gallery/lib/demo/material/grid_list_demo.dart
Normal file
@@ -0,0 +1,397 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
enum GridDemoTileStyle { imageOnly, oneLine, twoLine }
|
||||
|
||||
typedef BannerTapCallback = void Function(Photo photo);
|
||||
|
||||
const double _kMinFlingVelocity = 800.0;
|
||||
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
|
||||
|
||||
class Photo {
|
||||
Photo({
|
||||
this.assetName,
|
||||
this.assetPackage,
|
||||
this.title,
|
||||
this.caption,
|
||||
this.isFavorite = false,
|
||||
});
|
||||
|
||||
final String assetName;
|
||||
final String assetPackage;
|
||||
final String title;
|
||||
final String caption;
|
||||
|
||||
bool isFavorite;
|
||||
String get tag => assetName; // Assuming that all asset names are unique.
|
||||
|
||||
bool get isValid =>
|
||||
assetName != null &&
|
||||
title != null &&
|
||||
caption != null &&
|
||||
isFavorite != null;
|
||||
}
|
||||
|
||||
class GridPhotoViewer extends StatefulWidget {
|
||||
const GridPhotoViewer({Key key, this.photo}) : super(key: key);
|
||||
|
||||
final Photo photo;
|
||||
|
||||
@override
|
||||
_GridPhotoViewerState createState() => _GridPhotoViewerState();
|
||||
}
|
||||
|
||||
class _GridTitleText extends StatelessWidget {
|
||||
const _GridTitleText(this.text);
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(text),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GridPhotoViewerState extends State<GridPhotoViewer>
|
||||
with SingleTickerProviderStateMixin {
|
||||
AnimationController _controller;
|
||||
Animation<Offset> _flingAnimation;
|
||||
Offset _offset = Offset.zero;
|
||||
double _scale = 1.0;
|
||||
Offset _normalizedOffset;
|
||||
double _previousScale;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(vsync: this)
|
||||
..addListener(_handleFlingAnimation);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// The maximum offset value is 0,0. If the size of this renderer's box is w,h
|
||||
// then the minimum offset value is w - _scale * w, h - _scale * h.
|
||||
Offset _clampOffset(Offset offset) {
|
||||
final Size size = context.size;
|
||||
final Offset minOffset = Offset(size.width, size.height) * (1.0 - _scale);
|
||||
return Offset(
|
||||
offset.dx.clamp(minOffset.dx, 0.0), offset.dy.clamp(minOffset.dy, 0.0));
|
||||
}
|
||||
|
||||
void _handleFlingAnimation() {
|
||||
setState(() {
|
||||
_offset = _flingAnimation.value;
|
||||
});
|
||||
}
|
||||
|
||||
void _handleOnScaleStart(ScaleStartDetails details) {
|
||||
setState(() {
|
||||
_previousScale = _scale;
|
||||
_normalizedOffset = (details.focalPoint - _offset) / _scale;
|
||||
// The fling animation stops if an input gesture starts.
|
||||
_controller.stop();
|
||||
});
|
||||
}
|
||||
|
||||
void _handleOnScaleUpdate(ScaleUpdateDetails details) {
|
||||
setState(() {
|
||||
_scale = (_previousScale * details.scale).clamp(1.0, 4.0);
|
||||
// Ensure that image location under the focal point stays in the same place despite scaling.
|
||||
_offset = _clampOffset(details.focalPoint - _normalizedOffset * _scale);
|
||||
});
|
||||
}
|
||||
|
||||
void _handleOnScaleEnd(ScaleEndDetails details) {
|
||||
final double magnitude = details.velocity.pixelsPerSecond.distance;
|
||||
if (magnitude < _kMinFlingVelocity) return;
|
||||
final Offset direction = details.velocity.pixelsPerSecond / magnitude;
|
||||
final double distance = (Offset.zero & context.size).shortestSide;
|
||||
_flingAnimation = _controller.drive(Tween<Offset>(
|
||||
begin: _offset, end: _clampOffset(_offset + direction * distance)));
|
||||
_controller
|
||||
..value = 0.0
|
||||
..fling(velocity: magnitude / 1000.0);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onScaleStart: _handleOnScaleStart,
|
||||
onScaleUpdate: _handleOnScaleUpdate,
|
||||
onScaleEnd: _handleOnScaleEnd,
|
||||
child: ClipRect(
|
||||
child: Transform(
|
||||
transform: Matrix4.identity()
|
||||
..translate(_offset.dx, _offset.dy)
|
||||
..scale(_scale),
|
||||
child: Image.asset(
|
||||
'${widget.photo.assetName}',
|
||||
// TODO(flutter_web): package: widget.photo.assetPackage,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GridDemoPhotoItem extends StatelessWidget {
|
||||
GridDemoPhotoItem(
|
||||
{Key key,
|
||||
@required this.photo,
|
||||
@required this.tileStyle,
|
||||
@required this.onBannerTap})
|
||||
: assert(photo != null && photo.isValid),
|
||||
assert(tileStyle != null),
|
||||
assert(onBannerTap != null),
|
||||
super(key: key);
|
||||
|
||||
final Photo photo;
|
||||
final GridDemoTileStyle tileStyle;
|
||||
final BannerTapCallback
|
||||
onBannerTap; // User taps on the photo's header or footer.
|
||||
|
||||
void showPhoto(BuildContext context) {
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute<void>(builder: (BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(photo.title)),
|
||||
body: SizedBox.expand(
|
||||
child: Hero(
|
||||
tag: photo.tag,
|
||||
child: GridPhotoViewer(photo: photo),
|
||||
),
|
||||
),
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Widget image = GestureDetector(
|
||||
onTap: () {
|
||||
showPhoto(context);
|
||||
},
|
||||
child: Hero(
|
||||
key: Key(photo.assetName),
|
||||
tag: photo.tag,
|
||||
child: Image.asset(
|
||||
'${photo.assetName}',
|
||||
// TODO(flutter_web): package: photo.assetPackage,
|
||||
fit: BoxFit.cover,
|
||||
)));
|
||||
|
||||
final IconData icon = photo.isFavorite ? Icons.star : Icons.star_border;
|
||||
|
||||
switch (tileStyle) {
|
||||
case GridDemoTileStyle.imageOnly:
|
||||
return image;
|
||||
|
||||
case GridDemoTileStyle.oneLine:
|
||||
return GridTile(
|
||||
header: GestureDetector(
|
||||
onTap: () {
|
||||
onBannerTap(photo);
|
||||
},
|
||||
child: GridTileBar(
|
||||
title: _GridTitleText(photo.title),
|
||||
backgroundColor: Colors.black45,
|
||||
leading: Icon(
|
||||
icon,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: image,
|
||||
);
|
||||
|
||||
case GridDemoTileStyle.twoLine:
|
||||
return GridTile(
|
||||
footer: GestureDetector(
|
||||
onTap: () {
|
||||
onBannerTap(photo);
|
||||
},
|
||||
child: GridTileBar(
|
||||
backgroundColor: Colors.black45,
|
||||
title: _GridTitleText(photo.title),
|
||||
subtitle: _GridTitleText(photo.caption),
|
||||
trailing: Icon(
|
||||
icon,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: image,
|
||||
);
|
||||
}
|
||||
assert(tileStyle != null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class GridListDemo extends StatefulWidget {
|
||||
const GridListDemo({Key key}) : super(key: key);
|
||||
|
||||
static const String routeName = '/material/grid-list';
|
||||
|
||||
@override
|
||||
GridListDemoState createState() => GridListDemoState();
|
||||
}
|
||||
|
||||
class GridListDemoState extends State<GridListDemo> {
|
||||
GridDemoTileStyle _tileStyle = GridDemoTileStyle.twoLine;
|
||||
|
||||
List<Photo> photos = <Photo>[
|
||||
Photo(
|
||||
assetName: 'places/india_chennai_flower_market.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Chennai',
|
||||
caption: 'Flower Market',
|
||||
),
|
||||
Photo(
|
||||
assetName: 'places/india_tanjore_bronze_works.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Tanjore',
|
||||
caption: 'Bronze Works',
|
||||
),
|
||||
Photo(
|
||||
assetName: 'places/india_tanjore_market_merchant.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Tanjore',
|
||||
caption: 'Market',
|
||||
),
|
||||
Photo(
|
||||
assetName: 'places/india_tanjore_thanjavur_temple.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Tanjore',
|
||||
caption: 'Thanjavur Temple',
|
||||
),
|
||||
Photo(
|
||||
assetName: 'places/india_tanjore_thanjavur_temple_carvings.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Tanjore',
|
||||
caption: 'Thanjavur Temple',
|
||||
),
|
||||
Photo(
|
||||
assetName: 'places/india_pondicherry_salt_farm.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Pondicherry',
|
||||
caption: 'Salt Farm',
|
||||
),
|
||||
Photo(
|
||||
assetName: 'places/india_chennai_highway.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Chennai',
|
||||
caption: 'Scooters',
|
||||
),
|
||||
Photo(
|
||||
assetName: 'places/india_chettinad_silk_maker.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Chettinad',
|
||||
caption: 'Silk Maker',
|
||||
),
|
||||
Photo(
|
||||
assetName: 'places/india_chettinad_produce.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Chettinad',
|
||||
caption: 'Lunch Prep',
|
||||
),
|
||||
Photo(
|
||||
assetName: 'places/india_tanjore_market_technology.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Tanjore',
|
||||
caption: 'Market',
|
||||
),
|
||||
Photo(
|
||||
assetName: 'places/india_pondicherry_beach.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Pondicherry',
|
||||
caption: 'Beach',
|
||||
),
|
||||
Photo(
|
||||
assetName: 'places/india_pondicherry_fisherman.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Pondicherry',
|
||||
caption: 'Fisherman',
|
||||
),
|
||||
];
|
||||
|
||||
void changeTileStyle(GridDemoTileStyle value) {
|
||||
setState(() {
|
||||
_tileStyle = value;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Orientation orientation = MediaQuery.of(context).orientation;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Grid list'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(GridListDemo.routeName),
|
||||
PopupMenuButton<GridDemoTileStyle>(
|
||||
onSelected: changeTileStyle,
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuItem<GridDemoTileStyle>>[
|
||||
const PopupMenuItem<GridDemoTileStyle>(
|
||||
value: GridDemoTileStyle.imageOnly,
|
||||
child: Text('Image only'),
|
||||
),
|
||||
const PopupMenuItem<GridDemoTileStyle>(
|
||||
value: GridDemoTileStyle.oneLine,
|
||||
child: Text('One line'),
|
||||
),
|
||||
const PopupMenuItem<GridDemoTileStyle>(
|
||||
value: GridDemoTileStyle.twoLine,
|
||||
child: Text('Two line'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: GridView.count(
|
||||
crossAxisCount: (orientation == Orientation.portrait) ? 2 : 3,
|
||||
mainAxisSpacing: 4.0,
|
||||
crossAxisSpacing: 4.0,
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
childAspectRatio:
|
||||
(orientation == Orientation.portrait) ? 1.0 : 1.3,
|
||||
children: photos.map<Widget>((Photo photo) {
|
||||
return GridDemoPhotoItem(
|
||||
photo: photo,
|
||||
tileStyle: _tileStyle,
|
||||
onBannerTap: (Photo photo) {
|
||||
setState(() {
|
||||
photo.isFavorite = !photo.isFavorite;
|
||||
});
|
||||
});
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
135
web/gallery/lib/demo/material/icons_demo.dart
Normal file
135
web/gallery/lib/demo/material/icons_demo.dart
Normal file
@@ -0,0 +1,135 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class IconsDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/icons';
|
||||
|
||||
@override
|
||||
IconsDemoState createState() => IconsDemoState();
|
||||
}
|
||||
|
||||
class IconsDemoState extends State<IconsDemo> {
|
||||
static final List<MaterialColor> iconColors = <MaterialColor>[
|
||||
Colors.red,
|
||||
Colors.pink,
|
||||
Colors.purple,
|
||||
Colors.deepPurple,
|
||||
Colors.indigo,
|
||||
Colors.blue,
|
||||
Colors.lightBlue,
|
||||
Colors.cyan,
|
||||
Colors.teal,
|
||||
Colors.green,
|
||||
Colors.lightGreen,
|
||||
Colors.lime,
|
||||
Colors.yellow,
|
||||
Colors.amber,
|
||||
Colors.orange,
|
||||
Colors.deepOrange,
|
||||
Colors.brown,
|
||||
Colors.grey,
|
||||
Colors.blueGrey,
|
||||
];
|
||||
|
||||
int iconColorIndex = 8; // teal
|
||||
|
||||
Color get iconColor => iconColors[iconColorIndex];
|
||||
|
||||
void handleIconButtonPress() {
|
||||
setState(() {
|
||||
iconColorIndex = (iconColorIndex + 1) % iconColors.length;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Icons'),
|
||||
actions: <Widget>[MaterialDemoDocumentationButton(IconsDemo.routeName)],
|
||||
),
|
||||
body: IconTheme(
|
||||
data: IconThemeData(color: iconColor),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
children: <Widget>[
|
||||
_IconsDemoCard(
|
||||
handleIconButtonPress, Icons.face), // direction-agnostic icon
|
||||
const SizedBox(height: 24.0),
|
||||
_IconsDemoCard(handleIconButtonPress,
|
||||
Icons.battery_unknown), // direction-aware icon
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _IconsDemoCard extends StatelessWidget {
|
||||
const _IconsDemoCard(this.handleIconButtonPress, this.icon);
|
||||
|
||||
final VoidCallback handleIconButtonPress;
|
||||
final IconData icon;
|
||||
|
||||
Widget _buildIconButton(double iconSize, IconData icon, bool enabled) {
|
||||
return IconButton(
|
||||
icon: Icon(icon),
|
||||
iconSize: iconSize,
|
||||
tooltip: "${enabled ? 'Enabled' : 'Disabled'} icon button",
|
||||
onPressed: enabled ? handleIconButtonPress : null);
|
||||
}
|
||||
|
||||
Widget _centeredText(String label) => Padding(
|
||||
// Match the default padding of IconButton.
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(label, textAlign: TextAlign.center),
|
||||
);
|
||||
|
||||
TableRow _buildIconRow(double size) {
|
||||
return TableRow(
|
||||
children: <Widget>[
|
||||
_centeredText(size.floor().toString()),
|
||||
_buildIconButton(size, icon, true),
|
||||
_buildIconButton(size, icon, false),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final TextStyle textStyle =
|
||||
theme.textTheme.subhead.copyWith(color: theme.textTheme.caption.color);
|
||||
return Card(
|
||||
child: DefaultTextStyle(
|
||||
style: textStyle,
|
||||
child: Semantics(
|
||||
explicitChildNodes: true,
|
||||
child: Table(
|
||||
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
||||
children: <TableRow>[
|
||||
TableRow(children: <Widget>[
|
||||
_centeredText('Size'),
|
||||
_centeredText('Enabled'),
|
||||
_centeredText('Disabled'),
|
||||
]),
|
||||
_buildIconRow(18.0),
|
||||
_buildIconRow(24.0),
|
||||
_buildIconRow(36.0),
|
||||
_buildIconRow(48.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
228
web/gallery/lib/demo/material/leave_behind_demo.dart
Normal file
228
web/gallery/lib/demo/material/leave_behind_demo.dart
Normal file
@@ -0,0 +1,228 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:collection/collection.dart' show lowerBound;
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
import 'package:flutter_web/semantics.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
enum LeaveBehindDemoAction { reset, horizontalSwipe, leftSwipe, rightSwipe }
|
||||
|
||||
class LeaveBehindItem implements Comparable<LeaveBehindItem> {
|
||||
LeaveBehindItem({this.index, this.name, this.subject, this.body});
|
||||
|
||||
LeaveBehindItem.from(LeaveBehindItem item)
|
||||
: index = item.index,
|
||||
name = item.name,
|
||||
subject = item.subject,
|
||||
body = item.body;
|
||||
|
||||
final int index;
|
||||
final String name;
|
||||
final String subject;
|
||||
final String body;
|
||||
|
||||
@override
|
||||
int compareTo(LeaveBehindItem other) => index.compareTo(other.index);
|
||||
}
|
||||
|
||||
class LeaveBehindDemo extends StatefulWidget {
|
||||
const LeaveBehindDemo({Key key}) : super(key: key);
|
||||
|
||||
static const String routeName = '/material/leave-behind';
|
||||
|
||||
@override
|
||||
LeaveBehindDemoState createState() => LeaveBehindDemoState();
|
||||
}
|
||||
|
||||
class LeaveBehindDemoState extends State<LeaveBehindDemo> {
|
||||
static final GlobalKey<ScaffoldState> _scaffoldKey =
|
||||
GlobalKey<ScaffoldState>();
|
||||
DismissDirection _dismissDirection = DismissDirection.horizontal;
|
||||
List<LeaveBehindItem> leaveBehindItems;
|
||||
|
||||
void initListItems() {
|
||||
leaveBehindItems = List<LeaveBehindItem>.generate(16, (int index) {
|
||||
return LeaveBehindItem(
|
||||
index: index,
|
||||
name: 'Item $index Sender',
|
||||
subject: 'Subject: $index',
|
||||
body: "[$index] first line of the message's body...");
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initListItems();
|
||||
}
|
||||
|
||||
void handleDemoAction(LeaveBehindDemoAction action) {
|
||||
setState(() {
|
||||
switch (action) {
|
||||
case LeaveBehindDemoAction.reset:
|
||||
initListItems();
|
||||
break;
|
||||
case LeaveBehindDemoAction.horizontalSwipe:
|
||||
_dismissDirection = DismissDirection.horizontal;
|
||||
break;
|
||||
case LeaveBehindDemoAction.leftSwipe:
|
||||
_dismissDirection = DismissDirection.endToStart;
|
||||
break;
|
||||
case LeaveBehindDemoAction.rightSwipe:
|
||||
_dismissDirection = DismissDirection.startToEnd;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void handleUndo(LeaveBehindItem item) {
|
||||
final int insertionIndex = lowerBound(leaveBehindItems, item);
|
||||
setState(() {
|
||||
leaveBehindItems.insert(insertionIndex, item);
|
||||
});
|
||||
}
|
||||
|
||||
void _handleArchive(LeaveBehindItem item) {
|
||||
setState(() {
|
||||
leaveBehindItems.remove(item);
|
||||
});
|
||||
_scaffoldKey.currentState.showSnackBar(SnackBar(
|
||||
content: Text('You archived item ${item.index}'),
|
||||
action: SnackBarAction(
|
||||
label: 'UNDO',
|
||||
onPressed: () {
|
||||
handleUndo(item);
|
||||
})));
|
||||
}
|
||||
|
||||
void _handleDelete(LeaveBehindItem item) {
|
||||
setState(() {
|
||||
leaveBehindItems.remove(item);
|
||||
});
|
||||
_scaffoldKey.currentState.showSnackBar(SnackBar(
|
||||
content: Text('You deleted item ${item.index}'),
|
||||
action: SnackBarAction(
|
||||
label: 'UNDO',
|
||||
onPressed: () {
|
||||
handleUndo(item);
|
||||
})));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget body;
|
||||
if (leaveBehindItems.isEmpty) {
|
||||
body = Center(
|
||||
child: RaisedButton(
|
||||
onPressed: () => handleDemoAction(LeaveBehindDemoAction.reset),
|
||||
child: const Text('Reset the list'),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
body = ListView(
|
||||
children: leaveBehindItems.map<Widget>((LeaveBehindItem item) {
|
||||
return _LeaveBehindListItem(
|
||||
item: item,
|
||||
onArchive: _handleArchive,
|
||||
onDelete: _handleDelete,
|
||||
dismissDirection: _dismissDirection,
|
||||
);
|
||||
}).toList());
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(title: const Text('Swipe to dismiss'), actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(LeaveBehindDemo.routeName),
|
||||
PopupMenuButton<LeaveBehindDemoAction>(
|
||||
onSelected: handleDemoAction,
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuEntry<LeaveBehindDemoAction>>[
|
||||
const PopupMenuItem<LeaveBehindDemoAction>(
|
||||
value: LeaveBehindDemoAction.reset,
|
||||
child: Text('Reset the list')),
|
||||
const PopupMenuDivider(),
|
||||
CheckedPopupMenuItem<LeaveBehindDemoAction>(
|
||||
value: LeaveBehindDemoAction.horizontalSwipe,
|
||||
checked: _dismissDirection == DismissDirection.horizontal,
|
||||
child: const Text('Horizontal swipe')),
|
||||
CheckedPopupMenuItem<LeaveBehindDemoAction>(
|
||||
value: LeaveBehindDemoAction.leftSwipe,
|
||||
checked: _dismissDirection == DismissDirection.endToStart,
|
||||
child: const Text('Only swipe left')),
|
||||
CheckedPopupMenuItem<LeaveBehindDemoAction>(
|
||||
value: LeaveBehindDemoAction.rightSwipe,
|
||||
checked: _dismissDirection == DismissDirection.startToEnd,
|
||||
child: const Text('Only swipe right'))
|
||||
])
|
||||
]),
|
||||
body: body,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LeaveBehindListItem extends StatelessWidget {
|
||||
const _LeaveBehindListItem({
|
||||
Key key,
|
||||
@required this.item,
|
||||
@required this.onArchive,
|
||||
@required this.onDelete,
|
||||
@required this.dismissDirection,
|
||||
}) : super(key: key);
|
||||
|
||||
final LeaveBehindItem item;
|
||||
final DismissDirection dismissDirection;
|
||||
final void Function(LeaveBehindItem) onArchive;
|
||||
final void Function(LeaveBehindItem) onDelete;
|
||||
|
||||
void _handleArchive() {
|
||||
onArchive(item);
|
||||
}
|
||||
|
||||
void _handleDelete() {
|
||||
onDelete(item);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return Semantics(
|
||||
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
|
||||
const CustomSemanticsAction(label: 'Archive'): _handleArchive,
|
||||
const CustomSemanticsAction(label: 'Delete'): _handleDelete,
|
||||
},
|
||||
child: Dismissible(
|
||||
key: ObjectKey(item),
|
||||
direction: dismissDirection,
|
||||
onDismissed: (DismissDirection direction) {
|
||||
if (direction == DismissDirection.endToStart)
|
||||
_handleArchive();
|
||||
else
|
||||
_handleDelete();
|
||||
},
|
||||
background: Container(
|
||||
color: theme.primaryColor,
|
||||
child: const ListTile(
|
||||
leading: Icon(Icons.delete, color: Colors.white, size: 36.0))),
|
||||
secondaryBackground: Container(
|
||||
color: theme.primaryColor,
|
||||
child: const ListTile(
|
||||
trailing:
|
||||
Icon(Icons.archive, color: Colors.white, size: 36.0))),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.canvasColor,
|
||||
border: Border(bottom: BorderSide(color: theme.dividerColor))),
|
||||
child: ListTile(
|
||||
title: Text(item.name),
|
||||
subtitle: Text('${item.subject}\n${item.body}'),
|
||||
isThreeLine: true),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
273
web/gallery/lib/demo/material/list_demo.dart
Normal file
273
web/gallery/lib/demo/material/list_demo.dart
Normal file
@@ -0,0 +1,273 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
enum _MaterialListType {
|
||||
/// A list tile that contains a single line of text.
|
||||
oneLine,
|
||||
|
||||
/// A list tile that contains a [CircleAvatar] followed by a single line of text.
|
||||
oneLineWithAvatar,
|
||||
|
||||
/// A list tile that contains two lines of text.
|
||||
twoLine,
|
||||
|
||||
/// A list tile that contains three lines of text.
|
||||
threeLine,
|
||||
}
|
||||
|
||||
class ListDemo extends StatefulWidget {
|
||||
const ListDemo({Key key}) : super(key: key);
|
||||
|
||||
static const String routeName = '/material/list';
|
||||
|
||||
@override
|
||||
_ListDemoState createState() => _ListDemoState();
|
||||
}
|
||||
|
||||
class _ListDemoState extends State<ListDemo> {
|
||||
static final GlobalKey<ScaffoldState> scaffoldKey =
|
||||
GlobalKey<ScaffoldState>();
|
||||
|
||||
PersistentBottomSheetController<void> _bottomSheet;
|
||||
_MaterialListType _itemType = _MaterialListType.threeLine;
|
||||
bool _dense = false;
|
||||
bool _showAvatars = true;
|
||||
bool _showIcons = false;
|
||||
bool _showDividers = false;
|
||||
bool _reverseSort = false;
|
||||
List<String> items = <String>[
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
'F',
|
||||
'G',
|
||||
'H',
|
||||
'I',
|
||||
'J',
|
||||
'K',
|
||||
'L',
|
||||
'M',
|
||||
'N',
|
||||
];
|
||||
|
||||
void changeItemType(_MaterialListType type) {
|
||||
setState(() {
|
||||
_itemType = type;
|
||||
});
|
||||
_bottomSheet?.setState(() {});
|
||||
}
|
||||
|
||||
void _showConfigurationSheet() {
|
||||
final PersistentBottomSheetController<void> bottomSheet = scaffoldKey
|
||||
.currentState
|
||||
.showBottomSheet<void>((BuildContext bottomSheetContext) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(top: BorderSide(color: Colors.black26)),
|
||||
),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
primary: false,
|
||||
children: <Widget>[
|
||||
MergeSemantics(
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: const Text('One-line'),
|
||||
trailing: Radio<_MaterialListType>(
|
||||
value: _showAvatars
|
||||
? _MaterialListType.oneLineWithAvatar
|
||||
: _MaterialListType.oneLine,
|
||||
groupValue: _itemType,
|
||||
onChanged: changeItemType,
|
||||
)),
|
||||
),
|
||||
MergeSemantics(
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: const Text('Two-line'),
|
||||
trailing: Radio<_MaterialListType>(
|
||||
value: _MaterialListType.twoLine,
|
||||
groupValue: _itemType,
|
||||
onChanged: changeItemType,
|
||||
)),
|
||||
),
|
||||
MergeSemantics(
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: const Text('Three-line'),
|
||||
trailing: Radio<_MaterialListType>(
|
||||
value: _MaterialListType.threeLine,
|
||||
groupValue: _itemType,
|
||||
onChanged: changeItemType,
|
||||
),
|
||||
),
|
||||
),
|
||||
MergeSemantics(
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: const Text('Show avatar'),
|
||||
trailing: Checkbox(
|
||||
value: _showAvatars,
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
_showAvatars = value;
|
||||
});
|
||||
_bottomSheet?.setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
MergeSemantics(
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: const Text('Show icon'),
|
||||
trailing: Checkbox(
|
||||
value: _showIcons,
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
_showIcons = value;
|
||||
});
|
||||
_bottomSheet?.setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
MergeSemantics(
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: const Text('Show dividers'),
|
||||
trailing: Checkbox(
|
||||
value: _showDividers,
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
_showDividers = value;
|
||||
});
|
||||
_bottomSheet?.setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
MergeSemantics(
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: const Text('Dense layout'),
|
||||
trailing: Checkbox(
|
||||
value: _dense,
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
_dense = value;
|
||||
});
|
||||
_bottomSheet?.setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
setState(() {
|
||||
_bottomSheet = bottomSheet;
|
||||
});
|
||||
|
||||
_bottomSheet.closed.whenComplete(() {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_bottomSheet = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Widget buildListTile(BuildContext context, String item) {
|
||||
Widget secondary;
|
||||
if (_itemType == _MaterialListType.twoLine) {
|
||||
secondary = const Text('Additional item information.');
|
||||
} else if (_itemType == _MaterialListType.threeLine) {
|
||||
secondary = const Text(
|
||||
'Even more additional list item information appears on line three.',
|
||||
);
|
||||
}
|
||||
return MergeSemantics(
|
||||
child: ListTile(
|
||||
isThreeLine: _itemType == _MaterialListType.threeLine,
|
||||
dense: _dense,
|
||||
leading: _showAvatars
|
||||
? ExcludeSemantics(child: CircleAvatar(child: Text(item)))
|
||||
: null,
|
||||
title: Text('This item represents $item.'),
|
||||
subtitle: secondary,
|
||||
trailing: _showIcons
|
||||
? Icon(Icons.info, color: Theme.of(context).disabledColor)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String layoutText = _dense ? ' \u2013 Dense' : '';
|
||||
String itemTypeText;
|
||||
switch (_itemType) {
|
||||
case _MaterialListType.oneLine:
|
||||
case _MaterialListType.oneLineWithAvatar:
|
||||
itemTypeText = 'Single-line';
|
||||
break;
|
||||
case _MaterialListType.twoLine:
|
||||
itemTypeText = 'Two-line';
|
||||
break;
|
||||
case _MaterialListType.threeLine:
|
||||
itemTypeText = 'Three-line';
|
||||
break;
|
||||
}
|
||||
|
||||
Iterable<Widget> listTiles =
|
||||
items.map<Widget>((String item) => buildListTile(context, item));
|
||||
if (_showDividers)
|
||||
listTiles = ListTile.divideTiles(context: context, tiles: listTiles);
|
||||
|
||||
return Scaffold(
|
||||
key: scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: Text('Scrolling list\n$itemTypeText$layoutText'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(ListDemo.routeName),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.sort_by_alpha),
|
||||
tooltip: 'Sort',
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_reverseSort = !_reverseSort;
|
||||
items.sort((String a, String b) =>
|
||||
_reverseSort ? b.compareTo(a) : a.compareTo(b));
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Theme.of(context).platform == TargetPlatform.iOS
|
||||
? Icons.more_horiz
|
||||
: Icons.more_vert,
|
||||
),
|
||||
tooltip: 'Show menu',
|
||||
onPressed: _bottomSheet == null ? _showConfigurationSheet : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Scrollbar(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.symmetric(vertical: _dense ? 4.0 : 8.0),
|
||||
children: listTiles.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
39
web/gallery/lib/demo/material/material.dart
Normal file
39
web/gallery/lib/demo/material/material.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
export 'backdrop_demo.dart';
|
||||
export 'bottom_app_bar_demo.dart';
|
||||
export 'bottom_navigation_demo.dart';
|
||||
export 'material_button_demo.dart';
|
||||
export 'cards_demo.dart';
|
||||
export 'chip_demo.dart';
|
||||
export 'data_table_demo.dart';
|
||||
export 'date_and_time_picker_demo.dart';
|
||||
export 'dialog_demo.dart';
|
||||
export 'drawer_demo.dart';
|
||||
export 'editable_text_demo.dart';
|
||||
export 'elevation_demo.dart';
|
||||
export 'expansion_panels_demo.dart';
|
||||
export 'grid_list_demo.dart';
|
||||
export 'icons_demo.dart';
|
||||
export 'leave_behind_demo.dart';
|
||||
export 'list_demo.dart';
|
||||
export 'menu_demo.dart';
|
||||
export 'modal_bottom_sheet_demo.dart';
|
||||
export 'overscroll_demo.dart';
|
||||
export 'page_selector_demo.dart';
|
||||
export 'persistent_bottom_sheet_demo.dart';
|
||||
export 'progress_indicator_demo.dart';
|
||||
export 'reorderable_list_demo.dart';
|
||||
export 'scrollable_tabs_demo.dart';
|
||||
export 'search_demo.dart';
|
||||
export 'selection_controls_demo.dart';
|
||||
export 'slider_demo.dart';
|
||||
export 'snack_bar_demo.dart';
|
||||
export 'tabs_demo.dart';
|
||||
export 'tabs_fab_demo.dart';
|
||||
export 'text_demo.dart';
|
||||
export 'text_form_field_demo.dart';
|
||||
export 'tooltip_demo.dart';
|
||||
export 'two_level_list_demo.dart';
|
||||
103
web/gallery/lib/demo/material/material_button_demo.dart
Normal file
103
web/gallery/lib/demo/material/material_button_demo.dart
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class ButtonsDemo extends StatelessWidget {
|
||||
static const String routeName = '/material/buttons';
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
IconData _backIcon() {
|
||||
switch (Theme.of(context).platform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
return Icons.arrow_back;
|
||||
case TargetPlatform.iOS:
|
||||
return Icons.arrow_back_ios;
|
||||
}
|
||||
assert(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: Icon(_backIcon()),
|
||||
alignment: Alignment.centerLeft,
|
||||
tooltip: 'Back',
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
title: const Text('Material buttons'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(ButtonsDemo.routeName)
|
||||
],
|
||||
),
|
||||
body: Center(
|
||||
child: _buildButtons(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildButtons() {
|
||||
return Column(
|
||||
children: [
|
||||
pad(MaterialButton(
|
||||
onPressed: () {
|
||||
print('MaterialButton pressed');
|
||||
},
|
||||
elevation: 3.0,
|
||||
child: Text('MaterialButton'),
|
||||
)),
|
||||
pad(FlatButton(
|
||||
onPressed: () {
|
||||
print('FlatButton pressed');
|
||||
},
|
||||
child: Text('FlatButton'),
|
||||
)),
|
||||
pad(RaisedButton(
|
||||
onPressed: () {},
|
||||
elevation: 0.0,
|
||||
child: Text('RaisedButton 0.0'),
|
||||
)),
|
||||
pad(RaisedButton(
|
||||
onPressed: () {},
|
||||
elevation: 1.0,
|
||||
child: Text('RaisedButton 1.0'),
|
||||
)),
|
||||
pad(RaisedButton(
|
||||
onPressed: () {},
|
||||
elevation: 2.0,
|
||||
child: Text('RaisedButton 2.0'),
|
||||
)),
|
||||
pad(RaisedButton(
|
||||
onPressed: () {},
|
||||
elevation: 3.0,
|
||||
child: Text('RaisedButton 3.0'),
|
||||
)),
|
||||
pad(RaisedButton(
|
||||
onPressed: () {},
|
||||
elevation: 4.0,
|
||||
child: Text('RaisedButton 4.0'),
|
||||
)),
|
||||
pad(RaisedButton(
|
||||
onPressed: () {},
|
||||
elevation: 8.0,
|
||||
child: Text('RaisedButton 8.0'),
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Padding pad(Widget widget) => Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: widget,
|
||||
);
|
||||
181
web/gallery/lib/demo/material/menu_demo.dart
Normal file
181
web/gallery/lib/demo/material/menu_demo.dart
Normal file
@@ -0,0 +1,181 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class MenuDemo extends StatefulWidget {
|
||||
const MenuDemo({Key key}) : super(key: key);
|
||||
|
||||
static const String routeName = '/material/menu';
|
||||
|
||||
@override
|
||||
MenuDemoState createState() => MenuDemoState();
|
||||
}
|
||||
|
||||
class MenuDemoState extends State<MenuDemo> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
final String _simpleValue1 = 'Menu item value one';
|
||||
final String _simpleValue2 = 'Menu item value two';
|
||||
final String _simpleValue3 = 'Menu item value three';
|
||||
String _simpleValue;
|
||||
|
||||
final String _checkedValue1 = 'One';
|
||||
final String _checkedValue2 = 'Two';
|
||||
final String _checkedValue3 = 'Free';
|
||||
final String _checkedValue4 = 'Four';
|
||||
List<String> _checkedValues;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_simpleValue = _simpleValue2;
|
||||
_checkedValues = <String>[_checkedValue3];
|
||||
}
|
||||
|
||||
void showInSnackBar(String value) {
|
||||
_scaffoldKey.currentState.showSnackBar(SnackBar(content: Text(value)));
|
||||
}
|
||||
|
||||
void showMenuSelection(String value) {
|
||||
if (<String>[_simpleValue1, _simpleValue2, _simpleValue3].contains(value))
|
||||
_simpleValue = value;
|
||||
showInSnackBar('You selected: $value');
|
||||
}
|
||||
|
||||
void showCheckedMenuSelections(String value) {
|
||||
if (_checkedValues.contains(value))
|
||||
_checkedValues.remove(value);
|
||||
else
|
||||
_checkedValues.add(value);
|
||||
|
||||
showInSnackBar('Checked $_checkedValues');
|
||||
}
|
||||
|
||||
bool isChecked(String value) => _checkedValues.contains(value);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: const Text('Menus'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(MenuDemo.routeName),
|
||||
PopupMenuButton<String>(
|
||||
onSelected: showMenuSelection,
|
||||
itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
|
||||
const PopupMenuItem<String>(
|
||||
value: 'Toolbar menu', child: Text('Toolbar menu')),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'Right here', child: Text('Right here')),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'Hooray!', child: Text('Hooray!')),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView(padding: kMaterialListPadding, children: <Widget>[
|
||||
// Pressing the PopupMenuButton on the right of this item shows
|
||||
// a simple menu with one disabled item. Typically the contents
|
||||
// of this "contextual menu" would reflect the app's state.
|
||||
ListTile(
|
||||
title: const Text('An item with a context menu button'),
|
||||
trailing: PopupMenuButton<String>(
|
||||
padding: EdgeInsets.zero,
|
||||
onSelected: showMenuSelection,
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuItem<String>>[
|
||||
PopupMenuItem<String>(
|
||||
value: _simpleValue1,
|
||||
child: const Text('Context menu item one')),
|
||||
const PopupMenuItem<String>(
|
||||
enabled: false,
|
||||
child: Text('A disabled menu item')),
|
||||
PopupMenuItem<String>(
|
||||
value: _simpleValue3,
|
||||
child: const Text('Context menu item three')),
|
||||
])),
|
||||
// Pressing the PopupMenuButton on the right of this item shows
|
||||
// a menu whose items have text labels and icons and a divider
|
||||
// That separates the first three items from the last one.
|
||||
ListTile(
|
||||
title: const Text('An item with a sectioned menu'),
|
||||
trailing: PopupMenuButton<String>(
|
||||
padding: EdgeInsets.zero,
|
||||
onSelected: showMenuSelection,
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuEntry<String>>[
|
||||
const PopupMenuItem<String>(
|
||||
value: 'Preview',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.visibility),
|
||||
title: Text('Preview'))),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'Share',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.person_add),
|
||||
title: Text('Share'))),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'Get Link',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.link),
|
||||
title: Text('Get link'))),
|
||||
const PopupMenuDivider(),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'Remove',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.delete),
|
||||
title: Text('Remove')))
|
||||
])),
|
||||
// This entire list item is a PopupMenuButton. Tapping anywhere shows
|
||||
// a menu whose current value is highlighted and aligned over the
|
||||
// list item's center line.
|
||||
PopupMenuButton<String>(
|
||||
padding: EdgeInsets.zero,
|
||||
initialValue: _simpleValue,
|
||||
onSelected: showMenuSelection,
|
||||
child: ListTile(
|
||||
title: const Text('An item with a simple menu'),
|
||||
subtitle: Text(_simpleValue)),
|
||||
itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
|
||||
PopupMenuItem<String>(
|
||||
value: _simpleValue1, child: Text(_simpleValue1)),
|
||||
PopupMenuItem<String>(
|
||||
value: _simpleValue2, child: Text(_simpleValue2)),
|
||||
PopupMenuItem<String>(
|
||||
value: _simpleValue3, child: Text(_simpleValue3))
|
||||
]),
|
||||
// Pressing the PopupMenuButton on the right of this item shows a menu
|
||||
// whose items have checked icons that reflect this app's state.
|
||||
ListTile(
|
||||
title: const Text('An item with a checklist menu'),
|
||||
trailing: PopupMenuButton<String>(
|
||||
padding: EdgeInsets.zero,
|
||||
onSelected: showCheckedMenuSelections,
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuItem<String>>[
|
||||
CheckedPopupMenuItem<String>(
|
||||
value: _checkedValue1,
|
||||
checked: isChecked(_checkedValue1),
|
||||
child: Text(_checkedValue1)),
|
||||
CheckedPopupMenuItem<String>(
|
||||
value: _checkedValue2,
|
||||
enabled: false,
|
||||
checked: isChecked(_checkedValue2),
|
||||
child: Text(_checkedValue2)),
|
||||
CheckedPopupMenuItem<String>(
|
||||
value: _checkedValue3,
|
||||
checked: isChecked(_checkedValue3),
|
||||
child: Text(_checkedValue3)),
|
||||
CheckedPopupMenuItem<String>(
|
||||
value: _checkedValue4,
|
||||
checked: isChecked(_checkedValue4),
|
||||
child: Text(_checkedValue4))
|
||||
]))
|
||||
]));
|
||||
}
|
||||
}
|
||||
38
web/gallery/lib/demo/material/modal_bottom_sheet_demo.dart
Normal file
38
web/gallery/lib/demo/material/modal_bottom_sheet_demo.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class ModalBottomSheetDemo extends StatelessWidget {
|
||||
static const String routeName = '/material/modal-bottom-sheet';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Modal bottom sheet'),
|
||||
actions: <Widget>[MaterialDemoDocumentationButton(routeName)],
|
||||
),
|
||||
body: Center(
|
||||
child: RaisedButton(
|
||||
child: const Text('SHOW BOTTOM SHEET'),
|
||||
onPressed: () {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Container(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Text(
|
||||
'This is the modal bottom sheet. Tap anywhere to dismiss.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontSize: 24.0))));
|
||||
});
|
||||
})));
|
||||
}
|
||||
}
|
||||
92
web/gallery/lib/demo/material/overscroll_demo.dart
Normal file
92
web/gallery/lib/demo/material/overscroll_demo.dart
Normal file
@@ -0,0 +1,92 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
enum IndicatorType { overscroll, refresh }
|
||||
|
||||
class OverscrollDemo extends StatefulWidget {
|
||||
const OverscrollDemo({Key key}) : super(key: key);
|
||||
|
||||
static const String routeName = '/material/overscroll';
|
||||
|
||||
@override
|
||||
OverscrollDemoState createState() => OverscrollDemoState();
|
||||
}
|
||||
|
||||
class OverscrollDemoState extends State<OverscrollDemo> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
|
||||
GlobalKey<RefreshIndicatorState>();
|
||||
static final List<String> _items = <String>[
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
'F',
|
||||
'G',
|
||||
'H',
|
||||
'I',
|
||||
'J',
|
||||
'K',
|
||||
'L',
|
||||
'M',
|
||||
'N'
|
||||
];
|
||||
|
||||
Future<void> _handleRefresh() {
|
||||
final Completer<void> completer = Completer<void>();
|
||||
Timer(const Duration(seconds: 3), () {
|
||||
completer.complete();
|
||||
});
|
||||
return completer.future.then<void>((_) {
|
||||
_scaffoldKey.currentState?.showSnackBar(SnackBar(
|
||||
content: const Text('Refresh complete'),
|
||||
action: SnackBarAction(
|
||||
label: 'RETRY',
|
||||
onPressed: () {
|
||||
_refreshIndicatorKey.currentState.show();
|
||||
})));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(title: const Text('Pull to refresh'), actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(OverscrollDemo.routeName),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
tooltip: 'Refresh',
|
||||
onPressed: () {
|
||||
_refreshIndicatorKey.currentState.show();
|
||||
}),
|
||||
]),
|
||||
body: RefreshIndicator(
|
||||
key: _refreshIndicatorKey,
|
||||
onRefresh: _handleRefresh,
|
||||
child: ListView.builder(
|
||||
padding: kMaterialListPadding,
|
||||
itemCount: _items.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final String item = _items[index];
|
||||
return ListTile(
|
||||
isThreeLine: true,
|
||||
leading: CircleAvatar(child: Text(item)),
|
||||
title: Text('This item represents $item.'),
|
||||
subtitle: const Text(
|
||||
'Even more additional list item information appears on line three.'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
98
web/gallery/lib/demo/material/page_selector_demo.dart
Normal file
98
web/gallery/lib/demo/material/page_selector_demo.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class _PageSelector extends StatelessWidget {
|
||||
const _PageSelector({this.icons});
|
||||
|
||||
final List<Icon> icons;
|
||||
|
||||
void _handleArrowButtonPress(BuildContext context, int delta) {
|
||||
final TabController controller = DefaultTabController.of(context);
|
||||
if (!controller.indexIsChanging)
|
||||
controller
|
||||
.animateTo((controller.index + delta).clamp(0, icons.length - 1));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TabController controller = DefaultTabController.of(context);
|
||||
final Color color = Theme.of(context).accentColor;
|
||||
return SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 16.0),
|
||||
child: Row(children: <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
color: color,
|
||||
onPressed: () {
|
||||
_handleArrowButtonPress(context, -1);
|
||||
},
|
||||
tooltip: 'Page back'),
|
||||
TabPageSelector(controller: controller),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
color: color,
|
||||
onPressed: () {
|
||||
_handleArrowButtonPress(context, 1);
|
||||
},
|
||||
tooltip: 'Page forward')
|
||||
], mainAxisAlignment: MainAxisAlignment.spaceBetween)),
|
||||
Expanded(
|
||||
child: IconTheme(
|
||||
data: IconThemeData(
|
||||
size: 128.0,
|
||||
color: color,
|
||||
),
|
||||
child: TabBarView(
|
||||
children: icons.map<Widget>((Icon icon) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Card(
|
||||
child: Center(
|
||||
child: icon,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList()),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PageSelectorDemo extends StatelessWidget {
|
||||
static const String routeName = '/material/page-selector';
|
||||
static final List<Icon> icons = <Icon>[
|
||||
const Icon(Icons.event, semanticLabel: 'Event'),
|
||||
const Icon(Icons.home, semanticLabel: 'Home'),
|
||||
const Icon(Icons.android, semanticLabel: 'Android'),
|
||||
const Icon(Icons.alarm, semanticLabel: 'Alarm'),
|
||||
const Icon(Icons.face, semanticLabel: 'Face'),
|
||||
const Icon(Icons.language, semanticLabel: 'Language'),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Page selector'),
|
||||
actions: <Widget>[MaterialDemoDocumentationButton(routeName)],
|
||||
),
|
||||
body: DefaultTabController(
|
||||
length: icons.length,
|
||||
child: _PageSelector(icons: icons),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
103
web/gallery/lib/demo/material/persistent_bottom_sheet_demo.dart
Normal file
103
web/gallery/lib/demo/material/persistent_bottom_sheet_demo.dart
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class PersistentBottomSheetDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/persistent-bottom-sheet';
|
||||
|
||||
@override
|
||||
_PersistentBottomSheetDemoState createState() =>
|
||||
_PersistentBottomSheetDemoState();
|
||||
}
|
||||
|
||||
class _PersistentBottomSheetDemoState extends State<PersistentBottomSheetDemo> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
VoidCallback _showBottomSheetCallback;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_showBottomSheetCallback = _showBottomSheet;
|
||||
}
|
||||
|
||||
void _showBottomSheet() {
|
||||
setState(() {
|
||||
// disable the button
|
||||
_showBottomSheetCallback = null;
|
||||
});
|
||||
_scaffoldKey.currentState
|
||||
.showBottomSheet<Null>((BuildContext context) {
|
||||
final ThemeData themeData = Theme.of(context);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border:
|
||||
Border(top: BorderSide(color: themeData.disabledColor))),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Text(
|
||||
'This is a Material persistent bottom sheet. Drag downwards to dismiss it.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: themeData.accentColor, fontSize: 24.0),
|
||||
),
|
||||
),
|
||||
);
|
||||
})
|
||||
.closed
|
||||
.whenComplete(() {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
// re-enable the button
|
||||
_showBottomSheetCallback = _showBottomSheet;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _showMessage() {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
content: const Text('You tapped the floating action button.'),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('OK'))
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: const Text('Persistent bottom sheet'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(
|
||||
PersistentBottomSheetDemo.routeName),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _showMessage,
|
||||
backgroundColor: Colors.redAccent,
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
semanticLabel: 'Add',
|
||||
),
|
||||
),
|
||||
body: Center(
|
||||
child: RaisedButton(
|
||||
onPressed: _showBottomSheetCallback,
|
||||
child: const Text('SHOW BOTTOM SHEET'))));
|
||||
}
|
||||
}
|
||||
132
web/gallery/lib/demo/material/progress_indicator_demo.dart
Normal file
132
web/gallery/lib/demo/material/progress_indicator_demo.dart
Normal file
@@ -0,0 +1,132 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class ProgressIndicatorDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/progress-indicator';
|
||||
|
||||
@override
|
||||
_ProgressIndicatorDemoState createState() => _ProgressIndicatorDemoState();
|
||||
}
|
||||
|
||||
class _ProgressIndicatorDemoState extends State<ProgressIndicatorDemo>
|
||||
with SingleTickerProviderStateMixin {
|
||||
AnimationController _controller;
|
||||
Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
vsync: this,
|
||||
animationBehavior: AnimationBehavior.preserve,
|
||||
)..forward();
|
||||
|
||||
_animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.0, 0.9, curve: Curves.fastOutSlowIn),
|
||||
reverseCurve: Curves.fastOutSlowIn)
|
||||
..addStatusListener((AnimationStatus status) {
|
||||
if (status == AnimationStatus.dismissed)
|
||||
_controller.forward();
|
||||
else if (status == AnimationStatus.completed) _controller.reverse();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.stop();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleTap() {
|
||||
setState(() {
|
||||
// valueAnimation.isAnimating is part of our build state
|
||||
if (_controller.isAnimating) {
|
||||
_controller.stop();
|
||||
} else {
|
||||
switch (_controller.status) {
|
||||
case AnimationStatus.dismissed:
|
||||
case AnimationStatus.forward:
|
||||
_controller.forward();
|
||||
break;
|
||||
case AnimationStatus.reverse:
|
||||
case AnimationStatus.completed:
|
||||
_controller.reverse();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildIndicators(BuildContext context, Widget child) {
|
||||
final List<Widget> indicators = <Widget>[
|
||||
const SizedBox(width: 200.0, child: LinearProgressIndicator()),
|
||||
const LinearProgressIndicator(),
|
||||
const LinearProgressIndicator(),
|
||||
LinearProgressIndicator(value: _animation.value),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
const CircularProgressIndicator(),
|
||||
SizedBox(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: CircularProgressIndicator(value: _animation.value)),
|
||||
SizedBox(
|
||||
width: 100.0,
|
||||
height: 20.0,
|
||||
child: Text('${(_animation.value * 100.0).toStringAsFixed(1)}%',
|
||||
textAlign: TextAlign.right),
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
return Column(
|
||||
children: indicators
|
||||
.map<Widget>((Widget c) => Container(
|
||||
child: c,
|
||||
margin:
|
||||
const EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0)))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Progress indicators'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(ProgressIndicatorDemo.routeName)
|
||||
],
|
||||
),
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
child: DefaultTextStyle(
|
||||
style: Theme.of(context).textTheme.title,
|
||||
child: GestureDetector(
|
||||
onTap: _handleTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12.0, horizontal: 8.0),
|
||||
child: AnimatedBuilder(
|
||||
animation: _animation, builder: _buildIndicators),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
219
web/gallery/lib/demo/material/reorderable_list_demo.dart
Normal file
219
web/gallery/lib/demo/material/reorderable_list_demo.dart
Normal file
@@ -0,0 +1,219 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/foundation.dart';
|
||||
import 'package:flutter_web/material.dart';
|
||||
import 'package:flutter_web/rendering.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
enum _ReorderableListType {
|
||||
/// A list tile that contains a [CircleAvatar].
|
||||
horizontalAvatar,
|
||||
|
||||
/// A list tile that contains a [CircleAvatar].
|
||||
verticalAvatar,
|
||||
|
||||
/// A list tile that contains three lines of text and a checkbox.
|
||||
threeLine,
|
||||
}
|
||||
|
||||
class ReorderableListDemo extends StatefulWidget {
|
||||
const ReorderableListDemo({Key key}) : super(key: key);
|
||||
|
||||
static const String routeName = '/material/reorderable-list';
|
||||
|
||||
@override
|
||||
_ListDemoState createState() => _ListDemoState();
|
||||
}
|
||||
|
||||
class _ListItem {
|
||||
_ListItem(this.value, this.checkState);
|
||||
|
||||
final String value;
|
||||
|
||||
bool checkState;
|
||||
}
|
||||
|
||||
class _ListDemoState extends State<ReorderableListDemo> {
|
||||
static final GlobalKey<ScaffoldState> scaffoldKey =
|
||||
GlobalKey<ScaffoldState>();
|
||||
|
||||
PersistentBottomSheetController<void> _bottomSheet;
|
||||
_ReorderableListType _itemType = _ReorderableListType.threeLine;
|
||||
bool _reverseSort = false;
|
||||
final List<_ListItem> _items = <String>[
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
'F',
|
||||
'G',
|
||||
'H',
|
||||
'I',
|
||||
'J',
|
||||
'K',
|
||||
'L',
|
||||
'M',
|
||||
'N',
|
||||
].map<_ListItem>((String item) => _ListItem(item, false)).toList();
|
||||
|
||||
void changeItemType(_ReorderableListType type) {
|
||||
setState(() {
|
||||
_itemType = type;
|
||||
});
|
||||
// Rebuild the bottom sheet to reflect the selected list view.
|
||||
_bottomSheet?.setState(() {});
|
||||
// Close the bottom sheet to give the user a clear view of the list.
|
||||
_bottomSheet?.close();
|
||||
}
|
||||
|
||||
void _showConfigurationSheet() {
|
||||
setState(() {
|
||||
_bottomSheet = scaffoldKey.currentState
|
||||
.showBottomSheet<void>((BuildContext bottomSheetContext) {
|
||||
return DecoratedBox(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(top: BorderSide(color: Colors.black26)),
|
||||
),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
primary: false,
|
||||
children: <Widget>[
|
||||
RadioListTile<_ReorderableListType>(
|
||||
dense: true,
|
||||
title: const Text('Horizontal Avatars'),
|
||||
value: _ReorderableListType.horizontalAvatar,
|
||||
groupValue: _itemType,
|
||||
onChanged: changeItemType,
|
||||
),
|
||||
RadioListTile<_ReorderableListType>(
|
||||
dense: true,
|
||||
title: const Text('Vertical Avatars'),
|
||||
value: _ReorderableListType.verticalAvatar,
|
||||
groupValue: _itemType,
|
||||
onChanged: changeItemType,
|
||||
),
|
||||
RadioListTile<_ReorderableListType>(
|
||||
dense: true,
|
||||
title: const Text('Three-line'),
|
||||
value: _ReorderableListType.threeLine,
|
||||
groupValue: _itemType,
|
||||
onChanged: changeItemType,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// Garbage collect the bottom sheet when it closes.
|
||||
_bottomSheet.closed.whenComplete(() {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_bottomSheet = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Widget buildListTile(_ListItem item) {
|
||||
const Widget secondary = Text(
|
||||
'Even more additional list item information appears on line three.',
|
||||
);
|
||||
Widget listTile;
|
||||
switch (_itemType) {
|
||||
case _ReorderableListType.threeLine:
|
||||
listTile = CheckboxListTile(
|
||||
key: Key(item.value),
|
||||
isThreeLine: true,
|
||||
value: item.checkState ?? false,
|
||||
onChanged: (bool newValue) {
|
||||
setState(() {
|
||||
item.checkState = newValue;
|
||||
});
|
||||
},
|
||||
title: Text('This item represents ${item.value}.'),
|
||||
subtitle: secondary,
|
||||
secondary: const Icon(Icons.drag_handle),
|
||||
);
|
||||
break;
|
||||
case _ReorderableListType.horizontalAvatar:
|
||||
case _ReorderableListType.verticalAvatar:
|
||||
listTile = Container(
|
||||
key: Key(item.value),
|
||||
height: 100.0,
|
||||
width: 100.0,
|
||||
child: CircleAvatar(
|
||||
child: Text(item.value),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return listTile;
|
||||
}
|
||||
|
||||
void _onReorder(int oldIndex, int newIndex) {
|
||||
setState(() {
|
||||
if (newIndex > oldIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final _ListItem item = _items.removeAt(oldIndex);
|
||||
_items.insert(newIndex, item);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
key: scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: const Text('Reorderable list'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(ReorderableListDemo.routeName),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.sort_by_alpha),
|
||||
tooltip: 'Sort',
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_reverseSort = !_reverseSort;
|
||||
_items.sort((_ListItem a, _ListItem b) => _reverseSort
|
||||
? b.value.compareTo(a.value)
|
||||
: a.value.compareTo(b.value));
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Theme.of(context).platform == TargetPlatform.iOS
|
||||
? Icons.more_horiz
|
||||
: Icons.more_vert,
|
||||
),
|
||||
tooltip: 'Show menu',
|
||||
onPressed: _bottomSheet == null ? _showConfigurationSheet : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Scrollbar(
|
||||
child: ReorderableListView(
|
||||
header: _itemType != _ReorderableListType.threeLine
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text('Header of the list',
|
||||
style: Theme.of(context).textTheme.headline))
|
||||
: null,
|
||||
onReorder: _onReorder,
|
||||
scrollDirection: _itemType == _ReorderableListType.horizontalAvatar
|
||||
? Axis.horizontal
|
||||
: Axis.vertical,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
children: _items.map<Widget>(buildListTile).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
195
web/gallery/lib/demo/material/scrollable_tabs_demo.dart
Normal file
195
web/gallery/lib/demo/material/scrollable_tabs_demo.dart
Normal file
@@ -0,0 +1,195 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
enum TabsDemoStyle { iconsAndText, iconsOnly, textOnly }
|
||||
|
||||
class _Page {
|
||||
const _Page({this.icon, this.text});
|
||||
final IconData icon;
|
||||
final String text;
|
||||
}
|
||||
|
||||
const List<_Page> _allPages = <_Page>[
|
||||
_Page(icon: Icons.grade, text: 'TRIUMPH'),
|
||||
_Page(icon: Icons.playlist_add, text: 'NOTE'),
|
||||
_Page(icon: Icons.check_circle, text: 'SUCCESS'),
|
||||
_Page(icon: Icons.question_answer, text: 'OVERSTATE'),
|
||||
_Page(icon: Icons.sentiment_very_satisfied, text: 'SATISFACTION'),
|
||||
_Page(icon: Icons.camera, text: 'APERTURE'),
|
||||
_Page(icon: Icons.assignment_late, text: 'WE MUST'),
|
||||
_Page(icon: Icons.assignment_turned_in, text: 'WE CAN'),
|
||||
_Page(icon: Icons.group, text: 'ALL'),
|
||||
_Page(icon: Icons.block, text: 'EXCEPT'),
|
||||
_Page(icon: Icons.sentiment_very_dissatisfied, text: 'CRYING'),
|
||||
_Page(icon: Icons.error, text: 'MISTAKE'),
|
||||
_Page(icon: Icons.loop, text: 'TRYING'),
|
||||
_Page(icon: Icons.cake, text: 'CAKE'),
|
||||
];
|
||||
|
||||
class ScrollableTabsDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/scrollable-tabs';
|
||||
|
||||
@override
|
||||
ScrollableTabsDemoState createState() => ScrollableTabsDemoState();
|
||||
}
|
||||
|
||||
class ScrollableTabsDemoState extends State<ScrollableTabsDemo>
|
||||
with SingleTickerProviderStateMixin {
|
||||
TabController _controller;
|
||||
TabsDemoStyle _demoStyle = TabsDemoStyle.iconsAndText;
|
||||
bool _customIndicator = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TabController(vsync: this, length: _allPages.length);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void changeDemoStyle(TabsDemoStyle style) {
|
||||
setState(() {
|
||||
_demoStyle = style;
|
||||
});
|
||||
}
|
||||
|
||||
Decoration getIndicator() {
|
||||
if (!_customIndicator) return const UnderlineTabIndicator();
|
||||
|
||||
switch (_demoStyle) {
|
||||
case TabsDemoStyle.iconsAndText:
|
||||
return ShapeDecoration(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(4.0)),
|
||||
side: BorderSide(
|
||||
color: Colors.white24,
|
||||
width: 2.0,
|
||||
),
|
||||
) +
|
||||
const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(4.0)),
|
||||
side: BorderSide(
|
||||
color: Colors.transparent,
|
||||
width: 4.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
case TabsDemoStyle.iconsOnly:
|
||||
return ShapeDecoration(
|
||||
shape: const CircleBorder(
|
||||
side: BorderSide(
|
||||
color: Colors.white24,
|
||||
width: 4.0,
|
||||
),
|
||||
) +
|
||||
const CircleBorder(
|
||||
side: BorderSide(
|
||||
color: Colors.transparent,
|
||||
width: 4.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
case TabsDemoStyle.textOnly:
|
||||
return ShapeDecoration(
|
||||
shape: const StadiumBorder(
|
||||
side: BorderSide(
|
||||
color: Colors.white24,
|
||||
width: 2.0,
|
||||
),
|
||||
) +
|
||||
const StadiumBorder(
|
||||
side: BorderSide(
|
||||
color: Colors.transparent,
|
||||
width: 4.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color iconColor = Theme.of(context).accentColor;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Scrollable tabs'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(ScrollableTabsDemo.routeName),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.sentiment_very_satisfied),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_customIndicator = !_customIndicator;
|
||||
});
|
||||
},
|
||||
),
|
||||
PopupMenuButton<TabsDemoStyle>(
|
||||
onSelected: changeDemoStyle,
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuItem<TabsDemoStyle>>[
|
||||
const PopupMenuItem<TabsDemoStyle>(
|
||||
value: TabsDemoStyle.iconsAndText,
|
||||
child: Text('Icons and text')),
|
||||
const PopupMenuItem<TabsDemoStyle>(
|
||||
value: TabsDemoStyle.iconsOnly,
|
||||
child: Text('Icons only')),
|
||||
const PopupMenuItem<TabsDemoStyle>(
|
||||
value: TabsDemoStyle.textOnly, child: Text('Text only')),
|
||||
],
|
||||
),
|
||||
],
|
||||
bottom: TabBar(
|
||||
controller: _controller,
|
||||
isScrollable: true,
|
||||
indicator: getIndicator(),
|
||||
tabs: _allPages.map<Tab>((_Page page) {
|
||||
assert(_demoStyle != null);
|
||||
switch (_demoStyle) {
|
||||
case TabsDemoStyle.iconsAndText:
|
||||
return Tab(text: page.text, icon: Icon(page.icon));
|
||||
case TabsDemoStyle.iconsOnly:
|
||||
return Tab(icon: Icon(page.icon));
|
||||
case TabsDemoStyle.textOnly:
|
||||
return Tab(text: page.text);
|
||||
}
|
||||
return null;
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
controller: _controller,
|
||||
children: _allPages.map<Widget>((_Page page) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Container(
|
||||
key: ObjectKey(page.icon),
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Card(
|
||||
child: Center(
|
||||
child: Icon(
|
||||
page.icon,
|
||||
color: iconColor,
|
||||
size: 128.0,
|
||||
semanticLabel: 'Placeholder for ${page.text} tab',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList()),
|
||||
);
|
||||
}
|
||||
}
|
||||
295
web/gallery/lib/demo/material/search_demo.dart
Normal file
295
web/gallery/lib/demo/material/search_demo.dart
Normal file
@@ -0,0 +1,295 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class SearchDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/search';
|
||||
|
||||
@override
|
||||
_SearchDemoState createState() => _SearchDemoState();
|
||||
}
|
||||
|
||||
class _SearchDemoState extends State<SearchDemo> {
|
||||
final _SearchDemoSearchDelegate _delegate = _SearchDemoSearchDelegate();
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
int _lastIntegerSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
tooltip: 'Navigation menu',
|
||||
icon: AnimatedIcon(
|
||||
icon: AnimatedIcons.menu_arrow,
|
||||
color: Colors.white,
|
||||
progress: _delegate.transitionAnimation,
|
||||
),
|
||||
onPressed: () {
|
||||
_scaffoldKey.currentState.openDrawer();
|
||||
},
|
||||
),
|
||||
title: const Text('Numbers'),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
tooltip: 'Search',
|
||||
icon: const Icon(Icons.search),
|
||||
onPressed: () async {
|
||||
final int selected = await showSearch<int>(
|
||||
context: context,
|
||||
delegate: _delegate,
|
||||
);
|
||||
if (selected != null && selected != _lastIntegerSelected) {
|
||||
setState(() {
|
||||
_lastIntegerSelected = selected;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
MaterialDemoDocumentationButton(SearchDemo.routeName),
|
||||
IconButton(
|
||||
tooltip: 'More (not implemented)',
|
||||
icon: Icon(
|
||||
Theme.of(context).platform == TargetPlatform.iOS
|
||||
? Icons.more_horiz
|
||||
: Icons.more_vert,
|
||||
),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
MergeSemantics(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const <Widget>[
|
||||
Text('Press the '),
|
||||
Tooltip(
|
||||
message: 'search',
|
||||
child: Icon(
|
||||
Icons.search,
|
||||
size: 18.0,
|
||||
),
|
||||
),
|
||||
Text(' icon in the AppBar'),
|
||||
],
|
||||
),
|
||||
const Text(
|
||||
'and search for an integer between 0 and 100,000.'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 64.0),
|
||||
Text('Last selected integer: ${_lastIntegerSelected ?? 'NONE'}.')
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
tooltip: 'Back', // Tests depend on this label to exit the demo.
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
label: const Text('Close demo'),
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
drawer: Drawer(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
const UserAccountsDrawerHeader(
|
||||
accountName: Text('Peter Widget'),
|
||||
accountEmail: Text('peter.widget@example.com'),
|
||||
currentAccountPicture: CircleAvatar(
|
||||
backgroundImage: AssetImage(
|
||||
'people/square/peter.png',
|
||||
),
|
||||
),
|
||||
margin: EdgeInsets.zero,
|
||||
),
|
||||
MediaQuery.removePadding(
|
||||
context: context,
|
||||
// DrawerHeader consumes top MediaQuery padding.
|
||||
removeTop: true,
|
||||
child: const ListTile(
|
||||
leading: Icon(Icons.payment),
|
||||
title: Text('Placeholder'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SearchDemoSearchDelegate extends SearchDelegate<int> {
|
||||
final List<int> _data =
|
||||
List<int>.generate(100001, (int i) => i).reversed.toList();
|
||||
final List<int> _history = <int>[42607, 85604, 66374, 44, 174];
|
||||
|
||||
@override
|
||||
Widget buildLeading(BuildContext context) {
|
||||
return IconButton(
|
||||
tooltip: 'Back',
|
||||
icon: AnimatedIcon(
|
||||
icon: AnimatedIcons.menu_arrow,
|
||||
progress: transitionAnimation,
|
||||
),
|
||||
onPressed: () {
|
||||
close(context, null);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildSuggestions(BuildContext context) {
|
||||
final Iterable<int> suggestions = query.isEmpty
|
||||
? _history
|
||||
: _data.where((int i) => '$i'.startsWith(query));
|
||||
|
||||
return _SuggestionList(
|
||||
query: query,
|
||||
suggestions: suggestions.map<String>((int i) => '$i').toList(),
|
||||
onSelected: (String suggestion) {
|
||||
query = suggestion;
|
||||
showResults(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildResults(BuildContext context) {
|
||||
final int searched = int.tryParse(query);
|
||||
if (searched == null || !_data.contains(searched)) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'"$query"\n is not a valid integer between 0 and 100,000.\nTry again.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView(
|
||||
children: <Widget>[
|
||||
_ResultCard(
|
||||
title: 'This integer',
|
||||
integer: searched,
|
||||
searchDelegate: this,
|
||||
),
|
||||
_ResultCard(
|
||||
title: 'Next integer',
|
||||
integer: searched + 1,
|
||||
searchDelegate: this,
|
||||
),
|
||||
_ResultCard(
|
||||
title: 'Previous integer',
|
||||
integer: searched - 1,
|
||||
searchDelegate: this,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Widget> buildActions(BuildContext context) {
|
||||
return <Widget>[
|
||||
query.isEmpty
|
||||
? IconButton(
|
||||
tooltip: 'Voice Search',
|
||||
icon: const Icon(Icons.mic),
|
||||
onPressed: () {
|
||||
query = 'TODO: implement voice input';
|
||||
},
|
||||
)
|
||||
: IconButton(
|
||||
tooltip: 'Clear',
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
query = '';
|
||||
showSuggestions(context);
|
||||
},
|
||||
)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class _ResultCard extends StatelessWidget {
|
||||
const _ResultCard({this.integer, this.title, this.searchDelegate});
|
||||
|
||||
final int integer;
|
||||
final String title;
|
||||
final SearchDelegate<int> searchDelegate;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
searchDelegate.close(context, integer);
|
||||
},
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Text(title),
|
||||
Text(
|
||||
'$integer',
|
||||
style: theme.textTheme.headline.copyWith(fontSize: 72.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SuggestionList extends StatelessWidget {
|
||||
const _SuggestionList({this.suggestions, this.query, this.onSelected});
|
||||
|
||||
final List<String> suggestions;
|
||||
final String query;
|
||||
final ValueChanged<String> onSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return ListView.builder(
|
||||
itemCount: suggestions.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
final String suggestion = suggestions[i];
|
||||
return ListTile(
|
||||
leading: query.isEmpty ? const Icon(Icons.history) : const Icon(null),
|
||||
title: RichText(
|
||||
text: TextSpan(
|
||||
text: suggestion.substring(0, query.length),
|
||||
style:
|
||||
theme.textTheme.subhead.copyWith(fontWeight: FontWeight.bold),
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: suggestion.substring(query.length),
|
||||
style: theme.textTheme.subhead,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
onSelected(suggestion);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
111
web/gallery/lib/demo/material/selection_controls_demo.dart
Normal file
111
web/gallery/lib/demo/material/selection_controls_demo.dart
Normal file
@@ -0,0 +1,111 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class SelectionControlsDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/selection';
|
||||
|
||||
_SelectionControlsDemoState createState() => _SelectionControlsDemoState();
|
||||
}
|
||||
|
||||
class _SelectionControlsDemoState extends State<SelectionControlsDemo> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
bool checkboxValueA = true;
|
||||
bool checkboxValueB = false;
|
||||
bool checkboxValueC;
|
||||
int radioValue = 0;
|
||||
|
||||
void handleRadioValueChanged(int value) {
|
||||
setState(() {
|
||||
radioValue = value;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return wrapScaffold('Selection Controls', context, _scaffoldKey,
|
||||
_buildContents(), SelectionControlsDemo.routeName);
|
||||
}
|
||||
|
||||
Widget _buildContents() {
|
||||
return Material(
|
||||
color: Colors.white,
|
||||
child: new Column(
|
||||
children: <Widget>[buildCheckbox(), Divider(), buildRadio()]));
|
||||
}
|
||||
|
||||
Widget buildCheckbox() {
|
||||
return Align(
|
||||
alignment: const Alignment(0.0, -0.2),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Checkbox(
|
||||
value: checkboxValueA,
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
checkboxValueA = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
Checkbox(
|
||||
value: checkboxValueB,
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
checkboxValueB = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
Checkbox(
|
||||
value: checkboxValueC,
|
||||
tristate: true,
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
checkboxValueC = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(mainAxisSize: MainAxisSize.min, children: const <Widget>[
|
||||
// Disabled checkboxes
|
||||
Checkbox(value: true, onChanged: null),
|
||||
Checkbox(value: false, onChanged: null),
|
||||
Checkbox(value: null, tristate: true, onChanged: null),
|
||||
])
|
||||
]));
|
||||
}
|
||||
|
||||
Widget buildRadio() {
|
||||
return Align(
|
||||
alignment: const Alignment(0.0, -0.2),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
||||
Row(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
||||
Radio<int>(
|
||||
value: 0,
|
||||
groupValue: radioValue,
|
||||
onChanged: handleRadioValueChanged),
|
||||
Radio<int>(
|
||||
value: 1,
|
||||
groupValue: radioValue,
|
||||
onChanged: handleRadioValueChanged),
|
||||
Radio<int>(
|
||||
value: 2,
|
||||
groupValue: radioValue,
|
||||
onChanged: handleRadioValueChanged)
|
||||
]),
|
||||
// Disabled radio buttons
|
||||
Row(mainAxisSize: MainAxisSize.min, children: const <Widget>[
|
||||
Radio<int>(value: 0, groupValue: 0, onChanged: null),
|
||||
Radio<int>(value: 1, groupValue: 0, onChanged: null),
|
||||
Radio<int>(value: 2, groupValue: 0, onChanged: null)
|
||||
])
|
||||
]));
|
||||
}
|
||||
}
|
||||
240
web/gallery/lib/demo/material/slider_demo.dart
Normal file
240
web/gallery/lib/demo/material/slider_demo.dart
Normal file
@@ -0,0 +1,240 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class SliderDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/slider';
|
||||
|
||||
@override
|
||||
_SliderDemoState createState() => _SliderDemoState();
|
||||
}
|
||||
|
||||
Path _triangle(double size, Offset thumbCenter, {bool invert = false}) {
|
||||
final Path thumbPath = Path();
|
||||
final double height = math.sqrt(3.0) / 2.0;
|
||||
final double halfSide = size / 2.0;
|
||||
final double centerHeight = size * height / 3.0;
|
||||
final double sign = invert ? -1.0 : 1.0;
|
||||
thumbPath.moveTo(
|
||||
thumbCenter.dx - halfSide, thumbCenter.dy + sign * centerHeight);
|
||||
thumbPath.lineTo(thumbCenter.dx, thumbCenter.dy - 2.0 * sign * centerHeight);
|
||||
thumbPath.lineTo(
|
||||
thumbCenter.dx + halfSide, thumbCenter.dy + sign * centerHeight);
|
||||
thumbPath.close();
|
||||
return thumbPath;
|
||||
}
|
||||
|
||||
class _CustomThumbShape extends SliderComponentShape {
|
||||
static const double _thumbSize = 4.0;
|
||||
static const double _disabledThumbSize = 3.0;
|
||||
|
||||
@override
|
||||
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
|
||||
return isEnabled
|
||||
? const Size.fromRadius(_thumbSize)
|
||||
: const Size.fromRadius(_disabledThumbSize);
|
||||
}
|
||||
|
||||
static final Animatable<double> sizeTween = Tween<double>(
|
||||
begin: _disabledThumbSize,
|
||||
end: _thumbSize,
|
||||
);
|
||||
|
||||
@override
|
||||
void paint(
|
||||
PaintingContext context,
|
||||
Offset thumbCenter, {
|
||||
Animation<double> activationAnimation,
|
||||
Animation<double> enableAnimation,
|
||||
bool isDiscrete,
|
||||
TextPainter labelPainter,
|
||||
RenderBox parentBox,
|
||||
SliderThemeData sliderTheme,
|
||||
TextDirection textDirection,
|
||||
double value,
|
||||
}) {
|
||||
final Canvas canvas = context.canvas;
|
||||
final ColorTween colorTween = ColorTween(
|
||||
begin: sliderTheme.disabledThumbColor,
|
||||
end: sliderTheme.thumbColor,
|
||||
);
|
||||
final double size = _thumbSize * sizeTween.evaluate(enableAnimation);
|
||||
final Path thumbPath = _triangle(size, thumbCenter);
|
||||
canvas.drawPath(
|
||||
thumbPath, Paint()..color = colorTween.evaluate(enableAnimation));
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomValueIndicatorShape extends SliderComponentShape {
|
||||
static const double _indicatorSize = 4.0;
|
||||
static const double _disabledIndicatorSize = 3.0;
|
||||
static const double _slideUpHeight = 40.0;
|
||||
|
||||
@override
|
||||
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
|
||||
return Size.fromRadius(isEnabled ? _indicatorSize : _disabledIndicatorSize);
|
||||
}
|
||||
|
||||
static final Animatable<double> sizeTween = Tween<double>(
|
||||
begin: _disabledIndicatorSize,
|
||||
end: _indicatorSize,
|
||||
);
|
||||
|
||||
@override
|
||||
void paint(
|
||||
PaintingContext context,
|
||||
Offset thumbCenter, {
|
||||
Animation<double> activationAnimation,
|
||||
Animation<double> enableAnimation,
|
||||
bool isDiscrete,
|
||||
TextPainter labelPainter,
|
||||
RenderBox parentBox,
|
||||
SliderThemeData sliderTheme,
|
||||
TextDirection textDirection,
|
||||
double value,
|
||||
}) {
|
||||
final Canvas canvas = context.canvas;
|
||||
final ColorTween enableColor = ColorTween(
|
||||
begin: sliderTheme.disabledThumbColor,
|
||||
end: sliderTheme.valueIndicatorColor,
|
||||
);
|
||||
final Tween<double> slideUpTween = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: _slideUpHeight,
|
||||
);
|
||||
final double size = _indicatorSize * sizeTween.evaluate(enableAnimation);
|
||||
final Offset slideUpOffset =
|
||||
Offset(0.0, -slideUpTween.evaluate(activationAnimation));
|
||||
final Path thumbPath = _triangle(
|
||||
size,
|
||||
thumbCenter + slideUpOffset,
|
||||
invert: true,
|
||||
);
|
||||
final Color paintColor = enableColor
|
||||
.evaluate(enableAnimation)
|
||||
.withAlpha((255.0 * activationAnimation.value).round());
|
||||
canvas.drawPath(
|
||||
thumbPath,
|
||||
Paint()..color = paintColor,
|
||||
);
|
||||
canvas.drawLine(
|
||||
thumbCenter,
|
||||
thumbCenter + slideUpOffset,
|
||||
Paint()
|
||||
..color = paintColor
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2.0);
|
||||
labelPainter.paint(
|
||||
canvas,
|
||||
thumbCenter +
|
||||
slideUpOffset +
|
||||
Offset(-labelPainter.width / 2.0, -labelPainter.height - 4.0));
|
||||
}
|
||||
}
|
||||
|
||||
class _SliderDemoState extends State<SliderDemo> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
double _value = 25.0;
|
||||
double _discreteValue = 40.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return wrapScaffold('Slider Demo', context, _scaffoldKey,
|
||||
_buildContents(context), SliderDemo.routeName);
|
||||
}
|
||||
|
||||
Widget _buildContents(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Slider(
|
||||
value: _value,
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
_value = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Text('Continuous'),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Slider(value: 0.25, onChanged: (double val) {}),
|
||||
Text('Disabled'),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Slider(
|
||||
value: _discreteValue,
|
||||
min: 0.0,
|
||||
max: 200.0,
|
||||
divisions: 5,
|
||||
label: '${_discreteValue.round()}',
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
_discreteValue = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Text('Discrete'),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
SliderTheme(
|
||||
data: theme.sliderTheme.copyWith(
|
||||
activeTrackColor: Colors.deepPurple,
|
||||
inactiveTrackColor: Colors.black26,
|
||||
activeTickMarkColor: Colors.white70,
|
||||
inactiveTickMarkColor: Colors.black,
|
||||
overlayColor: Colors.black12,
|
||||
thumbColor: Colors.deepPurple,
|
||||
valueIndicatorColor: Colors.deepPurpleAccent,
|
||||
thumbShape: _CustomThumbShape(),
|
||||
valueIndicatorShape: _CustomValueIndicatorShape(),
|
||||
valueIndicatorTextStyle: theme.accentTextTheme.body2
|
||||
.copyWith(color: Colors.black87),
|
||||
),
|
||||
child: Slider(
|
||||
value: _discreteValue,
|
||||
min: 0.0,
|
||||
max: 200.0,
|
||||
divisions: 5,
|
||||
semanticFormatterCallback: (double value) =>
|
||||
value.round().toString(),
|
||||
label: '${_discreteValue.round()}',
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
_discreteValue = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const Text('Discrete with Custom Theme'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
83
web/gallery/lib/demo/material/snack_bar_demo.dart
Normal file
83
web/gallery/lib/demo/material/snack_bar_demo.dart
Normal file
@@ -0,0 +1,83 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
const String _text1 =
|
||||
'Snackbars provide lightweight feedback about an operation by '
|
||||
'showing a brief message at the bottom of the screen. Snackbars '
|
||||
'can contain an action.';
|
||||
|
||||
const String _text2 =
|
||||
'Snackbars should contain a single line of text directly related '
|
||||
'to the operation performed. They cannot contain icons.';
|
||||
|
||||
const String _text3 =
|
||||
'By default snackbars automatically disappear after a few seconds ';
|
||||
|
||||
class SnackBarDemo extends StatefulWidget {
|
||||
const SnackBarDemo({Key key}) : super(key: key);
|
||||
|
||||
static const String routeName = '/material/snack-bar';
|
||||
|
||||
@override
|
||||
_SnackBarDemoState createState() => _SnackBarDemoState();
|
||||
}
|
||||
|
||||
class _SnackBarDemoState extends State<SnackBarDemo> {
|
||||
int _snackBarIndex = 1;
|
||||
|
||||
Widget buildBody(BuildContext context) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
children: <Widget>[
|
||||
const Text(_text1),
|
||||
const Text(_text2),
|
||||
Center(
|
||||
child: Row(children: <Widget>[
|
||||
RaisedButton(
|
||||
child: const Text('SHOW A SNACKBAR'),
|
||||
onPressed: () {
|
||||
final int thisSnackBarIndex = _snackBarIndex++;
|
||||
Scaffold.of(context).showSnackBar(SnackBar(
|
||||
content: Text('This is snackbar #$thisSnackBarIndex.'),
|
||||
action: SnackBarAction(
|
||||
label: 'ACTION',
|
||||
onPressed: () {
|
||||
Scaffold.of(context).showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
'You pressed snackbar $thisSnackBarIndex\'s action.')));
|
||||
}),
|
||||
));
|
||||
}),
|
||||
]),
|
||||
),
|
||||
const Text(_text3),
|
||||
].map<Widget>((Widget child) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: child);
|
||||
}).toList()),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Snackbar'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(SnackBarDemo.routeName)
|
||||
],
|
||||
),
|
||||
body: Builder(
|
||||
// Create an inner BuildContext so that the snackBar onPressed methods
|
||||
// can refer to the Scaffold with Scaffold.of().
|
||||
builder: buildBody));
|
||||
}
|
||||
}
|
||||
23
web/gallery/lib/demo/material/stack_demo.dart
Normal file
23
web/gallery/lib/demo/material/stack_demo.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
class StackDemo extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Colors.greenAccent,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
child: Stack(children: [
|
||||
Text('A'),
|
||||
Text('B'),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
42
web/gallery/lib/demo/material/switch_demo.dart
Normal file
42
web/gallery/lib/demo/material/switch_demo.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class SwitchDemo extends StatefulWidget {
|
||||
static const routeName = '/material/switch';
|
||||
|
||||
@override
|
||||
SwitchDemoState createState() => SwitchDemoState();
|
||||
}
|
||||
|
||||
class SwitchDemoState extends State<SwitchDemo> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return wrapScaffold('Switch Demo', context, _scaffoldKey, _buildContents(),
|
||||
SwitchDemo.routeName);
|
||||
}
|
||||
|
||||
bool _value = true;
|
||||
|
||||
Widget _buildContents() {
|
||||
return Material(
|
||||
child: Column(
|
||||
children: [
|
||||
Switch(
|
||||
value: _value,
|
||||
onChanged: (bool newValue) {
|
||||
setState(() {
|
||||
_value = newValue;
|
||||
});
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
209
web/gallery/lib/demo/material/tabs_demo.dart
Normal file
209
web/gallery/lib/demo/material/tabs_demo.dart
Normal file
@@ -0,0 +1,209 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// Each TabBarView contains a _Page and for each _Page there is a list
|
||||
// of _CardData objects. Each _CardData object is displayed by a _CardItem.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
|
||||
|
||||
class _Page {
|
||||
_Page({this.label});
|
||||
final String label;
|
||||
String get id => label[0];
|
||||
@override
|
||||
String toString() => '$runtimeType("$label")';
|
||||
}
|
||||
|
||||
class _CardData {
|
||||
const _CardData({this.title, this.imageAsset, this.imageAssetPackage});
|
||||
final String title;
|
||||
final String imageAsset;
|
||||
final String imageAssetPackage;
|
||||
}
|
||||
|
||||
final Map<_Page, List<_CardData>> _allPages = <_Page, List<_CardData>>{
|
||||
_Page(label: 'HOME'): <_CardData>[
|
||||
const _CardData(
|
||||
title: 'Flatwear',
|
||||
imageAsset: 'products/flatwear.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
),
|
||||
const _CardData(
|
||||
title: 'Pine Table',
|
||||
imageAsset: 'products/table.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
),
|
||||
const _CardData(
|
||||
title: 'Blue Cup',
|
||||
imageAsset: 'products/cup.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
),
|
||||
const _CardData(
|
||||
title: 'Tea Set',
|
||||
imageAsset: 'products/teaset.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
),
|
||||
const _CardData(
|
||||
title: 'Desk Set',
|
||||
imageAsset: 'products/deskset.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
),
|
||||
const _CardData(
|
||||
title: 'Blue Linen Napkins',
|
||||
imageAsset: 'products/napkins.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
),
|
||||
const _CardData(
|
||||
title: 'Planters',
|
||||
imageAsset: 'products/planters.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
),
|
||||
const _CardData(
|
||||
title: 'Kitchen Quattro',
|
||||
imageAsset: 'products/kitchen_quattro.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
),
|
||||
const _CardData(
|
||||
title: 'Platter',
|
||||
imageAsset: 'products/platter.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
),
|
||||
],
|
||||
_Page(label: 'APPAREL'): <_CardData>[
|
||||
const _CardData(
|
||||
title: 'Cloud-White Dress',
|
||||
imageAsset: 'products/dress.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
),
|
||||
const _CardData(
|
||||
title: 'Ginger Scarf',
|
||||
imageAsset: 'products/scarf.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
),
|
||||
const _CardData(
|
||||
title: 'Blush Sweats',
|
||||
imageAsset: 'products/sweats.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
class _CardDataItem extends StatelessWidget {
|
||||
const _CardDataItem({this.page, this.data});
|
||||
|
||||
static const double height = 272.0;
|
||||
final _Page page;
|
||||
final _CardData data;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Align(
|
||||
alignment:
|
||||
page.id == 'H' ? Alignment.centerLeft : Alignment.centerRight,
|
||||
child: CircleAvatar(child: Text('${page.id}')),
|
||||
),
|
||||
SizedBox(width: 144.0, height: 144.0, child: new Text('image')
|
||||
// Image.asset(
|
||||
// data.imageAsset,
|
||||
// package: data.imageAssetPackage,
|
||||
// fit: BoxFit.contain,
|
||||
// ),
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
data.title,
|
||||
style: Theme.of(context).textTheme.title,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TabsDemo extends StatelessWidget {
|
||||
static const String routeName = '/material/tabs';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: _allPages.length,
|
||||
child: Scaffold(
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
return <Widget>[
|
||||
SliverAppBar(
|
||||
title: const Text('Tabs and scrolling'),
|
||||
actions: <Widget>[MaterialDemoDocumentationButton(routeName)],
|
||||
pinned: true,
|
||||
expandedHeight: 150.0,
|
||||
forceElevated: innerBoxIsScrolled,
|
||||
bottom: TabBar(
|
||||
tabs: _allPages.keys
|
||||
.map<Widget>(
|
||||
(_Page page) => Tab(text: page.label),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
body: TabBarView(
|
||||
children: _allPages.keys.map<Widget>((_Page page) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return CustomScrollView(
|
||||
key: PageStorageKey<_Page>(page),
|
||||
slivers: <Widget>[
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
horizontal: 16.0,
|
||||
),
|
||||
sliver: SliverFixedExtentList(
|
||||
itemExtent: _CardDataItem.height,
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
final _CardData data = _allPages[page][index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: _CardDataItem(
|
||||
page: page,
|
||||
data: data,
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: _allPages[page].length,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
150
web/gallery/lib/demo/material/tabs_fab_demo.dart
Normal file
150
web/gallery/lib/demo/material/tabs_fab_demo.dart
Normal file
@@ -0,0 +1,150 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
const String _explanatoryText =
|
||||
"When the Scaffold's floating action button changes, the new button fades and "
|
||||
'turns into view. In this demo, changing tabs can cause the app to be rebuilt '
|
||||
'with a FloatingActionButton that the Scaffold distinguishes from the others '
|
||||
'by its key.';
|
||||
|
||||
class _Page {
|
||||
_Page({this.label, this.colors, this.icon});
|
||||
|
||||
final String label;
|
||||
final MaterialColor colors;
|
||||
final IconData icon;
|
||||
|
||||
Color get labelColor =>
|
||||
colors != null ? colors.shade300 : Colors.grey.shade300;
|
||||
bool get fabDefined => colors != null && icon != null;
|
||||
Color get fabColor => colors.shade400;
|
||||
Icon get fabIcon => Icon(icon);
|
||||
Key get fabKey => ValueKey<Color>(fabColor);
|
||||
}
|
||||
|
||||
final List<_Page> _allPages = <_Page>[
|
||||
_Page(label: 'Blue', colors: Colors.indigo, icon: Icons.add),
|
||||
_Page(label: 'Eco', colors: Colors.green, icon: Icons.create),
|
||||
_Page(label: 'No'),
|
||||
_Page(label: 'Teal', colors: Colors.teal, icon: Icons.add),
|
||||
_Page(label: 'Red', colors: Colors.red, icon: Icons.create),
|
||||
];
|
||||
|
||||
class TabsFabDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/tabs-fab';
|
||||
|
||||
@override
|
||||
_TabsFabDemoState createState() => _TabsFabDemoState();
|
||||
}
|
||||
|
||||
class _TabsFabDemoState extends State<TabsFabDemo>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
TabController _controller;
|
||||
_Page _selectedPage;
|
||||
bool _extendedButtons = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TabController(vsync: this, length: _allPages.length);
|
||||
_controller.addListener(_handleTabSelection);
|
||||
_selectedPage = _allPages[0];
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleTabSelection() {
|
||||
setState(() {
|
||||
_selectedPage = _allPages[_controller.index];
|
||||
});
|
||||
}
|
||||
|
||||
void _showExplanatoryText() {
|
||||
_scaffoldKey.currentState.showBottomSheet<Null>((BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(color: Theme.of(context).dividerColor))),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Text(_explanatoryText,
|
||||
style: Theme.of(context).textTheme.subhead)));
|
||||
});
|
||||
}
|
||||
|
||||
Widget buildTabView(_Page page) {
|
||||
return Builder(builder: (BuildContext context) {
|
||||
return Container(
|
||||
key: ValueKey<String>(page.label),
|
||||
padding: const EdgeInsets.fromLTRB(48.0, 48.0, 48.0, 96.0),
|
||||
child: Card(
|
||||
child: Center(
|
||||
child: Text(page.label,
|
||||
style: TextStyle(color: page.labelColor, fontSize: 32.0),
|
||||
textAlign: TextAlign.center))));
|
||||
});
|
||||
}
|
||||
|
||||
Widget buildFloatingActionButton(_Page page) {
|
||||
if (!page.fabDefined) return null;
|
||||
|
||||
if (_extendedButtons) {
|
||||
return FloatingActionButton.extended(
|
||||
key: ValueKey<Key>(page.fabKey),
|
||||
tooltip: 'Show explanation',
|
||||
backgroundColor: page.fabColor,
|
||||
icon: page.fabIcon,
|
||||
label: Text(page.label.toUpperCase()),
|
||||
onPressed: _showExplanatoryText);
|
||||
}
|
||||
|
||||
return FloatingActionButton(
|
||||
key: page.fabKey,
|
||||
tooltip: 'Show explanation',
|
||||
backgroundColor: page.fabColor,
|
||||
child: page.fabIcon,
|
||||
onPressed: _showExplanatoryText);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: const Text('FAB per tab'),
|
||||
bottom: TabBar(
|
||||
controller: _controller,
|
||||
tabs: _allPages
|
||||
.map<Widget>((_Page page) => Tab(text: page.label.toUpperCase()))
|
||||
.toList(),
|
||||
),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(TabsFabDemo.routeName),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.sentiment_very_satisfied),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_extendedButtons = !_extendedButtons;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: buildFloatingActionButton(_selectedPage),
|
||||
body: TabBarView(
|
||||
controller: _controller,
|
||||
children: _allPages.map<Widget>(buildTabView).toList()),
|
||||
);
|
||||
}
|
||||
}
|
||||
52
web/gallery/lib/demo/material/text_demo.dart
Normal file
52
web/gallery/lib/demo/material/text_demo.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
class TextDemo extends StatelessWidget {
|
||||
static const routeName = '/material/text';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Text'),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
pad(Text('Single line of text')),
|
||||
Divider(),
|
||||
// Single line with many whitespaces in between.
|
||||
pad(Text(' Text with a lot of whitespace ')),
|
||||
Divider(),
|
||||
// Forced multi-line because of the \n.
|
||||
pad(Text('Text with a newline\ncharacter should render in 2 lines')),
|
||||
Divider(),
|
||||
// Multi-line with regular whitespace.
|
||||
pad(Text(
|
||||
'''Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas auctor
|
||||
vel ligula eget fermentum. Integer mattis nulla vitae ullamcorper
|
||||
dignissim. Donec vel velit vel eros lobortis laoreet at sit amet turpis.
|
||||
Ut in orci blandit, rhoncus metus quis, finibus augue. Nullam a elit
|
||||
venenatis metus accumsan dapibus. Vestibulum imperdiet tristique viverra.''',
|
||||
)),
|
||||
Divider(),
|
||||
// Multi-line with a lot of whitespace in between.
|
||||
pad(Text(
|
||||
'''
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Maecenas auctor vel ligula eget fermentum.
|
||||
Integer mattis nulla vitae ullamcorper dignissim.
|
||||
Donec vel velit vel eros lobortis laoreet at sit amet turpis.''',
|
||||
)),
|
||||
Divider(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Padding pad(Widget child) =>
|
||||
Padding(padding: EdgeInsets.all(12), child: child);
|
||||
}
|
||||
341
web/gallery/lib/demo/material/text_form_field_demo.dart
Normal file
341
web/gallery/lib/demo/material/text_form_field_demo.dart
Normal file
@@ -0,0 +1,341 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
import 'package:flutter_web/services.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class TextFormFieldDemo extends StatefulWidget {
|
||||
const TextFormFieldDemo({Key key}) : super(key: key);
|
||||
|
||||
static const String routeName = '/material/text-form-field';
|
||||
|
||||
@override
|
||||
TextFormFieldDemoState createState() => TextFormFieldDemoState();
|
||||
}
|
||||
|
||||
class PersonData {
|
||||
String name = '';
|
||||
String phoneNumber = '';
|
||||
String email = '';
|
||||
String password = '';
|
||||
}
|
||||
|
||||
class PasswordField extends StatefulWidget {
|
||||
const PasswordField({
|
||||
this.fieldKey,
|
||||
this.hintText,
|
||||
this.labelText,
|
||||
this.helperText,
|
||||
this.onSaved,
|
||||
this.validator,
|
||||
this.onFieldSubmitted,
|
||||
});
|
||||
|
||||
final Key fieldKey;
|
||||
final String hintText;
|
||||
final String labelText;
|
||||
final String helperText;
|
||||
final FormFieldSetter<String> onSaved;
|
||||
final FormFieldValidator<String> validator;
|
||||
final ValueChanged<String> onFieldSubmitted;
|
||||
|
||||
@override
|
||||
_PasswordFieldState createState() => _PasswordFieldState();
|
||||
}
|
||||
|
||||
class _PasswordFieldState extends State<PasswordField> {
|
||||
bool _obscureText = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
key: widget.fieldKey,
|
||||
obscureText: _obscureText,
|
||||
maxLength: 8,
|
||||
onSaved: widget.onSaved,
|
||||
validator: widget.validator,
|
||||
onFieldSubmitted: widget.onFieldSubmitted,
|
||||
decoration: InputDecoration(
|
||||
border: const UnderlineInputBorder(),
|
||||
filled: true,
|
||||
hintText: widget.hintText,
|
||||
labelText: widget.labelText,
|
||||
helperText: widget.helperText,
|
||||
suffixIcon: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_obscureText = !_obscureText;
|
||||
});
|
||||
},
|
||||
child: Icon(
|
||||
_obscureText ? Icons.visibility : Icons.visibility_off,
|
||||
semanticLabel: _obscureText ? 'show password' : 'hide password',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TextFormFieldDemoState extends State<TextFormFieldDemo> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
PersonData person = PersonData();
|
||||
|
||||
void showInSnackBar(String value) {
|
||||
_scaffoldKey.currentState.showSnackBar(SnackBar(content: Text(value)));
|
||||
}
|
||||
|
||||
bool _autovalidate = false;
|
||||
bool _formWasEdited = false;
|
||||
|
||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||
final GlobalKey<FormFieldState<String>> _passwordFieldKey =
|
||||
GlobalKey<FormFieldState<String>>();
|
||||
final _UsNumberTextInputFormatter _phoneNumberFormatter =
|
||||
_UsNumberTextInputFormatter();
|
||||
void _handleSubmitted() {
|
||||
final FormState form = _formKey.currentState;
|
||||
if (!form.validate()) {
|
||||
_autovalidate = true; // Start validating on every change.
|
||||
showInSnackBar('Please fix the errors in red before submitting.');
|
||||
} else {
|
||||
form.save();
|
||||
showInSnackBar('${person.name}\'s phone number is ${person.phoneNumber}');
|
||||
}
|
||||
}
|
||||
|
||||
String _validateName(String value) {
|
||||
_formWasEdited = true;
|
||||
if (value.isEmpty) return 'Name is required.';
|
||||
final RegExp nameExp = RegExp(r'^[A-Za-z ]+$');
|
||||
if (!nameExp.hasMatch(value))
|
||||
return 'Please enter only alphabetical characters.';
|
||||
return null;
|
||||
}
|
||||
|
||||
String _validatePhoneNumber(String value) {
|
||||
_formWasEdited = true;
|
||||
final RegExp phoneExp = RegExp(r'^\(\d\d\d\) \d\d\d\-\d\d\d\d$');
|
||||
if (!phoneExp.hasMatch(value))
|
||||
return '(###) ###-#### - Enter a US phone number.';
|
||||
return null;
|
||||
}
|
||||
|
||||
String _validatePassword(String value) {
|
||||
_formWasEdited = true;
|
||||
final FormFieldState<String> passwordField = _passwordFieldKey.currentState;
|
||||
if (passwordField.value == null || passwordField.value.isEmpty)
|
||||
return 'Please enter a password.';
|
||||
if (passwordField.value != value) return 'The passwords don\'t match';
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<bool> _warnUserAboutInvalidData() async {
|
||||
final FormState form = _formKey.currentState;
|
||||
if (form == null || !_formWasEdited || form.validate()) return true;
|
||||
|
||||
return await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('This form has errors'),
|
||||
content: const Text('Really leave this form?'),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
child: const Text('YES'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
),
|
||||
FlatButton(
|
||||
child: const Text('NO'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: const Text('Text fields'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(TextFormFieldDemo.routeName)
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
autovalidate: _autovalidate,
|
||||
onWillPop: _warnUserAboutInvalidData,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
const SizedBox(height: 24.0),
|
||||
TextFormField(
|
||||
textCapitalization: TextCapitalization.words,
|
||||
decoration: const InputDecoration(
|
||||
border: UnderlineInputBorder(),
|
||||
filled: true,
|
||||
icon: Icon(Icons.person),
|
||||
hintText: 'What do people call you?',
|
||||
labelText: 'Name * ',
|
||||
),
|
||||
onSaved: (String value) {
|
||||
person.name = value;
|
||||
},
|
||||
validator: _validateName,
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
border: UnderlineInputBorder(),
|
||||
filled: true,
|
||||
icon: Icon(Icons.phone),
|
||||
hintText: 'Where can we reach you?',
|
||||
labelText: 'Phone Number * ',
|
||||
prefixText: '+1',
|
||||
),
|
||||
keyboardType: TextInputType.phone,
|
||||
onSaved: (String value) {
|
||||
person.phoneNumber = value;
|
||||
},
|
||||
validator: _validatePhoneNumber,
|
||||
// TextInputFormatters are applied in sequence.
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
WhitelistingTextInputFormatter.digitsOnly,
|
||||
// Fit the validating format.
|
||||
_phoneNumberFormatter,
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
border: UnderlineInputBorder(),
|
||||
filled: true,
|
||||
icon: Icon(Icons.email),
|
||||
hintText: 'Your email address',
|
||||
labelText: 'E-mail',
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
onSaved: (String value) {
|
||||
person.email = value;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
hintText:
|
||||
'Tell us about yourself (e.g., write down what you do or what hobbies you have)',
|
||||
helperText: 'Keep it short, this is just a demo.',
|
||||
labelText: 'Life story',
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
TextFormField(
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Salary',
|
||||
prefixText: '\$',
|
||||
suffixText: 'USD',
|
||||
suffixStyle: TextStyle(color: Colors.green)),
|
||||
maxLines: 1,
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
PasswordField(
|
||||
fieldKey: _passwordFieldKey,
|
||||
helperText: 'No more than 8 characters.',
|
||||
labelText: 'Password *',
|
||||
onFieldSubmitted: (String value) {
|
||||
setState(() {
|
||||
person.password = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
TextFormField(
|
||||
enabled:
|
||||
person.password != null && person.password.isNotEmpty,
|
||||
decoration: const InputDecoration(
|
||||
border: UnderlineInputBorder(),
|
||||
filled: true,
|
||||
labelText: 'Re-type password',
|
||||
),
|
||||
maxLength: 8,
|
||||
obscureText: true,
|
||||
validator: _validatePassword,
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
Center(
|
||||
child: RaisedButton(
|
||||
child: const Text('SUBMIT'),
|
||||
onPressed: _handleSubmitted,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
Text('* indicates required field',
|
||||
style: Theme.of(context).textTheme.caption),
|
||||
const SizedBox(height: 24.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Format incoming numeric text to fit the format of (###) ###-#### ##...
|
||||
class _UsNumberTextInputFormatter extends TextInputFormatter {
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue, TextEditingValue newValue) {
|
||||
final int newTextLength = newValue.text.length;
|
||||
int selectionIndex = newValue.selection.end;
|
||||
int usedSubstringIndex = 0;
|
||||
final StringBuffer newText = StringBuffer();
|
||||
if (newTextLength >= 1) {
|
||||
newText.write('(');
|
||||
if (newValue.selection.end >= 1) selectionIndex++;
|
||||
}
|
||||
if (newTextLength >= 4) {
|
||||
newText.write(newValue.text.substring(0, usedSubstringIndex = 3) + ') ');
|
||||
if (newValue.selection.end >= 3) selectionIndex += 2;
|
||||
}
|
||||
if (newTextLength >= 7) {
|
||||
newText.write(newValue.text.substring(3, usedSubstringIndex = 6) + '-');
|
||||
if (newValue.selection.end >= 6) selectionIndex++;
|
||||
}
|
||||
if (newTextLength >= 11) {
|
||||
newText.write(newValue.text.substring(6, usedSubstringIndex = 10) + ' ');
|
||||
if (newValue.selection.end >= 10) selectionIndex++;
|
||||
}
|
||||
// Dump the rest.
|
||||
if (newTextLength >= usedSubstringIndex)
|
||||
newText.write(newValue.text.substring(usedSubstringIndex));
|
||||
return TextEditingValue(
|
||||
text: newText.toString(),
|
||||
selection: TextSelection.collapsed(offset: selectionIndex),
|
||||
);
|
||||
}
|
||||
}
|
||||
59
web/gallery/lib/demo/material/tooltip_demo.dart
Normal file
59
web/gallery/lib/demo/material/tooltip_demo.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
const String _introText =
|
||||
'Tooltips are short identifying messages that briefly appear in response to '
|
||||
'a long press. Tooltip messages are also used by services that make Flutter '
|
||||
'apps accessible, like screen readers.';
|
||||
|
||||
class TooltipDemo extends StatelessWidget {
|
||||
static const String routeName = '/material/tooltips';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Tooltips'),
|
||||
actions: <Widget>[MaterialDemoDocumentationButton(routeName)],
|
||||
),
|
||||
body: Builder(builder: (BuildContext context) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
Text(_introText, style: theme.textTheme.subhead),
|
||||
Row(children: <Widget>[
|
||||
Text('Long press the ', style: theme.textTheme.subhead),
|
||||
Tooltip(
|
||||
message: 'call icon',
|
||||
child: Icon(Icons.call,
|
||||
size: 18.0, color: theme.iconTheme.color)),
|
||||
Text(' icon.', style: theme.textTheme.subhead)
|
||||
]),
|
||||
Center(
|
||||
child: IconButton(
|
||||
iconSize: 48.0,
|
||||
icon: const Icon(Icons.call),
|
||||
color: theme.iconTheme.color,
|
||||
tooltip: 'Place a phone call',
|
||||
onPressed: () {
|
||||
Scaffold.of(context).showSnackBar(const SnackBar(
|
||||
content: Text('That was an ordinary tap.')));
|
||||
}))
|
||||
].map<Widget>((Widget widget) {
|
||||
return Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(top: 16.0, left: 16.0, right: 16.0),
|
||||
child: widget);
|
||||
}).toList()),
|
||||
);
|
||||
}));
|
||||
}
|
||||
}
|
||||
34
web/gallery/lib/demo/material/two_level_list_demo.dart
Normal file
34
web/gallery/lib/demo/material/two_level_list_demo.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class TwoLevelListDemo extends StatelessWidget {
|
||||
static const String routeName = '/material/two-level-list';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Expand/collapse list control'),
|
||||
actions: <Widget>[MaterialDemoDocumentationButton(routeName)],
|
||||
),
|
||||
body: ListView(children: <Widget>[
|
||||
const ListTile(title: Text('Top')),
|
||||
ExpansionTile(
|
||||
title: const Text('Sublist'),
|
||||
backgroundColor: Theme.of(context).accentColor.withOpacity(0.025),
|
||||
children: const <Widget>[
|
||||
ListTile(title: Text('One')),
|
||||
ListTile(title: Text('Two')),
|
||||
// https://en.wikipedia.org/wiki/Free_Four
|
||||
ListTile(title: Text('Free')),
|
||||
ListTile(title: Text('Four'))
|
||||
]),
|
||||
const ListTile(title: Text('Bottom'))
|
||||
]));
|
||||
}
|
||||
}
|
||||
718
web/gallery/lib/demo/pesto_demo.dart
Normal file
718
web/gallery/lib/demo/pesto_demo.dart
Normal 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')
|
||||
],
|
||||
),
|
||||
];
|
||||
254
web/gallery/lib/demo/shrine/shrine_data.dart
Normal file
254
web/gallery/lib/demo/shrine/shrine_data.dart
Normal 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: 'Ali’s shop',
|
||||
avatarAsset: 'people/square/ali.png',
|
||||
avatarAssetPackage: _kGalleryAssetsPackage,
|
||||
description:
|
||||
'Ali Connor’s 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: 'Peter’s 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 shop’s goods are handmade with love. Custom orders are '
|
||||
'available upon request if you need something extra special.');
|
||||
|
||||
const Vendor _sandra = Vendor(
|
||||
name: 'Sandra’s 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 you’re looking for a certain color or material.');
|
||||
|
||||
const Vendor _stella = Vendor(
|
||||
name: 'Stella’s 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: 'Trevor’s 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 shop’s 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:
|
||||
'Isn’t 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 don’t 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 don’t drop it in a bucket of '
|
||||
'red paint, then they won’t 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 you’re 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);
|
||||
}
|
||||
434
web/gallery/lib/demo/shrine/shrine_home.dart
Normal file
434
web/gallery/lib/demo/shrine/shrine_home.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
353
web/gallery/lib/demo/shrine/shrine_order.dart
Normal file
353
web/gallery/lib/demo/shrine/shrine_order.dart
Normal 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);
|
||||
}
|
||||
137
web/gallery/lib/demo/shrine/shrine_page.dart
Normal file
137
web/gallery/lib/demo/shrine/shrine_page.dart
Normal 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));
|
||||
}
|
||||
}
|
||||
76
web/gallery/lib/demo/shrine/shrine_theme.dart
Normal file
76
web/gallery/lib/demo/shrine/shrine_theme.dart
Normal 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;
|
||||
}
|
||||
100
web/gallery/lib/demo/shrine/shrine_types.dart
Normal file
100
web/gallery/lib/demo/shrine/shrine_types.dart
Normal 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)';
|
||||
}
|
||||
43
web/gallery/lib/demo/shrine_demo.dart
Normal file
43
web/gallery/lib/demo/shrine_demo.dart
Normal 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());
|
||||
}
|
||||
86
web/gallery/lib/demo/typography_demo.dart
Normal file
86
web/gallery/lib/demo/typography_demo.dart
Normal 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user