Deleting web/gallery because the newer gallery/gallery includes web support. (#240)

pull/243/head
Brett Morgan 6 years ago committed by GitHub
parent 9d6f6d3641
commit 234a7f3d0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,32 +0,0 @@
# Flutter gallery
A demo app for Flutter's material design and cupertino widgets, as
well as many other features of the Flutter SDK.
## Building
You can follow these instructions to build the gallery app
and install it onto your device.
### Prerequisites
If you are new to Flutter, please first follow
the [Flutter Setup](https://flutter.dev/setup/) guide.
### Building and installing the Flutter app
* `cd $FLUTTER_ROOT/examples/flutter_gallery`
* `flutter pub get`
* `flutter run --release`
The `flutter run --release` command both builds and installs the Flutter app.
## Prerelease checklist
* Verify that the About box's license page scrolls and reveals its long
long stream of license texts.
## Icon
Android launcher icons were generated using Android Asset Studio:
https://romannurik.github.io/AndroidAssetStudio/icons-launcher.html#foreground.type=image&foreground.space.trim=1&foreground.space.pad=0.1&foreColor=607d8b%2C0&crop=0&backgroundShape=square&backColor=fafafa%2C100&effects=none

@ -1,8 +0,0 @@
# Take our settings from the repo's main analysis_options.yaml file, but add
# an exclude for the build directory.
include: ../../analysis_options.yaml
analyzer:
exclude:
- build/**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

@ -1,17 +0,0 @@
// Copyright 2016 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 'fortnightly/fortnightly.dart';
export 'images_demo.dart';
export 'material/material.dart';
export 'pesto_demo.dart';
export 'shrine_demo.dart';
export 'transformations/transformations_demo.dart';
export 'typography_demo.dart';
export 'video_demo.dart';

@ -1,628 +0,0 @@
// Copyright 2017 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.
// Based on https://material.uplabs.com/posts/google-newsstand-navigation-pattern
// See also: https://material-motion.github.io/material-motion/documentation/
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/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)
follower.position.jumpToWithoutSettling(leader.position.pixels); // ignore: deprecated_member_use
}
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);
},
),
),
),
),
],
),
);
}
}

@ -1,170 +0,0 @@
// Copyright 2017 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.
// Raw data for the animation demo.
import 'package:flutter/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 = 'flutter_gallery_assets';
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,
],
),
];

@ -1,167 +0,0 @@
// Copyright 2017 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/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,
],
),
),
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,
);
}
}

@ -1,16 +0,0 @@
// Copyright 2017 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/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();
}

@ -1,270 +0,0 @@
// Copyright 2016 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/material.dart';
import 'logic.dart';
class Calculator extends StatefulWidget {
const Calculator({Key key}) : super(key: key);
@override
_CalculatorState createState() => _CalculatorState();
}
class _CalculatorState extends State<Calculator> {
/// As the user taps keys we update the current `_expression` and we also
/// keep a stack of previous expressions so we can return to earlier states
/// when the user hits the DEL key.
final List<CalcExpression> _expressionStack = <CalcExpression>[];
CalcExpression _expression = CalcExpression.empty();
// Make `expression` the current expression and push the previous current
// expression onto the stack.
void pushExpression(CalcExpression expression) {
_expressionStack.add(_expression);
_expression = expression;
}
/// Pop the top expression off of the stack and make it the current expression.
void popCalcExpression() {
if (_expressionStack.isNotEmpty) {
_expression = _expressionStack.removeLast();
} else {
_expression = CalcExpression.empty();
}
}
/// Set `resultExpression` to the current expression and clear the stack.
void setResult(CalcExpression resultExpression) {
_expressionStack.clear();
_expression = resultExpression;
}
void handleNumberTap(int n) {
final CalcExpression expression = _expression.appendDigit(n);
if (expression != null) {
setState(() {
pushExpression(expression);
});
}
}
void handlePointTap() {
final CalcExpression expression = _expression.appendPoint();
if (expression != null) {
setState(() {
pushExpression(expression);
});
}
}
void handlePlusTap() {
final CalcExpression expression = _expression.appendOperation(Operation.Addition);
if (expression != null) {
setState(() {
pushExpression(expression);
});
}
}
void handleMinusTap() {
final CalcExpression expression = _expression.appendMinus();
if (expression != null) {
setState(() {
pushExpression(expression);
});
}
}
void handleMultTap() {
final CalcExpression expression = _expression.appendOperation(Operation.Multiplication);
if (expression != null) {
setState(() {
pushExpression(expression);
});
}
}
void handleDivTap() {
final CalcExpression expression = _expression.appendOperation(Operation.Division);
if (expression != null) {
setState(() {
pushExpression(expression);
});
}
}
void handleEqualsTap() {
final CalcExpression resultExpression = _expression.computeResult();
if (resultExpression != null) {
setState(() {
setResult(resultExpression);
});
}
}
void handleDelTap() {
setState(() {
popCalcExpression();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).canvasColor,
elevation: 0.0,
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
// Give the key-pad 3/5 of the vertical space and the display 2/5.
Expanded(
flex: 2,
child: CalcDisplay(content: _expression.toString()),
),
const Divider(height: 1.0),
Expanded(
flex: 3,
child: KeyPad(calcState: this),
),
],
),
);
}
}
class CalcDisplay extends StatelessWidget {
const CalcDisplay({ this.content });
final String content;
@override
Widget build(BuildContext context) {
return Center(
child: Text(
content,
style: const TextStyle(fontSize: 24.0),
),
);
}
}
class KeyPad extends StatelessWidget {
const KeyPad({ this.calcState });
final _CalculatorState calcState;
@override
Widget build(BuildContext context) {
final ThemeData themeData = ThemeData(
primarySwatch: Colors.purple,
brightness: Brightness.dark,
platform: Theme.of(context).platform,
);
return Theme(
data: themeData,
child: Material(
child: Row(
children: <Widget>[
Expanded(
// We set flex equal to the number of columns so that the main keypad
// and the op keypad have sizes proportional to their number of
// columns.
flex: 3,
child: Column(
children: <Widget>[
KeyRow(<Widget>[
NumberKey(7, calcState),
NumberKey(8, calcState),
NumberKey(9, calcState),
]),
KeyRow(<Widget>[
NumberKey(4, calcState),
NumberKey(5, calcState),
NumberKey(6, calcState),
]),
KeyRow(<Widget>[
NumberKey(1, calcState),
NumberKey(2, calcState),
NumberKey(3, calcState),
]),
KeyRow(<Widget>[
CalcKey('.', calcState.handlePointTap),
NumberKey(0, calcState),
CalcKey('=', calcState.handleEqualsTap),
]),
],
),
),
Expanded(
child: Material(
color: themeData.backgroundColor,
child: Column(
children: <Widget>[
CalcKey('\u232B', calcState.handleDelTap),
CalcKey('\u00F7', calcState.handleDivTap),
CalcKey('\u00D7', calcState.handleMultTap),
CalcKey('-', calcState.handleMinusTap),
CalcKey('+', calcState.handlePlusTap),
],
),
),
),
],
),
),
);
}
}
class KeyRow extends StatelessWidget {
const KeyRow(this.keys);
final List<Widget> keys;
@override
Widget build(BuildContext context) {
return Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: keys,
),
);
}
}
class CalcKey extends StatelessWidget {
const CalcKey(this.text, this.onTap);
final String text;
final GestureTapCallback onTap;
@override
Widget build(BuildContext context) {
final Orientation orientation = MediaQuery.of(context).orientation;
return Expanded(
child: InkResponse(
onTap: onTap,
child: Center(
child: Text(
text,
style: TextStyle(
fontSize: (orientation == Orientation.portrait) ? 32.0 : 24.0
),
),
),
),
);
}
}
class NumberKey extends CalcKey {
NumberKey(int value, _CalculatorState calcState)
: super('$value', () {
calcState.handleNumberTap(value);
});
}

@ -1,343 +0,0 @@
// Copyright 2016 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.
/// A token that composes an expression. There are several kinds of tokens
/// that represent arithmetic operation symbols, numbers and pieces of numbers.
/// We need to represent pieces of numbers because the user may have only
/// entered a partial expression so far.
class ExpressionToken {
ExpressionToken(this.stringRep);
final String stringRep;
@override
String toString() => stringRep;
}
/// A token that represents a number.
class NumberToken extends ExpressionToken {
NumberToken(String stringRep, this.number) : super(stringRep);
NumberToken.fromNumber(num number) : this('$number', number);
final num number;
}
/// A token that represents an integer.
class IntToken extends NumberToken {
IntToken(String stringRep) : super(stringRep, int.parse(stringRep));
}
/// A token that represents a floating point number.
class FloatToken extends NumberToken {
FloatToken(String stringRep) : super(stringRep, _parse(stringRep));
static double _parse(String stringRep) {
String toParse = stringRep;
if (toParse.startsWith('.'))
toParse = '0' + toParse;
if (toParse.endsWith('.'))
toParse = toParse + '0';
return double.parse(toParse);
}
}
/// A token that represents a number that is the result of a computation.
class ResultToken extends NumberToken {
ResultToken(num number) : super.fromNumber(round(number));
/// rounds `number` to 14 digits of precision. A double precision
/// floating point number is guaranteed to have at least this many
/// decimal digits of precision.
static num round(num number) {
if (number is int)
return number;
return double.parse(number.toStringAsPrecision(14));
}
}
/// A token that represents the unary minus prefix.
class LeadingNegToken extends ExpressionToken {
LeadingNegToken() : super('-');
}
enum Operation { Addition, Subtraction, Multiplication, Division }
/// A token that represents an arithmetic operation symbol.
class OperationToken extends ExpressionToken {
OperationToken(this.operation)
: super(opString(operation));
Operation operation;
static String opString(Operation operation) {
switch (operation) {
case Operation.Addition:
return ' + ';
case Operation.Subtraction:
return ' - ';
case Operation.Multiplication:
return ' \u00D7 ';
case Operation.Division:
return ' \u00F7 ';
}
assert(operation != null);
return null;
}
}
/// As the user taps different keys the current expression can be in one
/// of several states.
enum ExpressionState {
/// The expression is empty or an operation symbol was just entered.
/// A new number must be started now.
Start,
/// A minus sign was entered as a leading negative prefix.
LeadingNeg,
/// We are in the midst of a number without a point.
Number,
/// A point was just entered.
Point,
/// We are in the midst of a number with a point.
NumberWithPoint,
/// A result is being displayed
Result,
}
/// An expression that can be displayed in a calculator. It is the result
/// of a sequence of user entries. It is represented by a sequence of tokens.
///
/// The tokens are not in one to one correspondence with the key taps because we
/// use one token per number, not one token per digit. A [CalcExpression] is
/// immutable. The `append*` methods return a new [CalcExpression] that
/// represents the appropriate expression when one additional key tap occurs.
class CalcExpression {
CalcExpression(this._list, this.state);
CalcExpression.empty()
: this(<ExpressionToken>[], ExpressionState.Start);
CalcExpression.result(FloatToken result)
: _list = <ExpressionToken>[],
state = ExpressionState.Result {
_list.add(result);
}
/// The tokens comprising the expression.
final List<ExpressionToken> _list;
/// The state of the expression.
final ExpressionState state;
/// The string representation of the expression. This will be displayed
/// in the calculator's display panel.
@override
String toString() {
final StringBuffer buffer = StringBuffer('');
buffer.writeAll(_list);
return buffer.toString();
}
/// Append a digit to the current expression and return a new expression
/// representing the result. Returns null to indicate that it is not legal
/// to append a digit in the current state.
CalcExpression appendDigit(int digit) {
ExpressionState newState = ExpressionState.Number;
ExpressionToken newToken;
final List<ExpressionToken> outList = _list.toList();
switch (state) {
case ExpressionState.Start:
// Start a new number with digit.
newToken = IntToken('$digit');
break;
case ExpressionState.LeadingNeg:
// Replace the leading neg with a negative number starting with digit.
outList.removeLast();
newToken = IntToken('-$digit');
break;
case ExpressionState.Number:
final ExpressionToken last = outList.removeLast();
newToken = IntToken('${last.stringRep}$digit');
break;
case ExpressionState.Point:
case ExpressionState.NumberWithPoint:
final ExpressionToken last = outList.removeLast();
newState = ExpressionState.NumberWithPoint;
newToken = FloatToken('${last.stringRep}$digit');
break;
case ExpressionState.Result:
// Cannot enter a number now
return null;
}
outList.add(newToken);
return CalcExpression(outList, newState);
}
/// Append a point to the current expression and return a new expression
/// representing the result. Returns null to indicate that it is not legal
/// to append a point in the current state.
CalcExpression appendPoint() {
ExpressionToken newToken;
final List<ExpressionToken> outList = _list.toList();
switch (state) {
case ExpressionState.Start:
newToken = FloatToken('.');
break;
case ExpressionState.LeadingNeg:
case ExpressionState.Number:
final ExpressionToken last = outList.removeLast();
newToken = FloatToken(last.stringRep + '.');
break;
case ExpressionState.Point:
case ExpressionState.NumberWithPoint:
case ExpressionState.Result:
// Cannot enter a point now
return null;
}
outList.add(newToken);
return CalcExpression(outList, ExpressionState.Point);
}
/// Append an operation symbol to the current expression and return a new
/// expression representing the result. Returns null to indicate that it is not
/// legal to append an operation symbol in the current state.
CalcExpression appendOperation(Operation op) {
switch (state) {
case ExpressionState.Start:
case ExpressionState.LeadingNeg:
case ExpressionState.Point:
// Cannot enter operation now.
return null;
case ExpressionState.Number:
case ExpressionState.NumberWithPoint:
case ExpressionState.Result:
break;
}
final List<ExpressionToken> outList = _list.toList();
outList.add(OperationToken(op));
return CalcExpression(outList, ExpressionState.Start);
}
/// Append a leading minus sign to the current expression and return a new
/// expression representing the result. Returns null to indicate that it is not
/// legal to append a leading minus sign in the current state.
CalcExpression appendLeadingNeg() {
switch (state) {
case ExpressionState.Start:
break;
case ExpressionState.LeadingNeg:
case ExpressionState.Point:
case ExpressionState.Number:
case ExpressionState.NumberWithPoint:
case ExpressionState.Result:
// Cannot enter leading neg now.
return null;
}
final List<ExpressionToken> outList = _list.toList();
outList.add(LeadingNegToken());
return CalcExpression(outList, ExpressionState.LeadingNeg);
}
/// Append a minus sign to the current expression and return a new expression
/// representing the result. Returns null to indicate that it is not legal
/// to append a minus sign in the current state. Depending on the current
/// state the minus sign will be interpreted as either a leading negative
/// sign or a subtraction operation.
CalcExpression appendMinus() {
switch (state) {
case ExpressionState.Start:
return appendLeadingNeg();
case ExpressionState.LeadingNeg:
case ExpressionState.Point:
case ExpressionState.Number:
case ExpressionState.NumberWithPoint:
case ExpressionState.Result:
return appendOperation(Operation.Subtraction);
default:
return null;
}
}
/// Computes the result of the current expression and returns a new
/// ResultExpression containing the result. Returns null to indicate that
/// it is not legal to compute a result in the current state.
CalcExpression computeResult() {
switch (state) {
case ExpressionState.Start:
case ExpressionState.LeadingNeg:
case ExpressionState.Point:
case ExpressionState.Result:
// Cannot compute result now.
return null;
case ExpressionState.Number:
case ExpressionState.NumberWithPoint:
break;
}
// We make a copy of _list because CalcExpressions are supposed to
// be immutable.
final List<ExpressionToken> list = _list.toList();
// We obey order-of-operations by computing the sum of the 'terms',
// where a "term" is defined to be a sequence of numbers separated by
// multiplication or division symbols.
num currentTermValue = removeNextTerm(list);
while (list.isNotEmpty) {
final OperationToken opToken = list.removeAt(0);
final num nextTermValue = removeNextTerm(list);
switch (opToken.operation) {
case Operation.Addition:
currentTermValue += nextTermValue;
break;
case Operation.Subtraction:
currentTermValue -= nextTermValue;
break;
case Operation.Multiplication:
case Operation.Division:
// Logic error.
assert(false);
}
}
final List<ExpressionToken> outList = <ExpressionToken>[
ResultToken(currentTermValue),
];
return CalcExpression(outList, ExpressionState.Result);
}
/// Removes the next "term" from `list` and returns its numeric value.
/// A "term" is a sequence of number tokens separated by multiplication
/// and division symbols.
static num removeNextTerm(List<ExpressionToken> list) {
assert(list != null && list.isNotEmpty);
final NumberToken firstNumToken = list.removeAt(0);
num currentValue = firstNumToken.number;
while (list.isNotEmpty) {
bool isDivision = false;
final OperationToken nextOpToken = list.first;
switch (nextOpToken.operation) {
case Operation.Addition:
case Operation.Subtraction:
// We have reached the end of the current term
return currentValue;
case Operation.Multiplication:
break;
case Operation.Division:
isDivision = true;
}
// Remove the operation token.
list.removeAt(0);
// Remove the next number token.
final NumberToken nextNumToken = list.removeAt(0);
final num nextNumber = nextNumToken.number;
if (isDivision)
currentValue /= nextNumber;
else
currentValue *= nextNumber;
}
return currentValue;
}
}

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

@ -1,149 +0,0 @@
// Copyright 2016 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/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);
return Scrollbar(
child: ListView(
itemExtent: kColorItemHeight,
children: <Widget>[
...primaryKeys.map<Widget>((int index) {
return DefaultTextStyle(
style: index > colors.threshold ? whiteTextStyle : blackTextStyle,
child: ColorItem(index: index, color: colors.primary[index]),
);
}),
if (colors.accent != null)
...accentKeys.map<Widget>((int index) {
return DefaultTextStyle(
style: index > colors.threshold ? whiteTextStyle : blackTextStyle,
child: ColorItem(index: index, color: colors.accent[index], prefix: 'A'),
);
}),
],
),
);
}
}
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(),
),
),
);
}
}

@ -1,345 +0,0 @@
// Copyright 2015 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/material.dart';
import 'package:flutter/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 {
const _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);
return MergeSemantics(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
...lines.sublist(0, lines.length - 1).map<Widget>((String line) => Text(line)),
Text(lines.last, style: themeData.textTheme.caption),
],
),
),
if (icon != null)
SizedBox(
width: 72.0,
child: IconButton(
icon: Icon(icon),
color: themeData.primaryColor,
onPressed: onPressed,
),
),
],
),
),
);
}
}
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',
],
),
],
),
]),
),
],
),
),
);
}
}

@ -1,14 +0,0 @@
// Copyright 2017 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 'cupertino_activity_indicator_demo.dart';
export 'cupertino_alert_demo.dart';
export 'cupertino_buttons_demo.dart';
export 'cupertino_navigation_demo.dart';
export 'cupertino_picker_demo.dart';
export 'cupertino_refresh_demo.dart';
export 'cupertino_segmented_control_demo.dart';
export 'cupertino_slider_demo.dart';
export 'cupertino_switch_demo.dart';
export 'cupertino_text_field_demo.dart';

@ -1,28 +0,0 @@
// Copyright 2017 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/cupertino.dart';
import '../../gallery/demo.dart';
class CupertinoProgressIndicatorDemo extends StatelessWidget {
static const String routeName = '/cupertino/progress_indicator';
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
// We're specifying a back label here because the previous page is a
// Material page. CupertinoPageRoutes could auto-populate these back
// labels.
previousPageTitle: 'Cupertino',
middle: const Text('Activity Indicator'),
trailing: CupertinoDemoDocumentationButton(routeName),
),
child: const Center(
child: CupertinoActivityIndicator(),
),
);
}
}

@ -1,262 +0,0 @@
// Copyright 2016 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/cupertino.dart';
import '../../gallery/demo.dart';
class CupertinoAlertDemo extends StatefulWidget {
static const String routeName = '/cupertino/alert';
@override
_CupertinoAlertDemoState createState() => _CupertinoAlertDemoState();
}
class _CupertinoAlertDemoState extends State<CupertinoAlertDemo> {
String lastSelectedValue;
void showDemoDialog({BuildContext context, Widget child}) {
showCupertinoDialog<String>(
context: context,
builder: (BuildContext context) => child,
).then((String value) {
if (value != null) {
setState(() { lastSelectedValue = value; });
}
});
}
void showDemoActionSheet({BuildContext context, Widget child}) {
showCupertinoModalPopup<String>(
context: context,
builder: (BuildContext context) => child,
).then((String value) {
if (value != null) {
setState(() { lastSelectedValue = value; });
}
});
}
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: const Text('Alerts'),
// We're specifying a back label here because the previous page is a
// Material page. CupertinoPageRoutes could auto-populate these back
// labels.
previousPageTitle: 'Cupertino',
trailing: CupertinoDemoDocumentationButton(CupertinoAlertDemo.routeName),
),
child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle,
child: Builder(
builder: (BuildContext context) {
return Stack(
alignment: Alignment.center,
children: <Widget>[
CupertinoScrollbar(
child: ListView(
// Add more padding to the normal safe area.
padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 72.0)
+ MediaQuery.of(context).padding,
children: <Widget>[
CupertinoButton.filled(
child: const Text('Alert'),
onPressed: () => _onAlertPress(context),
),
const Padding(padding: EdgeInsets.all(8.0)),
CupertinoButton.filled(
child: const Text('Alert with Title'),
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 36.0),
onPressed: () => _onAlertWithTitlePress(context),
),
const Padding(padding: EdgeInsets.all(8.0)),
CupertinoButton.filled(
child: const Text('Alert with Buttons'),
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 36.0),
onPressed: () => _onAlertWithButtonsPress(context),
),
const Padding(padding: EdgeInsets.all(8.0)),
CupertinoButton.filled(
child: const Text('Alert Buttons Only'),
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 36.0),
onPressed: () {
showDemoDialog(
context: context,
child: const CupertinoDessertDialog(),
);
},
),
const Padding(padding: EdgeInsets.all(8.0)),
CupertinoButton.filled(
child: const Text('Action Sheet'),
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 36.0),
onPressed: () => _onActionSheetPress(context),
),
],
),
),
if (lastSelectedValue != null)
Positioned(
bottom: 32.0,
child: Text('You selected: $lastSelectedValue'),
),
],
);
},
),
),
);
}
void _onAlertPress(BuildContext context) {
showDemoDialog(
context: context,
child: CupertinoAlertDialog(
title: const Text('Discard draft?'),
actions: <Widget>[
CupertinoDialogAction(
child: const Text('Discard'),
isDestructiveAction: true,
onPressed: () => Navigator.pop(context, 'Discard'),
),
CupertinoDialogAction(
child: const Text('Cancel'),
isDefaultAction: true,
onPressed: () => Navigator.pop(context, 'Cancel'),
),
],
),
);
}
void _onAlertWithTitlePress(BuildContext context) {
showDemoDialog(
context: context,
child: CupertinoAlertDialog(
title: const Text('Allow "Maps" to access your location while you are using the app?'),
content: const Text('Your current location will be displayed on the map and used '
'for directions, nearby search results, and estimated travel times.'),
actions: <Widget>[
CupertinoDialogAction(
child: const Text('Don\'t Allow'),
onPressed: () => Navigator.pop(context, 'Disallow'),
),
CupertinoDialogAction(
child: const Text('Allow'),
onPressed: () => Navigator.pop(context, 'Allow'),
),
],
),
);
}
void _onAlertWithButtonsPress(BuildContext context) {
showDemoDialog(
context: context,
child: const CupertinoDessertDialog(
title: Text('Select Favorite Dessert'),
content: Text('Please select your favorite type of dessert from the '
'list below. Your selection will be used to customize the suggested '
'list of eateries in your area.'),
),
);
}
void _onActionSheetPress(BuildContext context) {
showDemoActionSheet(
context: context,
child: CupertinoActionSheet(
title: const Text('Favorite Dessert'),
message: const Text('Please select the best dessert from the options below.'),
actions: <Widget>[
CupertinoActionSheetAction(
child: const Text('Profiteroles'),
onPressed: () => Navigator.pop(context, 'Profiteroles'),
),
CupertinoActionSheetAction(
child: const Text('Cannolis'),
onPressed: () => Navigator.pop(context, 'Cannolis'),
),
CupertinoActionSheetAction(
child: const Text('Trifle'),
onPressed: () => Navigator.pop(context, 'Trifle'),
),
],
cancelButton: CupertinoActionSheetAction(
child: const Text('Cancel'),
isDefaultAction: true,
onPressed: () => Navigator.pop(context, 'Cancel'),
),
),
);
}
}
class CupertinoDessertDialog extends StatelessWidget {
const CupertinoDessertDialog({Key key, this.title, this.content}) : super(key: key);
final Widget title;
final Widget content;
@override
Widget build(BuildContext context) {
return CupertinoAlertDialog(
title: title,
content: content,
actions: <Widget>[
CupertinoDialogAction(
child: const Text('Cheesecake'),
onPressed: () {
Navigator.pop(context, 'Cheesecake');
},
),
CupertinoDialogAction(
child: const Text('Tiramisu'),
onPressed: () {
Navigator.pop(context, 'Tiramisu');
},
),
CupertinoDialogAction(
child: const Text('Apple Pie'),
onPressed: () {
Navigator.pop(context, 'Apple Pie');
},
),
CupertinoDialogAction(
child: const Text("Devil's food cake"),
onPressed: () {
Navigator.pop(context, "Devil's food cake");
},
),
CupertinoDialogAction(
child: const Text('Banana Split'),
onPressed: () {
Navigator.pop(context, 'Banana Split');
},
),
CupertinoDialogAction(
child: const Text('Oatmeal Cookie'),
onPressed: () {
Navigator.pop(context, 'Oatmeal Cookies');
},
),
CupertinoDialogAction(
child: const Text('Chocolate Brownie'),
onPressed: () {
Navigator.pop(context, 'Chocolate Brownies');
},
),
CupertinoDialogAction(
child: const Text('Cancel'),
isDestructiveAction: true,
onPressed: () {
Navigator.pop(context, 'Cancel');
},
),
],
);
}
}

@ -1,89 +0,0 @@
// Copyright 2017 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/cupertino.dart';
import '../../gallery/demo.dart';
class CupertinoButtonsDemo extends StatefulWidget {
static const String routeName = '/cupertino/buttons';
@override
_CupertinoButtonDemoState createState() => _CupertinoButtonDemoState();
}
class _CupertinoButtonDemoState extends State<CupertinoButtonsDemo> {
int _pressedCount = 0;
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: const Text('Buttons'),
// We're specifying a back label here because the previous page is a
// Material page. CupertinoPageRoutes could auto-populate these back
// labels.
previousPageTitle: 'Cupertino',
trailing: CupertinoDemoDocumentationButton(CupertinoButtonsDemo.routeName),
),
child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle,
child: SafeArea(
child: Column(
children: <Widget>[
const Padding(
padding: EdgeInsets.all(16.0),
child: Text(
'iOS themed buttons are flat. They can have borders or backgrounds but '
'only when necessary.'
),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget> [
Text(_pressedCount > 0
? 'Button pressed $_pressedCount time${_pressedCount == 1 ? "" : "s"}'
: ' '),
const Padding(padding: EdgeInsets.all(12.0)),
Align(
alignment: const Alignment(0.0, -0.2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
CupertinoButton(
child: const Text('Cupertino Button'),
onPressed: () {
setState(() { _pressedCount += 1; });
},
),
const CupertinoButton(
child: Text('Disabled'),
onPressed: null,
),
],
),
),
const Padding(padding: EdgeInsets.all(12.0)),
CupertinoButton.filled(
child: const Text('With Background'),
onPressed: () {
setState(() { _pressedCount += 1; });
},
),
const Padding(padding: EdgeInsets.all(12.0)),
const CupertinoButton.filled(
child: Text('Disabled'),
onPressed: null,
),
],
),
),
],
),
),
),
);
}
}

@ -1,812 +0,0 @@
// Copyright 2017 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 'dart:math' as math;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
const List<Color> coolColors = <Color>[
Color.fromARGB(255, 255, 59, 48),
Color.fromARGB(255, 255, 149, 0),
Color.fromARGB(255, 255, 204, 0),
Color.fromARGB(255, 76, 217, 100),
Color.fromARGB(255, 90, 200, 250),
Color.fromARGB(255, 0, 122, 255),
Color.fromARGB(255, 88, 86, 214),
Color.fromARGB(255, 255, 45, 85),
];
const List<String> coolColorNames = <String>[
'Sarcoline', 'Coquelicot', 'Smaragdine', 'Mikado', 'Glaucous', 'Wenge',
'Fulvous', 'Xanadu', 'Falu', 'Eburnean', 'Amaranth', 'Australien',
'Banan', 'Falu', 'Gingerline', 'Incarnadine', 'Labrador', 'Nattier',
'Pervenche', 'Sinoper', 'Verditer', 'Watchet', 'Zaffre',
];
const int _kChildCount = 50;
class CupertinoNavigationDemo extends StatelessWidget {
CupertinoNavigationDemo()
: colorItems = List<Color>.generate(_kChildCount, (int index) {
return coolColors[math.Random().nextInt(coolColors.length)];
}) ,
colorNameItems = List<String>.generate(_kChildCount, (int index) {
return coolColorNames[math.Random().nextInt(coolColorNames.length)];
});
static const String routeName = '/cupertino/navigation';
final List<Color> colorItems;
final List<String> colorNameItems;
@override
Widget build(BuildContext context) {
return WillPopScope(
// Prevent swipe popping of this page. Use explicit exit buttons only.
onWillPop: () => Future<bool>.value(true),
child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle,
child: CupertinoTabScaffold(
tabBar: CupertinoTabBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.home),
title: Text('Home'),
),
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.conversation_bubble),
title: Text('Support'),
),
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.profile_circled),
title: Text('Profile'),
),
],
),
tabBuilder: (BuildContext context, int index) {
assert(index >= 0 && index <= 2);
switch (index) {
case 0:
return CupertinoTabView(
builder: (BuildContext context) {
return CupertinoDemoTab1(
colorItems: colorItems,
colorNameItems: colorNameItems,
);
},
defaultTitle: 'Colors',
);
break;
case 1:
return CupertinoTabView(
builder: (BuildContext context) => CupertinoDemoTab2(),
defaultTitle: 'Support Chat',
);
break;
case 2:
return CupertinoTabView(
builder: (BuildContext context) => CupertinoDemoTab3(),
defaultTitle: 'Account',
);
break;
}
return null;
},
),
),
);
}
}
class ExitButton extends StatelessWidget {
const ExitButton();
@override
Widget build(BuildContext context) {
return CupertinoButton(
padding: EdgeInsets.zero,
child: const Tooltip(
message: 'Back',
child: Text('Exit'),
excludeFromSemantics: true,
),
onPressed: () {
// The demo is on the root navigator.
Navigator.of(context, rootNavigator: true).pop();
},
);
}
}
final Widget trailingButtons = Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
CupertinoDemoDocumentationButton(CupertinoNavigationDemo.routeName),
const Padding(padding: EdgeInsets.only(left: 8.0)),
const ExitButton(),
],
);
class CupertinoDemoTab1 extends StatelessWidget {
const CupertinoDemoTab1({this.colorItems, this.colorNameItems});
final List<Color> colorItems;
final List<String> colorNameItems;
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
backgroundColor: CupertinoColors.systemGroupedBackground,
child: CustomScrollView(
semanticChildCount: _kChildCount,
slivers: <Widget>[
CupertinoSliverNavigationBar(
trailing: trailingButtons,
),
SliverPadding(
// Top media padding consumed by CupertinoSliverNavigationBar.
// Left/Right media padding consumed by Tab1RowItem.
padding: MediaQuery.of(context).removePadding(
removeTop: true,
removeLeft: true,
removeRight: true,
).padding,
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Tab1RowItem(
index: index,
lastItem: index == _kChildCount - 1,
color: colorItems[index],
colorName: colorNameItems[index],
);
},
childCount: _kChildCount,
),
),
),
],
),
);
}
}
class Tab1RowItem extends StatelessWidget {
const Tab1RowItem({this.index, this.lastItem, this.color, this.colorName});
final int index;
final bool lastItem;
final Color color;
final String colorName;
@override
Widget build(BuildContext context) {
final Widget row = GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Navigator.of(context).push(CupertinoPageRoute<void>(
title: colorName,
builder: (BuildContext context) => Tab1ItemPage(
color: color,
colorName: colorName,
index: index,
),
));
},
child: Container(
color: CupertinoDynamicColor.resolve(CupertinoColors.systemBackground, context),
child: SafeArea(
top: false,
bottom: false,
child: Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8.0, bottom: 8.0, right: 8.0),
child: Row(
children: <Widget>[
Container(
height: 60.0,
width: 60.0,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(8.0),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(colorName),
const Padding(padding: EdgeInsets.only(top: 8.0)),
Text(
'Buy this cool color',
style: TextStyle(
color: CupertinoDynamicColor.resolve(CupertinoColors.secondaryLabel, context),
fontSize: 13.0,
fontWeight: FontWeight.w300,
),
),
],
),
),
),
CupertinoButton(
padding: EdgeInsets.zero,
child: const Icon(CupertinoIcons.plus_circled,
semanticLabel: 'Add',
),
onPressed: () { },
),
CupertinoButton(
padding: EdgeInsets.zero,
child: const Icon(CupertinoIcons.share,
semanticLabel: 'Share',
),
onPressed: () { },
),
],
),
),
),
),
);
if (lastItem) {
return row;
}
return Column(
children: <Widget>[
row,
Container(
height: 1.0,
color: CupertinoDynamicColor.resolve(CupertinoColors.separator, context),
),
],
);
}
}
class Tab1ItemPage extends StatefulWidget {
const Tab1ItemPage({this.color, this.colorName, this.index});
final Color color;
final String colorName;
final int index;
@override
State<StatefulWidget> createState() => Tab1ItemPageState();
}
class Tab1ItemPageState extends State<Tab1ItemPage> {
@override
void initState() {
super.initState();
relatedColors = List<Color>.generate(10, (int index) {
final math.Random random = math.Random();
return Color.fromARGB(
255,
(widget.color.red + random.nextInt(100) - 50).clamp(0, 255),
(widget.color.green + random.nextInt(100) - 50).clamp(0, 255),
(widget.color.blue + random.nextInt(100) - 50).clamp(0, 255),
);
});
}
List<Color> relatedColors;
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
trailing: ExitButton(),
),
child: SafeArea(
top: false,
bottom: false,
child: ListView(
children: <Widget>[
const Padding(padding: EdgeInsets.only(top: 16.0)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Container(
height: 128.0,
width: 128.0,
decoration: BoxDecoration(
color: widget.color,
borderRadius: BorderRadius.circular(24.0),
),
),
const Padding(padding: EdgeInsets.only(left: 18.0)),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
widget.colorName,
style: const TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold),
),
const Padding(padding: EdgeInsets.only(top: 6.0)),
Text(
'Item number ${widget.index}',
style: TextStyle(
color: CupertinoDynamicColor.resolve(CupertinoColors.secondaryLabel, context),
fontSize: 16.0,
fontWeight: FontWeight.w100,
),
),
const Padding(padding: EdgeInsets.only(top: 20.0)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
CupertinoButton.filled(
minSize: 30.0,
padding: const EdgeInsets.symmetric(horizontal: 24.0),
borderRadius: BorderRadius.circular(32.0),
child: const Text(
'GET',
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.w700,
letterSpacing: -0.28,
),
),
onPressed: () { },
),
CupertinoButton.filled(
minSize: 30.0,
padding: EdgeInsets.zero,
borderRadius: BorderRadius.circular(32.0),
child: const Icon(CupertinoIcons.ellipsis),
onPressed: () { },
),
],
),
],
),
),
],
),
),
const Padding(
padding: EdgeInsets.only(left: 16.0, top: 28.0, bottom: 8.0),
child: Text(
'USERS ALSO LIKED',
style: TextStyle(
color: Color(0xFF646464),
letterSpacing: -0.60,
fontSize: 15.0,
fontWeight: FontWeight.w500,
),
),
),
SizedBox(
height: 200.0,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 10,
itemExtent: 160.0,
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.0),
color: relatedColors[index],
),
child: Center(
child: CupertinoButton(
child: const Icon(
CupertinoIcons.plus_circled,
color: CupertinoColors.white,
size: 36.0,
),
onPressed: () { },
),
),
),
);
},
),
),
],
),
),
);
}
}
class CupertinoDemoTab2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
trailing: trailingButtons,
),
child: CupertinoScrollbar(
child: ListView(
children: <Widget>[
CupertinoUserInterfaceLevel(
data: CupertinoUserInterfaceLevelData.elevated,
child: Tab2Header(),
),
...buildTab2Conversation(),
],
),
),
);
}
}
class Tab2Header extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: SafeArea(
top: false,
bottom: false,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
decoration: BoxDecoration(
color: CupertinoDynamicColor.resolve(CupertinoColors.systemFill, context),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
'SUPPORT TICKET',
style: TextStyle(
color: CupertinoDynamicColor.resolve(CupertinoColors.secondaryLabel, context),
letterSpacing: -0.9,
fontSize: 14.0,
fontWeight: FontWeight.w500,
),
),
Text(
'Show More',
style: TextStyle(
color: CupertinoDynamicColor.resolve(CupertinoColors.secondaryLabel, context),
letterSpacing: -0.6,
fontSize: 12.0,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
Container(
decoration: BoxDecoration(
color: CupertinoDynamicColor.resolve(CupertinoColors.quaternarySystemFill, context),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Text(
'Product or product packaging damaged during transit',
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.w700,
letterSpacing: -0.46,
),
),
const Padding(padding: EdgeInsets.only(top: 16.0)),
const Text(
'REVIEWERS',
style: TextStyle(
color: Color(0xFF646464),
fontSize: 12.0,
letterSpacing: -0.6,
fontWeight: FontWeight.w500,
),
),
const Padding(padding: EdgeInsets.only(top: 8.0)),
Row(
children: <Widget>[
Container(
width: 44.0,
height: 44.0,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage(
'people/square/trevor.png',
package: _kGalleryAssetsPackage,
),
),
shape: BoxShape.circle,
),
),
const Padding(padding: EdgeInsets.only(left: 8.0)),
Container(
width: 44.0,
height: 44.0,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage(
'people/square/sandra.png',
package: _kGalleryAssetsPackage,
),
),
shape: BoxShape.circle,
),
),
const Padding(padding: EdgeInsets.only(left: 2.0)),
const Icon(
CupertinoIcons.check_mark_circled,
color: Color(0xFF646464),
size: 20.0,
),
],
),
],
),
),
),
],
),
),
),
);
}
}
enum Tab2ConversationBubbleColor {
blue,
gray,
}
class Tab2ConversationBubble extends StatelessWidget {
const Tab2ConversationBubble({this.text, this.color});
final String text;
final Tab2ConversationBubbleColor color;
@override
Widget build(BuildContext context) {
Color backgroundColor;
Color foregroundColor;
switch (color) {
case Tab2ConversationBubbleColor.gray:
backgroundColor = CupertinoDynamicColor.resolve(CupertinoColors.systemFill, context);
foregroundColor = CupertinoDynamicColor.resolve(CupertinoColors.label, context);
break;
case Tab2ConversationBubbleColor.blue:
backgroundColor = CupertinoTheme.of(context).primaryColor;
foregroundColor = CupertinoColors.white;
break;
}
return Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(18.0)),
color: backgroundColor,
),
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
padding: const EdgeInsets.symmetric(horizontal: 14.0, vertical: 10.0),
child: Text(
text,
style: TextStyle(
color: foregroundColor,
letterSpacing: -0.4,
fontSize: 15.0,
fontWeight: FontWeight.w400,
),
),
);
}
}
class Tab2ConversationAvatar extends StatelessWidget {
const Tab2ConversationAvatar({this.text, this.color});
final String text;
final Color color;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
begin: FractionalOffset.topCenter,
end: FractionalOffset.bottomCenter,
colors: <Color>[
color,
Color.fromARGB(
color.alpha,
(color.red - 60).clamp(0, 255),
(color.green - 60).clamp(0, 255),
(color.blue - 60).clamp(0, 255),
),
],
),
),
margin: const EdgeInsets.only(left: 8.0, bottom: 8.0),
padding: const EdgeInsets.all(12.0),
child: Text(
text,
style: const TextStyle(
color: CupertinoColors.white,
fontSize: 13.0,
fontWeight: FontWeight.w500,
),
),
);
}
}
class Tab2ConversationRow extends StatelessWidget {
const Tab2ConversationRow({this.avatar, this.text});
final Tab2ConversationAvatar avatar;
final String text;
@override
Widget build(BuildContext context) {
final bool isSelf = avatar == null;
return SafeArea(
child: Row(
mainAxisAlignment: isSelf ? MainAxisAlignment.end : MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: isSelf ? CrossAxisAlignment.center : CrossAxisAlignment.end,
children: <Widget>[
if (avatar != null) avatar,
CupertinoUserInterfaceLevel(
data: CupertinoUserInterfaceLevelData.elevated,
child: Tab2ConversationBubble(
text: text,
color: isSelf
? Tab2ConversationBubbleColor.blue
: Tab2ConversationBubbleColor.gray,
),
),
],
),
);
}
}
List<Widget> buildTab2Conversation() {
return <Widget>[
const Tab2ConversationRow(
text: "My Xanadu doesn't look right",
),
const Tab2ConversationRow(
avatar: Tab2ConversationAvatar(
text: 'KL',
color: Color(0xFFFD5015),
),
text: "We'll rush you a new one.\nIt's gonna be incredible",
),
const Tab2ConversationRow(
text: 'Awesome thanks!',
),
const Tab2ConversationRow(
avatar: Tab2ConversationAvatar(
text: 'SJ',
color: Color(0xFF34CAD6),
),
text: "We'll send you our\nnewest Labrador too!",
),
const Tab2ConversationRow(
text: 'Yay',
),
const Tab2ConversationRow(
avatar: Tab2ConversationAvatar(
text: 'KL',
color: Color(0xFFFD5015),
),
text: "Actually there's one more thing...",
),
const Tab2ConversationRow(
text: "What's that?",
),
];
}
class CupertinoDemoTab3 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(trailing: trailingButtons),
backgroundColor: CupertinoColors.systemBackground,
child: ListView(
children: <Widget>[
const Padding(padding: EdgeInsets.only(top: 32.0)),
GestureDetector(
onTap: () {
Navigator.of(context, rootNavigator: true).push(
CupertinoPageRoute<bool>(
fullscreenDialog: true,
builder: (BuildContext context) => Tab3Dialog(),
),
);
},
child: Container(
decoration: BoxDecoration(
color: CupertinoTheme.of(context).scaffoldBackgroundColor,
border: const Border(
top: BorderSide(color: Color(0xFFBCBBC1), width: 0.0),
bottom: BorderSide(color: Color(0xFFBCBBC1), width: 0.0),
),
),
height: 44.0,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: SafeArea(
top: false,
bottom: false,
child: Row(
children: <Widget>[
Text(
'Sign in',
style: TextStyle(color: CupertinoTheme.of(context).primaryColor),
),
],
),
),
),
),
),
],
),
);
}
}
class Tab3Dialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
leading: CupertinoButton(
child: const Text('Cancel'),
padding: EdgeInsets.zero,
onPressed: () {
Navigator.of(context).pop(false);
},
),
),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Icon(
CupertinoIcons.profile_circled,
size: 160.0,
color: Color(0xFF646464),
),
const Padding(padding: EdgeInsets.only(top: 18.0)),
CupertinoButton.filled(
child: const Text('Sign in'),
onPressed: () {
Navigator.pop(context);
},
),
],
),
),
);
}
}

@ -1,292 +0,0 @@
// Copyright 2017 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/cupertino.dart';
import 'package:intl/intl.dart';
import '../../gallery/demo.dart';
import 'cupertino_navigation_demo.dart' show coolColorNames;
const double _kPickerSheetHeight = 216.0;
const double _kPickerItemHeight = 32.0;
class CupertinoPickerDemo extends StatefulWidget {
static const String routeName = '/cupertino/picker';
@override
_CupertinoPickerDemoState createState() => _CupertinoPickerDemoState();
}
class _BottomPicker extends StatelessWidget {
const _BottomPicker({
Key key,
@required this.child,
}) : assert(child != null),
super(key: key);
final Widget child;
@override
Widget build(BuildContext context) {
return Container(
height: _kPickerSheetHeight,
padding: const EdgeInsets.only(top: 6.0),
color: CupertinoColors.systemBackground.resolveFrom(context),
child: DefaultTextStyle(
style: TextStyle(
color: CupertinoColors.label.resolveFrom(context),
fontSize: 22.0,
),
child: GestureDetector(
// Blocks taps from propagating to the modal sheet and popping.
onTap: () { },
child: SafeArea(
top: false,
child: child,
),
),
),
);
}
}
class _Menu extends StatelessWidget {
const _Menu({
Key key,
@required this.children,
}) : assert(children != null),
super(key: key);
final List<Widget> children;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: CupertinoTheme.of(context).scaffoldBackgroundColor,
border: const Border(
top: BorderSide(color: Color(0xFFBCBBC1), width: 0.0),
bottom: BorderSide(color: Color(0xFFBCBBC1), width: 0.0),
),
),
height: 44.0,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SafeArea(
top: false,
bottom: false,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: children,
),
),
),
);
}
}
class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
int _selectedColorIndex = 0;
Duration timer = const Duration();
// Value that is shown in the date picker in date mode.
DateTime date = DateTime.now();
// Value that is shown in the date picker in time mode.
DateTime time = DateTime.now();
// Value that is shown in the date picker in dateAndTime mode.
DateTime dateTime = DateTime.now();
Widget _buildColorPicker(BuildContext context) {
final FixedExtentScrollController scrollController =
FixedExtentScrollController(initialItem: _selectedColorIndex);
return GestureDetector(
onTap: () async {
await showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) {
return _BottomPicker(
child: CupertinoPicker(
scrollController: scrollController,
itemExtent: _kPickerItemHeight,
backgroundColor: CupertinoColors.systemBackground.resolveFrom(context),
onSelectedItemChanged: (int index) {
setState(() => _selectedColorIndex = index);
},
children: List<Widget>.generate(coolColorNames.length, (int index) {
return Center(
child: Text(coolColorNames[index]),
);
}),
),
);
},
);
},
child: _Menu(
children: <Widget>[
const Text('Favorite Color'),
Text(
coolColorNames[_selectedColorIndex],
style: TextStyle(color: CupertinoDynamicColor.resolve(CupertinoColors.inactiveGray, context)),
),
],
),
);
}
Widget _buildCountdownTimerPicker(BuildContext context) {
return GestureDetector(
onTap: () {
showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) {
return _BottomPicker(
child: CupertinoTimerPicker(
backgroundColor: CupertinoColors.systemBackground.resolveFrom(context),
initialTimerDuration: timer,
onTimerDurationChanged: (Duration newTimer) {
setState(() => timer = newTimer);
},
),
);
},
);
},
child: _Menu(
children: <Widget>[
const Text('Countdown Timer'),
Text(
'${timer.inHours}:'
'${(timer.inMinutes % 60).toString().padLeft(2,'0')}:'
'${(timer.inSeconds % 60).toString().padLeft(2,'0')}',
style: TextStyle(color: CupertinoDynamicColor.resolve(CupertinoColors.inactiveGray, context)),
),
],
),
);
}
Widget _buildDatePicker(BuildContext context) {
return GestureDetector(
onTap: () {
showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) {
return _BottomPicker(
child: CupertinoDatePicker(
backgroundColor: CupertinoColors.systemBackground.resolveFrom(context),
mode: CupertinoDatePickerMode.date,
initialDateTime: date,
onDateTimeChanged: (DateTime newDateTime) {
setState(() => date = newDateTime);
},
),
);
},
);
},
child: _Menu(
children: <Widget>[
const Text('Date'),
Text(
DateFormat.yMMMMd().format(date),
style: TextStyle(color: CupertinoDynamicColor.resolve(CupertinoColors.inactiveGray, context)),
),
]
),
);
}
Widget _buildTimePicker(BuildContext context) {
return GestureDetector(
onTap: () {
showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) {
return _BottomPicker(
child: CupertinoDatePicker(
backgroundColor: CupertinoColors.systemBackground.resolveFrom(context),
mode: CupertinoDatePickerMode.time,
initialDateTime: time,
onDateTimeChanged: (DateTime newDateTime) {
setState(() => time = newDateTime);
},
),
);
},
);
},
child: _Menu(
children: <Widget>[
const Text('Time'),
Text(
DateFormat.jm().format(time),
style: TextStyle(color: CupertinoDynamicColor.resolve(CupertinoColors.inactiveGray, context)),
),
],
),
);
}
Widget _buildDateAndTimePicker(BuildContext context) {
return GestureDetector(
onTap: () {
showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) {
return _BottomPicker(
child: CupertinoDatePicker(
backgroundColor: CupertinoColors.systemBackground.resolveFrom(context),
mode: CupertinoDatePickerMode.dateAndTime,
initialDateTime: dateTime,
onDateTimeChanged: (DateTime newDateTime) {
setState(() => dateTime = newDateTime);
},
),
);
},
);
},
child: _Menu(
children: <Widget>[
const Text('Date and Time'),
Text(
DateFormat.yMMMd().add_jm().format(dateTime),
style: TextStyle(color: CupertinoDynamicColor.resolve(CupertinoColors.inactiveGray, context)),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: const Text('Picker'),
// We're specifying a back label here because the previous page is a
// Material page. CupertinoPageRoutes could auto-populate these back
// labels.
previousPageTitle: 'Cupertino',
trailing: CupertinoDemoDocumentationButton(CupertinoPickerDemo.routeName),
),
child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle,
child: ListView(
children: <Widget>[
const Padding(padding: EdgeInsets.only(top: 32.0)),
_buildColorPicker(context),
_buildCountdownTimerPicker(context),
_buildDatePicker(context),
_buildTimePicker(context),
_buildDateAndTimePicker(context),
],
),
),
);
}
}

@ -1,238 +0,0 @@
// 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 'dart:math' show Random;
import 'package:flutter/cupertino.dart';
import '../../gallery/demo.dart';
class CupertinoRefreshControlDemo extends StatefulWidget {
static const String routeName = '/cupertino/refresh';
@override
_CupertinoRefreshControlDemoState createState() => _CupertinoRefreshControlDemoState();
}
class _CupertinoRefreshControlDemoState extends State<CupertinoRefreshControlDemo> {
List<List<String>> randomizedContacts;
@override
void initState() {
super.initState();
repopulateList();
}
void repopulateList() {
final Random random = Random();
randomizedContacts = List<List<String>>.generate(
100,
(int index) {
return contacts[random.nextInt(contacts.length)]
// Randomly adds a telephone icon next to the contact or not.
..add(random.nextBool().toString());
},
);
}
@override
Widget build(BuildContext context) {
return DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle,
child: CupertinoPageScaffold(
backgroundColor: CupertinoColors.systemGroupedBackground,
child: CustomScrollView(
// If left unspecified, the [CustomScrollView] appends an
// [AlwaysScrollableScrollPhysics]. Behind the scene, the ScrollableState
// will attach that [AlwaysScrollableScrollPhysics] to the output of
// [ScrollConfiguration.of] which will be a [ClampingScrollPhysics]
// on Android.
// To demonstrate the iOS behavior in this demo and to ensure that the list
// always scrolls, we specifically use a [BouncingScrollPhysics] combined
// with a [AlwaysScrollableScrollPhysics]
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
slivers: <Widget>[
CupertinoSliverNavigationBar(
largeTitle: const Text('Refresh'),
// We're specifying a back label here because the previous page
// is a Material page. CupertinoPageRoutes could auto-populate
// these back labels.
previousPageTitle: 'Cupertino',
trailing: CupertinoDemoDocumentationButton(CupertinoRefreshControlDemo.routeName),
),
CupertinoSliverRefreshControl(
onRefresh: () {
return Future<void>.delayed(const Duration(seconds: 2))
..then<void>((_) {
if (mounted) {
setState(() => repopulateList());
}
});
},
),
SliverSafeArea(
top: false, // Top safe area is consumed by the navigation bar.
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return _ListItem(
name: randomizedContacts[index][0],
place: randomizedContacts[index][1],
date: randomizedContacts[index][2],
called: randomizedContacts[index][3] == 'true',
);
},
childCount: 20,
),
),
),
],
),
),
);
}
}
List<List<String>> contacts = <List<String>>[
<String>['George Washington', 'Westmoreland County', ' 4/30/1789'],
<String>['John Adams', 'Braintree', ' 3/4/1797'],
<String>['Thomas Jefferson', 'Shadwell', ' 3/4/1801'],
<String>['James Madison', 'Port Conway', ' 3/4/1809'],
<String>['James Monroe', 'Monroe Hall', ' 3/4/1817'],
<String>['Andrew Jackson', 'Waxhaws Region South/North', ' 3/4/1829'],
<String>['John Quincy Adams', 'Braintree', ' 3/4/1825'],
<String>['William Henry Harrison', 'Charles City County', ' 3/4/1841'],
<String>['Martin Van Buren', 'Kinderhook New', ' 3/4/1837'],
<String>['Zachary Taylor', 'Barboursville', ' 3/4/1849'],
<String>['John Tyler', 'Charles City County', ' 4/4/1841'],
<String>['James Buchanan', 'Cove Gap', ' 3/4/1857'],
<String>['James K. Polk', 'Pineville North', ' 3/4/1845'],
<String>['Millard Fillmore', 'Summerhill New', '7/9/1850'],
<String>['Franklin Pierce', 'Hillsborough New', ' 3/4/1853'],
<String>['Andrew Johnson', 'Raleigh North', ' 4/15/1865'],
<String>['Abraham Lincoln', 'Sinking Spring', ' 3/4/1861'],
<String>['Ulysses S. Grant', 'Point Pleasant', ' 3/4/1869'],
<String>['Rutherford B. Hayes', 'Delaware', ' 3/4/1877'],
<String>['Chester A. Arthur', 'Fairfield', ' 9/19/1881'],
<String>['James A. Garfield', 'Moreland Hills', ' 3/4/1881'],
<String>['Benjamin Harrison', 'North Bend', ' 3/4/1889'],
<String>['Grover Cleveland', 'Caldwell New', ' 3/4/1885'],
<String>['William McKinley', 'Niles', ' 3/4/1897'],
<String>['Woodrow Wilson', 'Staunton', ' 3/4/1913'],
<String>['William H. Taft', 'Cincinnati', ' 3/4/1909'],
<String>['Theodore Roosevelt', 'New York City New', ' 9/14/1901'],
<String>['Warren G. Harding', 'Blooming Grove', ' 3/4/1921'],
<String>['Calvin Coolidge', 'Plymouth', '8/2/1923'],
<String>['Herbert Hoover', 'West Branch', ' 3/4/1929'],
<String>['Franklin D. Roosevelt', 'Hyde Park New', ' 3/4/1933'],
<String>['Harry S. Truman', 'Lamar', ' 4/12/1945'],
<String>['Dwight D. Eisenhower', 'Denison', ' 1/20/1953'],
<String>['Lyndon B. Johnson', 'Stonewall', '11/22/1963'],
<String>['Ronald Reagan', 'Tampico', ' 1/20/1981'],
<String>['Richard Nixon', 'Yorba Linda', ' 1/20/1969'],
<String>['Gerald Ford', 'Omaha', 'August 9/1974'],
<String>['John F. Kennedy', 'Brookline', ' 1/20/1961'],
<String>['George H. W. Bush', 'Milton', ' 1/20/1989'],
<String>['Jimmy Carter', 'Plains', ' 1/20/1977'],
<String>['George W. Bush', 'New Haven', ' 1/20, 2001'],
<String>['Bill Clinton', 'Hope', ' 1/20/1993'],
<String>['Barack Obama', 'Honolulu', ' 1/20/2009'],
<String>['Donald J. Trump', 'New York City', ' 1/20/2017'],
];
class _ListItem extends StatelessWidget {
const _ListItem({
this.name,
this.place,
this.date,
this.called,
});
final String name;
final String place;
final String date;
final bool called;
@override
Widget build(BuildContext context) {
return Container(
color: CupertinoDynamicColor.resolve(CupertinoColors.systemBackground, context),
height: 60.0,
padding: const EdgeInsets.only(top: 9.0),
child: Row(
children: <Widget>[
Container(
width: 38.0,
child: called
? Align(
alignment: Alignment.topCenter,
child: Icon(
CupertinoIcons.phone_solid,
color: CupertinoColors.inactiveGray.resolveFrom(context),
size: 18.0,
),
)
: null,
),
Expanded(
child: Container(
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(color: Color(0xFFBCBBC1), width: 0.0),
),
),
padding: const EdgeInsets.only(left: 1.0, bottom: 9.0, right: 10.0),
child: Row(
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.w600,
letterSpacing: -0.18,
),
),
Text(
place,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 15.0,
letterSpacing: -0.24,
color: CupertinoColors.inactiveGray.resolveFrom(context),
),
),
],
),
),
Text(
date,
style: TextStyle(
color: CupertinoColors.inactiveGray.resolveFrom(context),
fontSize: 15.0,
letterSpacing: -0.41,
),
),
Padding(
padding: const EdgeInsets.only(left: 9.0),
child: Icon(
CupertinoIcons.info,
color: CupertinoTheme.of(context).primaryColor,
),
),
],
),
),
),
],
),
);
}
}

@ -1,145 +0,0 @@
// 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/cupertino.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
const Color _kKeyUmbraOpacity = Color(0x33000000); // alpha = 0.2
const Color _kKeyPenumbraOpacity = Color(0x24000000); // alpha = 0.14
const Color _kAmbientShadowOpacity = Color(0x1F000000); // alpha = 0.12
class CupertinoSegmentedControlDemo extends StatefulWidget {
static const String routeName = 'cupertino/segmented_control';
@override
_CupertinoSegmentedControlDemoState createState() => _CupertinoSegmentedControlDemoState();
}
class _CupertinoSegmentedControlDemoState extends State<CupertinoSegmentedControlDemo> {
final Map<int, Widget> children = const <int, Widget>{
0: Text('Midnight'),
1: Text('Viridian'),
2: Text('Cerulean'),
};
final Map<int, Widget> icons = const <int, Widget>{
0: Center(
child: FlutterLogo(
colors: Colors.indigo,
size: 200.0,
),
),
1: Center(
child: FlutterLogo(
colors: Colors.teal,
size: 200.0,
),
),
2: Center(
child: FlutterLogo(
colors: Colors.cyan,
size: 200.0,
),
),
};
int currentSegment = 0;
void onValueChanged(int newValue) {
setState(() {
currentSegment = newValue;
});
}
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: const Text('Segmented Control'),
// We're specifying a back label here because the previous page is a
// Material page. CupertinoPageRoutes could auto-populate these back
// labels.
previousPageTitle: 'Cupertino',
trailing: CupertinoDemoDocumentationButton(CupertinoSegmentedControlDemo.routeName),
),
child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle.copyWith(fontSize: 13),
child: SafeArea(
child: Column(
children: <Widget>[
const Padding(padding: EdgeInsets.all(16.0)),
SizedBox(
width: 500.0,
child: CupertinoSegmentedControl<int>(
children: children,
onValueChanged: onValueChanged,
groupValue: currentSegment,
),
),
SizedBox(
width: 500,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: CupertinoSlidingSegmentedControl<int>(
children: children,
onValueChanged: onValueChanged,
groupValue: currentSegment,
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 32.0,
horizontal: 16.0,
),
child: CupertinoUserInterfaceLevel(
data: CupertinoUserInterfaceLevelData.elevated,
child: Builder(
builder: (BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(
vertical: 64.0,
horizontal: 16.0,
),
decoration: BoxDecoration(
color: CupertinoTheme.of(context).scaffoldBackgroundColor,
borderRadius: BorderRadius.circular(3.0),
boxShadow: const <BoxShadow>[
BoxShadow(
offset: Offset(0.0, 3.0),
blurRadius: 5.0,
spreadRadius: -1.0,
color: _kKeyUmbraOpacity,
),
BoxShadow(
offset: Offset(0.0, 6.0),
blurRadius: 10.0,
spreadRadius: 0.0,
color: _kKeyPenumbraOpacity,
),
BoxShadow(
offset: Offset(0.0, 1.0),
blurRadius: 18.0,
spreadRadius: 0.0,
color: _kAmbientShadowOpacity,
),
],
),
child: icons[currentSegment],
);
},
),
),
),
),
],
),
),
),
);
}
}

@ -1,78 +0,0 @@
// Copyright 2017 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/cupertino.dart';
import '../../gallery/demo.dart';
class CupertinoSliderDemo extends StatefulWidget {
static const String routeName = '/cupertino/slider';
@override
_CupertinoSliderDemoState createState() => _CupertinoSliderDemoState();
}
class _CupertinoSliderDemoState extends State<CupertinoSliderDemo> {
double _value = 25.0;
double _discreteValue = 20.0;
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: const Text('Sliders'),
// We're specifying a back label here because the previous page is a
// Material page. CupertinoPageRoutes could auto-populate these back
// labels.
previousPageTitle: 'Cupertino',
trailing: CupertinoDemoDocumentationButton(CupertinoSliderDemo.routeName),
),
child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle,
child: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget> [
CupertinoSlider(
value: _value,
min: 0.0,
max: 100.0,
onChanged: (double value) {
setState(() {
_value = value;
});
},
),
Text('Cupertino Continuous: ${_value.toStringAsFixed(1)}'),
],
),
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget> [
CupertinoSlider(
value: _discreteValue,
min: 0.0,
max: 100.0,
divisions: 5,
onChanged: (double value) {
setState(() {
_discreteValue = value;
});
},
),
Text('Cupertino Discrete: $_discreteValue'),
],
),
],
),
),
),
),
);
}
}

@ -1,91 +0,0 @@
// Copyright 2017 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/cupertino.dart';
import '../../gallery/demo.dart';
class CupertinoSwitchDemo extends StatefulWidget {
static const String routeName = '/cupertino/switch';
@override
_CupertinoSwitchDemoState createState() => _CupertinoSwitchDemoState();
}
class _CupertinoSwitchDemoState extends State<CupertinoSwitchDemo> {
bool _switchValue = false;
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: const Text('Switch'),
// We're specifying a back label here because the previous page is a
// Material page. CupertinoPageRoutes could auto-populate these back
// labels.
previousPageTitle: 'Cupertino',
trailing: CupertinoDemoDocumentationButton(CupertinoSwitchDemo.routeName),
),
child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle,
child: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Semantics(
container: true,
child: Column(
children: <Widget>[
CupertinoSwitch(
value: _switchValue,
onChanged: (bool value) {
setState(() {
_switchValue = value;
});
},
),
Text(
"Enabled - ${_switchValue ? "On" : "Off"}"
),
],
),
),
Semantics(
container: true,
child: Column(
children: const <Widget>[
CupertinoSwitch(
value: true,
onChanged: null,
),
Text(
'Disabled - On'
),
],
),
),
Semantics(
container: true,
child: Column(
children: const <Widget>[
CupertinoSwitch(
value: false,
onChanged: null,
),
Text(
'Disabled - Off'
),
],
),
),
],
),
),
),
),
);
}
}

@ -1,194 +0,0 @@
// 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/cupertino.dart';
class CupertinoTextFieldDemo extends StatefulWidget {
static const String routeName = '/cupertino/text_fields';
@override
_CupertinoTextFieldDemoState createState() {
return _CupertinoTextFieldDemoState();
}
}
class _CupertinoTextFieldDemoState extends State<CupertinoTextFieldDemo> {
TextEditingController _chatTextController;
TextEditingController _locationTextController;
@override
void initState() {
super.initState();
_chatTextController = TextEditingController();
_locationTextController = TextEditingController(text: 'Montreal, Canada');
}
Widget _buildChatTextField() {
return CupertinoTextField(
controller: _chatTextController,
textCapitalization: TextCapitalization.sentences,
placeholder: 'Text Message',
decoration: BoxDecoration(
border: Border.all(
width: 0.0,
color: CupertinoColors.inactiveGray,
),
borderRadius: BorderRadius.circular(15.0),
),
maxLines: null,
keyboardType: TextInputType.multiline,
prefix: const Padding(padding: EdgeInsets.symmetric(horizontal: 4.0)),
suffix: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: CupertinoButton(
color: CupertinoColors.activeGreen,
minSize: 0.0,
child: const Icon(
CupertinoIcons.up_arrow,
size: 21.0,
color: CupertinoColors.white,
),
padding: const EdgeInsets.all(2.0),
borderRadius: BorderRadius.circular(15.0),
onPressed: ()=> setState(()=> _chatTextController.clear()),
),
),
autofocus: true,
suffixMode: OverlayVisibilityMode.editing,
onSubmitted: (String text)=> setState(()=> _chatTextController.clear()),
);
}
Widget _buildNameField() {
return const CupertinoTextField(
prefix: Icon(
CupertinoIcons.person_solid,
color: CupertinoColors.lightBackgroundGray,
size: 28.0,
),
padding: EdgeInsets.symmetric(horizontal: 6.0, vertical: 12.0),
clearButtonMode: OverlayVisibilityMode.editing,
textCapitalization: TextCapitalization.words,
autocorrect: false,
decoration: BoxDecoration(
border: Border(bottom: BorderSide(width: 0.0, color: CupertinoColors.inactiveGray)),
),
placeholder: 'Name',
);
}
Widget _buildEmailField() {
return const CupertinoTextField(
prefix: Icon(
CupertinoIcons.mail_solid,
color: CupertinoColors.lightBackgroundGray,
size: 28.0,
),
padding: EdgeInsets.symmetric(horizontal: 6.0, vertical: 12.0),
clearButtonMode: OverlayVisibilityMode.editing,
keyboardType: TextInputType.emailAddress,
autocorrect: false,
decoration: BoxDecoration(
border: Border(bottom: BorderSide(width: 0.0, color: CupertinoColors.inactiveGray)),
),
placeholder: 'Email',
);
}
Widget _buildLocationField() {
return CupertinoTextField(
controller: _locationTextController,
prefix: const Icon(
CupertinoIcons.location_solid,
color: CupertinoColors.lightBackgroundGray,
size: 28.0,
),
padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 12.0),
clearButtonMode: OverlayVisibilityMode.editing,
textCapitalization: TextCapitalization.words,
decoration: const BoxDecoration(
border: Border(bottom: BorderSide(width: 0.0, color: CupertinoColors.inactiveGray)),
),
placeholder: 'Location',
);
}
Widget _buildPinField() {
return const CupertinoTextField(
prefix: Icon(
CupertinoIcons.padlock_solid,
color: CupertinoColors.lightBackgroundGray,
size: 28.0,
),
padding: EdgeInsets.symmetric(horizontal: 6.0, vertical: 12.0),
clearButtonMode: OverlayVisibilityMode.editing,
keyboardType: TextInputType.number,
autocorrect: false,
obscureText: true,
decoration: BoxDecoration(
border: Border(bottom: BorderSide(width: 0.0, color: CupertinoColors.inactiveGray)),
),
placeholder: 'Create a PIN',
);
}
Widget _buildTagsField() {
return CupertinoTextField(
controller: TextEditingController(text: 'colleague, reading club'),
prefix: const Icon(
CupertinoIcons.tags_solid,
color: CupertinoColors.lightBackgroundGray,
size: 28.0,
),
enabled: false,
padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 12.0),
decoration: const BoxDecoration(
border: Border(bottom: BorderSide(width: 0.0, color: CupertinoColors.inactiveGray)),
),
);
}
@override
Widget build(BuildContext context) {
return DefaultTextStyle(
style: const TextStyle(
fontFamily: '.SF UI Text',
inherit: false,
fontSize: 17.0,
color: CupertinoColors.black,
),
child: CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
// We're specifying a back label here because the previous page is a
// Material page. CupertinoPageRoutes could auto-populate these back
// labels.
previousPageTitle: 'Cupertino',
middle: Text('Text Fields'),
),
child: CupertinoScrollbar(
child: ListView(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(vertical: 32.0, horizontal: 16.0),
child: Column(
children: <Widget>[
_buildNameField(),
_buildEmailField(),
_buildLocationField(),
_buildPinField(),
_buildTagsField(),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 32.0, horizontal: 16.0),
child: _buildChatTextField(),
),
],
),
),
),
);
}
}

@ -1,52 +0,0 @@
# Fortnightly
A Flutter sample app based on the Material study Fortnightly (a hypothetical, online newspaper.) It
showcases print-quality, custom typography, Material Theming, and text-heavy UI design and layout.
For info on the Fortnightly Material Study, see: https://material.io/design/material-studies/fortnightly.html
## Goals for this sample
* Help you understand how to customize and layout text.
* Provide you with example code for
* Text
* A short app bar (the menu button top left.)
* Avatar images
## Widgets / APIs
* BeveledRectangleBorder
* BoxConstraints on Container
* CircleAvatar
* ExactAssetImage
* Fonts
* SafeArea
* Stack
* SingleChildScrollView
* Text
* TextStyle
* TextTheme
## Notice
* Theming is passed as a parameter in the constructor of `MaterialApp` (`theme:`).
* `SafeArea` adds padding around notches and virtual home buttons on screens that have them (like
iPhone X+). Here, it protects the `ShortAppBar` from overlapping with the status bar (time)
and makes sure the bottom of the newspaper article has padding beneath it if necessary.
* The entire newspaper article is wrapped in a `SingleChildScrollView` widget which ensures that the
entire article can be viewed no matter what the screen's size or orientation is.
* The `Text` widget with text ' ¬ ' has a `TextStyle` that changes one parameter of an inherited
`TextStyle` using `.apply()``.
* The `Text` widget with text 'Connor Eghan' has a `TextStyle` created explicitly instead of
inheriting from theming.
* You can break up long strings in your source files by putting them on multiple lines.
* Fonts are imported with multiple files expressing their weights (Bold, Light, Medium, Regular)
but are accessed with a `FontWeight` value like `FontWeight.w800` for Merriweather-Bold.ttf.
## Questions/issues
If you have a general question about developing in Flutter, the best places to go are:
* [The FlutterDev Google Group](https://groups.google.com/forum/#!forum/flutter-dev)
* [The Flutter Gitter channel](https://gitter.im/flutter/flutter)
* [StackOverflow](https://stackoverflow.com/questions/tagged/flutter)

@ -1,236 +0,0 @@
// Copyright 2019 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/material.dart';
class FortnightlyDemo extends StatelessWidget {
static const String routeName = '/fortnightly';
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Fortnightly Demo',
theme: _fortnightlyTheme,
home: Scaffold(
body: Stack(
children: <Widget>[
FruitPage(),
SafeArea(
child: ShortAppBar(
onBackPressed: () {
Navigator.pop(context);
},
),
),
],
),
),
);
}
}
class ShortAppBar extends StatelessWidget {
const ShortAppBar({ this.onBackPressed });
final VoidCallback onBackPressed;
@override
Widget build(BuildContext context) {
return SizedBox(
width: 96,
height: 50,
child: Material(
color: Theme.of(context).colorScheme.surface,
elevation: 4,
shape: const BeveledRectangleBorder(
borderRadius: BorderRadius.only(bottomRight: Radius.circular(22)),
),
child: Row(
children: <Widget>[
IconButton(
icon: const Icon(Icons.arrow_back),
tooltip: 'Back',
onPressed: onBackPressed,
),
const SizedBox(width: 12),
Image.asset(
'logos/fortnightly/fortnightly_logo.png',
package: 'flutter_gallery_assets',
),
],
),
),
);
}
}
class FruitPage extends StatelessWidget {
static final String paragraph1 = '''Have you ever held a quince? It\'s strange;
covered in a fuzz somewhere between peach skin and a spider web. And it\'s
hard as soft lumber. You\'d be forgiven for thinking it\'s veneered Larch-wood.
But inhale the aroma and you\'ll instantly know you have something wonderful.
Its scent can fill a room for days. And all this before you\'ve even cooked it.
'''.replaceAll('\n', '');
static final String paragraph2 = '''Pomegranates on the other hand have become
almost ubiquitous. You can find its juice in any bodega, Walmart, and even some
gas stations. But at what cost? The pomegranate juice craze of the aughts made
\"megafarmers\" Lynda and Stewart Resnick billions. Unfortunately, it takes a lot
of water to make that much pomegranate juice. Water the Resnicks get from their
majority stake in the Kern Water Bank. How did one family come to hold control
over water meant for the whole central valley of California? The story will shock you.
'''.replaceAll('\n', '');
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).primaryTextTheme;
return SingleChildScrollView(
child: SafeArea(
top: false,
child: Container(
color: Theme.of(context).colorScheme.surface,
child: Column(
children: <Widget>[
Container(
constraints: const BoxConstraints.expand(height: 248),
child: Image.asset(
'food/fruits.png',
package: 'flutter_gallery_assets',
fit: BoxFit.fitWidth,
),
),
const SizedBox(height: 17),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
Text(
'US',
style: textTheme.overline,
),
Text(
' ¬ ',
// TODO(larche): Replace textTheme.display3.color with a ColorScheme value when known.
style: textTheme.overline.apply(color: textTheme.display3.color),
),
Text(
'CULTURE',
style: textTheme.overline,
),
],
),
const SizedBox(height: 10),
Text(
'Quince for Wisdom, Persimmon for Luck, Pomegranate for Love',
style: textTheme.display1,
),
const SizedBox(height: 10),
Text(
'How these crazy fruits sweetened our hearts, relationships,'
'and puffed pastries',
style: textTheme.body1,
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
children: <Widget>[
const CircleAvatar(
backgroundImage: ExactAssetImage(
'people/square/trevor.png',
package: 'flutter_gallery_assets',
),
radius: 20,
),
const SizedBox(width: 12),
Text(
'by',
style: textTheme.display3,
),
const SizedBox(width: 4),
const Text(
'Connor Eghan',
style: TextStyle(
fontFamily: 'Merriweather',
fontSize: 18,
fontWeight: FontWeight.w500,
color: Colors.black,
),
),
],
),
),
Text(
'$paragraph1\n\n$paragraph2',
style: textTheme.body2,
),
],
),
),
],
),
),
),
);
}
}
final ThemeData _fortnightlyTheme = _buildFortnightlyTheme();
ThemeData _buildFortnightlyTheme() {
final ThemeData base = ThemeData.light();
return base.copyWith(
primaryTextTheme: _buildTextTheme(base.primaryTextTheme),
scaffoldBackgroundColor: Colors.white,
);
}
TextTheme _buildTextTheme(TextTheme base) {
TextTheme theme = base.apply(bodyColor: Colors.black);
theme = theme.apply(displayColor: Colors.black);
theme = theme.copyWith(
display1: base.display1.copyWith(
fontFamily: 'Merriweather',
fontStyle: FontStyle.italic,
fontSize: 28,
fontWeight: FontWeight.w800,
color: Colors.black,
height: .88,
),
display3: base.display3.copyWith(
fontFamily: 'LibreFranklin',
fontSize: 18,
fontWeight: FontWeight.w500,
color: Colors.black.withAlpha(153),
),
headline: base.headline.copyWith(fontWeight: FontWeight.w500),
body1: base.body1.copyWith(
fontFamily: 'Merriweather',
fontSize: 14,
fontWeight: FontWeight.w300,
color: const Color(0xFF666666),
height: 1.11,
),
body2: base.body2.copyWith(
fontFamily: 'Merriweather',
fontSize: 16,
fontWeight: FontWeight.w300,
color: const Color(0xFF666666),
height: 1.4,
letterSpacing: .25,
),
overline: const TextStyle(
fontFamily: 'LibreFranklin',
fontSize: 10,
fontWeight: FontWeight.w700,
color: Colors.black,
),
);
return theme;
}

@ -1,44 +0,0 @@
// Copyright 2014 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/material.dart';
import '../gallery/demo.dart';
class ImagesDemo extends StatelessWidget {
static const String routeName = '/images';
@override
Widget build(BuildContext context) {
return TabbedComponentDemoScaffold(
title: 'Animated images',
demos: <ComponentDemoTabData>[
ComponentDemoTabData(
tabName: 'WEBP',
description: '',
exampleCodeTag: 'animated_image',
demoWidget: Semantics(
label: 'Example of animated WEBP',
child: Image.asset(
'animated_images/animated_flutter_stickers.webp',
package: 'flutter_gallery_assets',
),
),
),
ComponentDemoTabData(
tabName: 'GIF',
description: '',
exampleCodeTag: 'animated_image',
demoWidget: Semantics(
label: 'Example of animated GIF',
child:Image.asset(
'animated_images/animated_flutter_lgtm.gif',
package: 'flutter_gallery_assets',
),
),
),
],
);
}
}

@ -1,411 +0,0 @@
// 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/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 Scrollbar(
child: 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,
package: 'flutter_gallery_assets',
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,
),
);
}
}

@ -1,109 +0,0 @@
// Copyright 2019 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/material.dart';
import '../../gallery/demo.dart';
enum BannerDemoAction {
reset,
showMultipleActions,
showLeading,
}
class BannerDemo extends StatefulWidget {
const BannerDemo({ Key key }) : super(key: key);
static const String routeName = '/material/banner';
@override
_BannerDemoState createState() => _BannerDemoState();
}
class _BannerDemoState extends State<BannerDemo> {
static const int _numItems = 20;
bool _displayBanner = true;
bool _showMultipleActions = true;
bool _showLeading = true;
void handleDemoAction(BannerDemoAction action) {
setState(() {
switch (action) {
case BannerDemoAction.reset:
_displayBanner = true;
_showMultipleActions = true;
_showLeading = true;
break;
case BannerDemoAction.showMultipleActions:
_showMultipleActions = !_showMultipleActions;
break;
case BannerDemoAction.showLeading:
_showLeading = !_showLeading;
break;
}
});
}
@override
Widget build(BuildContext context) {
final Widget banner = MaterialBanner(
content: const Text('Your password was updated on your other device. Please sign in again.'),
leading: _showLeading ? const CircleAvatar(child: Icon(Icons.access_alarm)) : null,
actions: <Widget>[
FlatButton(
child: const Text('SIGN IN'),
onPressed: () {
setState(() {
_displayBanner = false;
});
},
),
if (_showMultipleActions)
FlatButton(
child: const Text('DISMISS'),
onPressed: () {
setState(() {
_displayBanner = false;
});
},
),
],
);
return Scaffold(
appBar: AppBar(
title: const Text('Banner'),
actions: <Widget>[
MaterialDemoDocumentationButton(BannerDemo.routeName),
PopupMenuButton<BannerDemoAction>(
onSelected: handleDemoAction,
itemBuilder: (BuildContext context) => <PopupMenuEntry<BannerDemoAction>>[
const PopupMenuItem<BannerDemoAction>(
value: BannerDemoAction.reset,
child: Text('Reset the banner'),
),
const PopupMenuDivider(),
CheckedPopupMenuItem<BannerDemoAction>(
value: BannerDemoAction.showMultipleActions,
checked: _showMultipleActions,
child: const Text('Multiple actions'),
),
CheckedPopupMenuItem<BannerDemoAction>(
value: BannerDemoAction.showLeading,
checked: _showLeading,
child: const Text('Leading icon'),
),
],
),
],
),
body: ListView.builder(itemCount: _displayBanner ? _numItems + 1 : _numItems, itemBuilder: (BuildContext context, int index) {
if (index == 0 && _displayBanner) {
return banner;
}
return ListTile(title: Text('Item ${_displayBanner ? index : index + 1}'),);
}),
);
}
}

@ -1,515 +0,0 @@
// 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/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: Scrollbar(
child: 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) {
return BottomAppBar(
color: color,
shape: shape,
child: Row(children: <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)) const Expanded(child: SizedBox()),
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.')),
);
},
),
]),
);
}
}
// 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;
}
}

@ -1,230 +0,0 @@
// Copyright 2016 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/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,
),
];
_navigationViews[_currentIndex].controller.value = 1.0;
}
@override
void dispose() {
for (NavigationIconView view in _navigationViews)
view.controller.dispose();
super.dispose();
}
Widget _buildTransitionsStack() {
final List<FadeTransition> transitions = <FadeTransition>[
for (NavigationIconView view in _navigationViews) 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,
);
}
}

@ -1,386 +0,0 @@
// Copyright 2016 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/material.dart';
import '../../gallery/demo.dart';
const String _raisedText =
'Raised buttons add dimension to mostly flat layouts. They emphasize '
'functions on busy or wide spaces.';
const String _raisedCode = 'buttons_raised';
const String _flatText = 'A flat button displays an ink splash on press '
'but does not lift. Use flat buttons on toolbars, in dialogs and '
'inline with padding';
const String _flatCode = 'buttons_flat';
const String _outlineText =
'Outline buttons become opaque and elevate when pressed. They are often '
'paired with raised buttons to indicate an alternative, secondary action.';
const String _outlineCode = 'buttons_outline';
const String _dropdownText =
'A dropdown button displays a menu that\'s used to select a value from a '
'small set of values. The button displays the current value and a down '
'arrow.';
const String _dropdownCode = 'buttons_dropdown';
const String _iconText =
'IconButtons are appropriate for toggle buttons that allow a single choice '
'to be selected or deselected, such as adding or removing an item\'s star.';
const String _iconCode = 'buttons_icon';
const String _actionText =
'Floating action buttons are used for a promoted action. They are '
'distinguished by a circled icon floating above the UI and can have motion '
'behaviors that include morphing, launching, and a transferring anchor '
'point.';
const String _actionCode = 'buttons_action';
class ButtonsDemo extends StatefulWidget {
static const String routeName = '/material/buttons';
@override
_ButtonsDemoState createState() => _ButtonsDemoState();
}
class _ButtonsDemoState extends State<ButtonsDemo> {
ShapeBorder _buttonShape;
@override
Widget build(BuildContext context) {
final ButtonThemeData buttonTheme = ButtonTheme.of(context).copyWith(
shape: _buttonShape
);
final List<ComponentDemoTabData> demos = <ComponentDemoTabData>[
ComponentDemoTabData(
tabName: 'RAISED',
description: _raisedText,
demoWidget: ButtonTheme.fromButtonThemeData(
data: buttonTheme,
child: buildRaisedButton(),
),
exampleCodeTag: _raisedCode,
documentationUrl: 'https://docs.flutter.io/flutter/material/RaisedButton-class.html',
),
ComponentDemoTabData(
tabName: 'FLAT',
description: _flatText,
demoWidget: ButtonTheme.fromButtonThemeData(
data: buttonTheme,
child: buildFlatButton(),
),
exampleCodeTag: _flatCode,
documentationUrl: 'https://docs.flutter.io/flutter/material/FlatButton-class.html',
),
ComponentDemoTabData(
tabName: 'OUTLINE',
description: _outlineText,
demoWidget: ButtonTheme.fromButtonThemeData(
data: buttonTheme,
child: buildOutlineButton(),
),
exampleCodeTag: _outlineCode,
documentationUrl: 'https://docs.flutter.io/flutter/material/OutlineButton-class.html',
),
ComponentDemoTabData(
tabName: 'DROPDOWN',
description: _dropdownText,
demoWidget: buildDropdownButton(),
exampleCodeTag: _dropdownCode,
documentationUrl: 'https://docs.flutter.io/flutter/material/DropdownButton-class.html',
),
ComponentDemoTabData(
tabName: 'ICON',
description: _iconText,
demoWidget: buildIconButton(),
exampleCodeTag: _iconCode,
documentationUrl: 'https://docs.flutter.io/flutter/material/IconButton-class.html',
),
ComponentDemoTabData(
tabName: 'ACTION',
description: _actionText,
demoWidget: buildActionButton(),
exampleCodeTag: _actionCode,
documentationUrl: 'https://docs.flutter.io/flutter/material/FloatingActionButton-class.html',
),
];
return TabbedComponentDemoScaffold(
title: 'Buttons',
demos: demos,
actions: <Widget>[
IconButton(
icon: const Icon(Icons.sentiment_very_satisfied, semanticLabel: 'Update shape'),
onPressed: () {
setState(() {
_buttonShape = _buttonShape == null ? const StadiumBorder() : null;
});
},
),
],
);
}
Widget buildRaisedButton() {
return Align(
alignment: const Alignment(0.0, -0.2),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ButtonBar(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
RaisedButton(
child: const Text('RAISED BUTTON', semanticsLabel: 'RAISED BUTTON 1'),
onPressed: () {
// Perform some action
},
),
const RaisedButton(
child: Text('DISABLED', semanticsLabel: 'DISABLED BUTTON 1'),
onPressed: null,
),
],
),
ButtonBar(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
RaisedButton.icon(
icon: const Icon(Icons.add, size: 18.0),
label: const Text('RAISED BUTTON', semanticsLabel: 'RAISED BUTTON 2'),
onPressed: () {
// Perform some action
},
),
RaisedButton.icon(
icon: const Icon(Icons.add, size: 18.0),
label: const Text('DISABLED', semanticsLabel: 'DISABLED BUTTON 2'),
onPressed: null,
),
],
),
],
),
);
}
Widget buildFlatButton() {
return Align(
alignment: const Alignment(0.0, -0.2),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ButtonBar(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton(
child: const Text('FLAT BUTTON', semanticsLabel: 'FLAT BUTTON 1'),
onPressed: () {
// Perform some action
},
),
const FlatButton(
child: Text('DISABLED', semanticsLabel: 'DISABLED BUTTON 3',),
onPressed: null,
),
],
),
ButtonBar(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatButton.icon(
icon: const Icon(Icons.add_circle_outline, size: 18.0),
label: const Text('FLAT BUTTON', semanticsLabel: 'FLAT BUTTON 2'),
onPressed: () {
// Perform some action
},
),
FlatButton.icon(
icon: const Icon(Icons.add_circle_outline, size: 18.0),
label: const Text('DISABLED', semanticsLabel: 'DISABLED BUTTON 4'),
onPressed: null,
),
],
),
],
),
);
}
Widget buildOutlineButton() {
return Align(
alignment: const Alignment(0.0, -0.2),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ButtonBar(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
OutlineButton(
child: const Text('OUTLINE BUTTON', semanticsLabel: 'OUTLINE BUTTON 1'),
onPressed: () {
// Perform some action
},
),
const OutlineButton(
child: Text('DISABLED', semanticsLabel: 'DISABLED BUTTON 5'),
onPressed: null,
),
],
),
ButtonBar(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
OutlineButton.icon(
icon: const Icon(Icons.add, size: 18.0),
label: const Text('OUTLINE BUTTON', semanticsLabel: 'OUTLINE BUTTON 2'),
onPressed: () {
// Perform some action
},
),
OutlineButton.icon(
icon: const Icon(Icons.add, size: 18.0),
label: const Text('DISABLED', semanticsLabel: 'DISABLED BUTTON 6'),
onPressed: null,
),
],
),
],
),
);
}
// https://en.wikipedia.org/wiki/Free_Four
String dropdown1Value = 'Free';
String dropdown2Value;
String dropdown3Value = 'Four';
Widget buildDropdownButton() {
return Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
ListTile(
title: const Text('Simple dropdown:'),
trailing: DropdownButton<String>(
value: dropdown1Value,
onChanged: (String newValue) {
setState(() {
dropdown1Value = newValue;
});
},
items: <String>['One', 'Two', 'Free', 'Four'].map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
),
),
const SizedBox(
height: 24.0,
),
ListTile(
title: const Text('Dropdown with a hint:'),
trailing: DropdownButton<String>(
value: dropdown2Value,
hint: const Text('Choose'),
onChanged: (String newValue) {
setState(() {
dropdown2Value = newValue;
});
},
items: <String>['One', 'Two', 'Free', 'Four'].map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
),
),
const SizedBox(
height: 24.0,
),
ListTile(
title: const Text('Scrollable dropdown:'),
trailing: DropdownButton<String>(
value: dropdown3Value,
onChanged: (String newValue) {
setState(() {
dropdown3Value = newValue;
});
},
items: <String>[
'One', 'Two', 'Free', 'Four', 'Can', 'I', 'Have', 'A', 'Little',
'Bit', 'More', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten',
]
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
})
.toList(),
),
),
],
),
);
}
bool iconButtonToggle = false;
Widget buildIconButton() {
return Align(
alignment: const Alignment(0.0, -0.2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
IconButton(
icon: const Icon(
Icons.thumb_up,
semanticLabel: 'Thumbs up',
),
onPressed: () {
setState(() => iconButtonToggle = !iconButtonToggle);
},
color: iconButtonToggle ? Theme.of(context).primaryColor : null,
),
const IconButton(
icon: Icon(
Icons.thumb_up,
semanticLabel: 'Thumbs not up',
),
onPressed: null,
),
]
.map<Widget>((Widget button) => SizedBox(width: 64.0, height: 64.0, child: button))
.toList(),
),
);
}
Widget buildActionButton() {
return Align(
alignment: const Alignment(0.0, -0.2),
child: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
// Perform some action
},
tooltip: 'floating action button',
),
);
}
}

@ -1,409 +0,0 @@
// Copyright 2016 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/foundation.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
enum CardDemoType {
standard,
tappable,
selectable,
}
class TravelDestination {
const TravelDestination({
@required this.assetName,
@required this.assetPackage,
@required this.title,
@required this.description,
@required this.city,
@required this.location,
this.type = CardDemoType.standard,
}) : assert(assetName != null),
assert(assetPackage != null),
assert(title != null),
assert(description != null),
assert(city != null),
assert(location != null);
final String assetName;
final String assetPackage;
final String title;
final String description;
final String city;
final String location;
final CardDemoType type;
}
const List<TravelDestination> destinations = <TravelDestination>[
TravelDestination(
assetName: 'places/india_thanjavur_market.png',
assetPackage: _kGalleryAssetsPackage,
title: 'Top 10 Cities to Visit in Tamil Nadu',
description: 'Number 10',
city: 'Thanjavur',
location: 'Thanjavur, Tamil Nadu',
),
TravelDestination(
assetName: 'places/india_chettinad_silk_maker.png',
assetPackage: _kGalleryAssetsPackage,
title: 'Artisans of Southern India',
description: 'Silk Spinners',
city: 'Chettinad',
location: 'Sivaganga, Tamil Nadu',
type: CardDemoType.tappable,
),
TravelDestination(
assetName: 'places/india_tanjore_thanjavur_temple.png',
assetPackage: _kGalleryAssetsPackage,
title: 'Brihadisvara Temple',
description: 'Temples',
city: 'Thanjavur',
location: 'Thanjavur, Tamil Nadu',
type: CardDemoType.selectable,
),
];
class TravelDestinationItem extends StatelessWidget {
const TravelDestinationItem({ Key key, @required this.destination, this.shape })
: assert(destination != null),
super(key: key);
// This height will allow for all the Card's content to fit comfortably within the card.
static const double height = 338.0;
final TravelDestination destination;
final ShapeBorder shape;
@override
Widget build(BuildContext context) {
return SafeArea(
top: false,
bottom: false,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
const SectionTitle(title: 'Normal'),
SizedBox(
height: height,
child: Card(
// This ensures that the Card's children are clipped correctly.
clipBehavior: Clip.antiAlias,
shape: shape,
child: TravelDestinationContent(destination: destination),
),
),
],
),
),
);
}
}
class TappableTravelDestinationItem extends StatelessWidget {
const TappableTravelDestinationItem({ Key key, @required this.destination, this.shape })
: assert(destination != null),
super(key: key);
// This height will allow for all the Card's content to fit comfortably within the card.
static const double height = 298.0;
final TravelDestination destination;
final ShapeBorder shape;
@override
Widget build(BuildContext context) {
return SafeArea(
top: false,
bottom: false,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
const SectionTitle(title: 'Tappable'),
SizedBox(
height: height,
child: Card(
// This ensures that the Card's children (including the ink splash) are clipped correctly.
clipBehavior: Clip.antiAlias,
shape: shape,
child: InkWell(
onTap: () {
print('Card was tapped');
},
// Generally, material cards use onSurface with 12% opacity for the pressed state.
splashColor: Theme.of(context).colorScheme.onSurface.withOpacity(0.12),
// Generally, material cards do not have a highlight overlay.
highlightColor: Colors.transparent,
child: TravelDestinationContent(destination: destination),
),
),
),
],
),
),
);
}
}
class SelectableTravelDestinationItem extends StatefulWidget {
const SelectableTravelDestinationItem({ Key key, @required this.destination, this.shape })
: assert(destination != null),
super(key: key);
final TravelDestination destination;
final ShapeBorder shape;
@override
_SelectableTravelDestinationItemState createState() => _SelectableTravelDestinationItemState();
}
class _SelectableTravelDestinationItemState extends State<SelectableTravelDestinationItem> {
// This height will allow for all the Card's content to fit comfortably within the card.
static const double height = 298.0;
bool _isSelected = false;
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
return SafeArea(
top: false,
bottom: false,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
const SectionTitle(title: 'Selectable (long press)'),
SizedBox(
height: height,
child: Card(
// This ensures that the Card's children (including the ink splash) are clipped correctly.
clipBehavior: Clip.antiAlias,
shape: widget.shape,
child: InkWell(
onLongPress: () {
print('Selectable card state changed');
setState(() {
_isSelected = !_isSelected;
});
},
// Generally, material cards use onSurface with 12% opacity for the pressed state.
splashColor: colorScheme.onSurface.withOpacity(0.12),
// Generally, material cards do not have a highlight overlay.
highlightColor: Colors.transparent,
child: Stack(
children: <Widget>[
Container(
color: _isSelected
// Generally, material cards use primary with 8% opacity for the selected state.
// See: https://material.io/design/interaction/states.html#anatomy
? colorScheme.primary.withOpacity(0.08)
: Colors.transparent,
),
TravelDestinationContent(destination: widget.destination),
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
Icons.check_circle,
color: _isSelected ? colorScheme.primary : Colors.transparent,
),
),
),
],
),
),
),
),
],
),
),
);
}
}
class SectionTitle extends StatelessWidget {
const SectionTitle({
Key key,
this.title,
}) : super(key: key);
final String title;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(4.0, 4.0, 4.0, 12.0),
child: Align(
alignment: Alignment.centerLeft,
child: Text(title, style: Theme.of(context).textTheme.subhead),
),
);
}
}
class TravelDestinationContent extends StatelessWidget {
const TravelDestinationContent({ Key key, @required this.destination })
: assert(destination != null),
super(key: key);
final TravelDestination destination;
@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 Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// Photo and title.
SizedBox(
height: 184.0,
child: Stack(
children: <Widget>[
Positioned.fill(
// In order to have the ink splash appear above the image, you
// must use Ink.image. This allows the image to be painted as part
// of the Material and display ink effects above it. Using a
// standard Image will obscure the ink splash.
child: Ink.image(
image: AssetImage(destination.assetName, package: destination.assetPackage),
fit: BoxFit.cover,
child: Container(),
),
),
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.
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,
style: descriptionStyle.copyWith(color: Colors.black54),
),
),
Text(destination.city),
Text(destination.location),
],
),
),
),
if (destination.type == CardDemoType.standard)
// share, explore buttons
ButtonBar(
alignment: MainAxisAlignment.start,
children: <Widget>[
FlatButton(
child: Text('SHARE', semanticsLabel: 'Share ${destination.title}'),
textColor: Colors.amber.shade500,
onPressed: () { print('pressed'); },
),
FlatButton(
child: Text('EXPLORE', semanticsLabel: 'Explore ${destination.title}'),
textColor: Colors.amber.shade500,
onPressed: () { print('pressed'); },
),
],
),
],
);
}
}
class CardsDemo extends StatefulWidget {
static const String routeName = '/material/cards';
@override
_CardsDemoState createState() => _CardsDemoState();
}
class _CardsDemoState extends State<CardsDemo> {
ShapeBorder _shape;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Cards'),
actions: <Widget>[
MaterialDemoDocumentationButton(CardsDemo.routeName),
IconButton(
icon: const Icon(
Icons.sentiment_very_satisfied,
semanticLabel: 'update shape',
),
onPressed: () {
setState(() {
_shape = _shape != null ? null : const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0),
bottomLeft: Radius.circular(2.0),
bottomRight: Radius.circular(2.0),
),
);
});
},
),
],
),
body: Scrollbar(
child: ListView(
padding: const EdgeInsets.only(top: 8.0, left: 8.0, right: 8.0),
children: destinations.map<Widget>((TravelDestination destination) {
Widget child;
switch (destination.type) {
case CardDemoType.standard:
child = TravelDestinationItem(destination: destination, shape: _shape);
break;
case CardDemoType.tappable:
child = TappableTravelDestinationItem(destination: destination, shape: _shape);
break;
case CardDemoType.selectable:
child = SelectableTravelDestinationItem(destination: destination, shape: _shape);
break;
}
return Container(
margin: const EdgeInsets.only(bottom: 8.0),
child: child,
);
}).toList(),
),
),
);
}
}

@ -1,331 +0,0 @@
// Copyright 2015 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/material.dart';
import '../../gallery/demo.dart';
const List<String> _defaultMaterials = <String>[
'poker',
'tortilla',
'fish and',
'micro',
'wood',
];
const List<String> _defaultActions = <String>[
'flake',
'cut',
'fragment',
'splinter',
'nick',
'fry',
'solder',
'cash in',
'eat',
];
const Map<String, String> _results = <String, String>{
'flake': 'flaking',
'cut': 'cutting',
'fragment': 'fragmenting',
'splinter': 'splintering',
'nick': 'nicking',
'fry': 'frying',
'solder': 'soldering',
'cash in': 'cashing in',
'eat': 'eating',
};
const List<String> _defaultTools = <String>[
'hammer',
'chisel',
'fryer',
'fabricator',
'customer',
];
const Map<String, String> _avatars = <String, String>{
'hammer': 'people/square/ali.png',
'chisel': 'people/square/sandra.png',
'fryer': 'people/square/trevor.png',
'fabricator': 'people/square/stella.png',
'customer': 'people/square/peter.png',
};
const Map<String, Set<String>> _toolActions = <String, Set<String>>{
'hammer': <String>{'flake', 'fragment', 'splinter'},
'chisel': <String>{'flake', 'nick', 'splinter'},
'fryer': <String>{'fry'},
'fabricator': <String>{'solder'},
'customer': <String>{'cash in', 'eat'},
};
const Map<String, Set<String>> _materialActions = <String, Set<String>>{
'poker': <String>{'cash in'},
'tortilla': <String>{'fry', 'eat'},
'fish and': <String>{'fry', 'eat'},
'micro': <String>{'solder', 'fragment'},
'wood': <String>{'flake', 'cut', 'splinter', 'nick'},
};
class _ChipsTile extends StatelessWidget {
const _ChipsTile({
Key key,
this.label,
this.children,
}) : super(key: key);
final String label;
final List<Widget> children;
// Wraps a list of chips into a ListTile for display as a section in the demo.
@override
Widget build(BuildContext context) {
return Card(
semanticContainer: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
padding: const EdgeInsets.only(top: 16.0, bottom: 4.0),
alignment: Alignment.center,
child: Text(label, textAlign: TextAlign.start),
),
if (children.isNotEmpty)
Wrap(
children: children.map<Widget>((Widget chip) {
return Padding(
padding: const EdgeInsets.all(2.0),
child: chip,
);
}).toList(),
)
else
Semantics(
container: true,
child: Container(
alignment: Alignment.center,
constraints: const BoxConstraints(minWidth: 48.0, minHeight: 48.0),
padding: const EdgeInsets.all(8.0),
child: Text('None', style: Theme.of(context).textTheme.caption.copyWith(fontStyle: FontStyle.italic)),
),
),
],
),
);
}
}
class ChipDemo extends StatefulWidget {
static const String routeName = '/material/chip';
@override
_ChipDemoState createState() => _ChipDemoState();
}
class _ChipDemoState extends State<ChipDemo> {
_ChipDemoState() {
_reset();
}
final Set<String> _materials = <String>{};
String _selectedMaterial = '';
String _selectedAction = '';
final Set<String> _tools = <String>{};
final Set<String> _selectedTools = <String>{};
final Set<String> _actions = <String>{};
bool _showShapeBorder = false;
// Initialize members with the default data.
void _reset() {
_materials.clear();
_materials.addAll(_defaultMaterials);
_actions.clear();
_actions.addAll(_defaultActions);
_tools.clear();
_tools.addAll(_defaultTools);
_selectedMaterial = '';
_selectedAction = '';
_selectedTools.clear();
}
void _removeMaterial(String name) {
_materials.remove(name);
if (_selectedMaterial == name) {
_selectedMaterial = '';
}
}
void _removeTool(String name) {
_tools.remove(name);
_selectedTools.remove(name);
}
String _capitalize(String name) {
assert(name != null && name.isNotEmpty);
return name.substring(0, 1).toUpperCase() + name.substring(1);
}
// This converts a String to a unique color, based on the hash value of the
// String object. It takes the bottom 16 bits of the hash, and uses that to
// pick a hue for an HSV color, and then creates the color (with a preset
// saturation and value). This means that any unique strings will also have
// unique colors, but they'll all be readable, since they have the same
// saturation and value.
Color _nameToColor(String name) {
assert(name.length > 1);
final int hash = name.hashCode & 0xffff;
final double hue = (360.0 * hash / (1 << 15)) % 360.0;
return HSVColor.fromAHSV(1.0, hue, 0.4, 0.90).toColor();
}
AssetImage _nameToAvatar(String name) {
assert(_avatars.containsKey(name));
return AssetImage(
_avatars[name],
package: 'flutter_gallery_assets',
);
}
String _createResult() {
if (_selectedAction.isEmpty) {
return '';
}
return _capitalize(_results[_selectedAction]) + '!';
}
@override
Widget build(BuildContext context) {
final List<Widget> chips = _materials.map<Widget>((String name) {
return Chip(
key: ValueKey<String>(name),
backgroundColor: _nameToColor(name),
label: Text(_capitalize(name)),
onDeleted: () {
setState(() {
_removeMaterial(name);
});
},
);
}).toList();
final List<Widget> inputChips = _tools.map<Widget>((String name) {
return InputChip(
key: ValueKey<String>(name),
avatar: CircleAvatar(
backgroundImage: _nameToAvatar(name),
),
label: Text(_capitalize(name)),
onDeleted: () {
setState(() {
_removeTool(name);
});
});
}).toList();
final List<Widget> choiceChips = _materials.map<Widget>((String name) {
return ChoiceChip(
key: ValueKey<String>(name),
backgroundColor: _nameToColor(name),
label: Text(_capitalize(name)),
selected: _selectedMaterial == name,
onSelected: (bool value) {
setState(() {
_selectedMaterial = value ? name : '';
});
},
);
}).toList();
final List<Widget> filterChips = _defaultTools.map<Widget>((String name) {
return FilterChip(
key: ValueKey<String>(name),
label: Text(_capitalize(name)),
selected: _tools.contains(name) && _selectedTools.contains(name),
onSelected: !_tools.contains(name)
? null
: (bool value) {
setState(() {
if (!value) {
_selectedTools.remove(name);
} else {
_selectedTools.add(name);
}
});
},
);
}).toList();
Set<String> allowedActions = <String>{};
if (_selectedMaterial != null && _selectedMaterial.isNotEmpty) {
for (String tool in _selectedTools) {
allowedActions.addAll(_toolActions[tool]);
}
allowedActions = allowedActions.intersection(_materialActions[_selectedMaterial]);
}
final List<Widget> actionChips = allowedActions.map<Widget>((String name) {
return ActionChip(
label: Text(_capitalize(name)),
onPressed: () {
setState(() {
_selectedAction = name;
});
},
);
}).toList();
final ThemeData theme = Theme.of(context);
final List<Widget> tiles = <Widget>[
const SizedBox(height: 8.0, width: 0.0),
_ChipsTile(label: 'Available Materials (Chip)', children: chips),
_ChipsTile(label: 'Available Tools (InputChip)', children: inputChips),
_ChipsTile(label: 'Choose a Material (ChoiceChip)', children: choiceChips),
_ChipsTile(label: 'Choose Tools (FilterChip)', children: filterChips),
_ChipsTile(label: 'Perform Allowed Action (ActionChip)', children: actionChips),
const Divider(),
Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Text(
_createResult(),
style: theme.textTheme.title,
),
),
),
];
return Scaffold(
appBar: AppBar(
title: const Text('Chips'),
actions: <Widget>[
MaterialDemoDocumentationButton(ChipDemo.routeName),
IconButton(
onPressed: () {
setState(() {
_showShapeBorder = !_showShapeBorder;
});
},
icon: const Icon(Icons.vignette, semanticLabel: 'Update border shape'),
),
],
),
body: ChipTheme(
data: _showShapeBorder
? theme.chipTheme.copyWith(
shape: BeveledRectangleBorder(
side: const BorderSide(width: 0.66, style: BorderStyle.solid, color: Colors.grey),
borderRadius: BorderRadius.circular(10.0),
))
: theme.chipTheme,
child: Scrollbar(child: ListView(children: tiles)),
),
floatingActionButton: FloatingActionButton(
onPressed: () => setState(_reset),
child: const Icon(Icons.refresh, semanticLabel: 'Reset chips'),
),
);
}
}

@ -1,236 +0,0 @@
// Copyright 2016 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/material.dart';
import 'package:flutter/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: Scrollbar(
child: 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,
),
],
),
),
);
}
}

@ -1,219 +0,0 @@
// Copyright 2015 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/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(),
),
),
],
),
),
),
);
}
}

@ -1,214 +0,0 @@
// Copyright 2016 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/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(),
),
);
}
}

@ -1,266 +0,0 @@
// Copyright 2016 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/material.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior;
import '../../gallery/demo.dart';
const String _kAsset0 = 'people/square/trevor.png';
const String _kAsset1 = 'people/square/stella.png';
const String _kAsset2 = 'people/square/sandra.png';
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
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:
case TargetPlatform.macOS:
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(
drawerDragStartBehavior: DragStartBehavior.down,
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'),
currentAccountPicture: const CircleAvatar(
backgroundImage: AssetImage(
_kAsset0,
package: _kGalleryAssetsPackage,
),
),
otherAccountsPictures: <Widget>[
GestureDetector(
dragStartBehavior: DragStartBehavior.down,
onTap: () {
_onOtherAccountsTap(context);
},
child: Semantics(
label: 'Switch to Account B',
child: const CircleAvatar(
backgroundImage: AssetImage(
_kAsset1,
package: _kGalleryAssetsPackage,
),
),
),
),
GestureDetector(
dragStartBehavior: DragStartBehavior.down,
onTap: () {
_onOtherAccountsTap(context);
},
child: Semantics(
label: 'Switch to Account C',
child: const CircleAvatar(
backgroundImage: AssetImage(
_kAsset2,
package: _kGalleryAssetsPackage,
),
),
),
),
],
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(
dragStartBehavior: DragStartBehavior.down,
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,
decoration: const BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
image: AssetImage(
_kAsset0,
package: _kGalleryAssetsPackage,
),
),
),
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text('Tap here to open the drawer',
style: Theme.of(context).textTheme.subhead,
),
),
],
),
),
),
),
);
}
void _onOtherAccountsTap(BuildContext context) {
showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Account switching not implemented.'),
actions: <Widget>[
FlatButton(
child: const Text('OK'),
onPressed: () {
Navigator.pop(context);
},
),
],
);
},
);
}
}

@ -1,67 +0,0 @@
// Copyright 2014 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/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: Scrollbar(child: ListView(children: buildCards())),
);
}
}

@ -1,362 +0,0 @@
// Copyright 2016 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/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 Container(
// Allow room for the value indicator.
padding: const EdgeInsets.only(top: 44.0),
child: 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(),
),
),
),
),
);
}
}

@ -1,40 +0,0 @@
// Copyright 2016 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/material.dart';
import '../../gallery/demo.dart';
class ExpansionTileListDemo extends StatelessWidget {
static const String routeName = '/material/expansion-tile-list';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Expand/collapse list control'),
actions: <Widget>[MaterialDemoDocumentationButton(routeName)],
),
body: Scrollbar(
child: 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')),
],
),
),
);
}
}

@ -1,268 +0,0 @@
// Copyright 2016 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/material.dart';
import 'package:intl/intl.dart';
// This demo is based on
// https://material.io/design/components/dialogs.html#full-screen-dialog
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: Scrollbar(
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(),
),
),
),
);
}
}

@ -1,395 +0,0 @@
// Copyright 2016 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/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,
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,
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(),
),
),
),
],
),
);
}
}

@ -1,138 +0,0 @@
// Copyright 2016 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/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: Scrollbar(
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),
],
),
),
),
);
}
}

@ -1,302 +0,0 @@
// Copyright 2016 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/material.dart';
import 'package:flutter/semantics.dart';
import '../../gallery/demo.dart';
enum LeaveBehindDemoAction {
reset,
horizontalSwipe,
leftSwipe,
rightSwipe,
confirmDismiss,
}
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;
bool _confirmDismiss = true;
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;
case LeaveBehindDemoAction.confirmDismiss:
_confirmDismiss = !_confirmDismiss;
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 = Scrollbar(
child: ListView(
children: leaveBehindItems.map<Widget>((LeaveBehindItem item) {
return _LeaveBehindListItem(
confirmDismiss: _confirmDismiss,
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'),
),
CheckedPopupMenuItem<LeaveBehindDemoAction>(
value: LeaveBehindDemoAction.confirmDismiss,
checked: _confirmDismiss,
child: const Text('Confirm dismiss'),
),
],
),
],
),
body: body,
);
}
}
class _LeaveBehindListItem extends StatelessWidget {
const _LeaveBehindListItem({
Key key,
@required this.item,
@required this.onArchive,
@required this.onDelete,
@required this.dismissDirection,
@required this.confirmDismiss,
}) : super(key: key);
final LeaveBehindItem item;
final DismissDirection dismissDirection;
final void Function(LeaveBehindItem) onArchive;
final void Function(LeaveBehindItem) onDelete;
final bool confirmDismiss;
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();
},
confirmDismiss: !confirmDismiss ? null : (DismissDirection dismissDirection) async {
switch(dismissDirection) {
case DismissDirection.endToStart:
return await _showConfirmationDialog(context, 'archive') == true;
case DismissDirection.startToEnd:
return await _showConfirmationDialog(context, 'delete') == true;
case DismissDirection.horizontal:
case DismissDirection.vertical:
case DismissDirection.up:
case DismissDirection.down:
assert(false);
}
return false;
},
background: Container(
color: theme.primaryColor,
child: const Center(
child: ListTile(
leading: Icon(Icons.delete, color: Colors.white, size: 36.0),
),
),
),
secondaryBackground: Container(
color: theme.primaryColor,
child: const Center(
child: 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,
),
),
),
);
}
Future<bool> _showConfirmationDialog(BuildContext context, String action) {
return showDialog<bool>(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Do you want to $action this item?'),
actions: <Widget>[
FlatButton(
child: const Text('Yes'),
onPressed: () {
Navigator.pop(context, true); // showDialog() returns true
},
),
FlatButton(
child: const Text('No'),
onPressed: () {
Navigator.pop(context, false); // showDialog() returns false
},
),
],
);
},
);
}
}

@ -1,251 +0,0 @@
// Copyright 2016 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/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(),
),
),
);
}
}

@ -1,38 +0,0 @@
// Copyright 2017 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 'banner_demo.dart';
export 'bottom_app_bar_demo.dart';
export 'bottom_navigation_demo.dart';
export 'buttons_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 'elevation_demo.dart';
export 'expansion_panels_demo.dart';
export 'expansion_tile_list_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_form_field_demo.dart';
export 'tooltip_demo.dart';

@ -1,219 +0,0 @@
// Copyright 2016 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/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),
),
],
),
),
],
),
);
}
}

@ -1,42 +0,0 @@
// Copyright 2015 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/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. Slide down to dismiss.',
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).accentColor,
fontSize: 24.0,
),
),
),
);
});
},
),
),
);
}
}

@ -1,83 +0,0 @@
// Copyright 2016 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/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: Scrollbar(
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.'),
);
},
),
),
),
);
}
}

@ -1,100 +0,0 @@
// Copyright 2015 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/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),
),
);
}
}

@ -1,103 +0,0 @@
// Copyright 2015 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/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<void>((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'),
),
),
);
}
}

@ -1,133 +0,0 @@
// Copyright 2015 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/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,
),
),
),
),
),
),
),
);
}
}

@ -1,221 +0,0 @@
// 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/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/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 _reverse = false;
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(() {
// Trigger a rebuild.
});
// Close the bottom sheet to give the user a clear view of the list.
_bottomSheet?.close();
}
void changeReverse(bool newValue) {
setState(() {
_reverse = newValue;
});
// Rebuild the bottom sheet to reflect the selected list view.
_bottomSheet?.setState(() {
// Trigger a rebuild.
});
// 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>[
CheckboxListTile(
dense: true,
title: const Text('Reverse'),
value: _reverse,
onChanged: changeReverse,
),
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,
reverse: _reverse,
scrollDirection: _itemType == _ReorderableListType.horizontalAvatar ? Axis.horizontal : Axis.vertical,
padding: const EdgeInsets.symmetric(vertical: 8.0),
children: _items.map<Widget>(buildListTile).toList(),
),
),
);
}
}

@ -1,200 +0,0 @@
// Copyright 2015 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/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(),
),
);
}
}

@ -1,295 +0,0 @@
// 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/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',
package: 'flutter_gallery_assets',
),
),
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>[
if (query.isEmpty)
IconButton(
tooltip: 'Voice Search',
icon: const Icon(Icons.mic),
onPressed: () {
query = 'TODO: implement voice input';
},
)
else
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);
},
);
},
);
}
}

@ -1,205 +0,0 @@
// Copyright 2015 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/material.dart';
import '../../gallery/demo.dart';
const String _checkboxText =
'Checkboxes allow the user to select multiple options from a set. '
'A normal checkbox\'s value is true or false and a tristate checkbox\'s '
'value can also be null.';
const String _checkboxCode = 'selectioncontrols_checkbox';
const String _radioText =
'Radio buttons allow the user to select one option from a set. Use radio '
'buttons for exclusive selection if you think that the user needs to see '
'all available options side-by-side.';
const String _radioCode = 'selectioncontrols_radio';
const String _switchText =
'On/off switches toggle the state of a single settings option. The option '
'that the switch controls, as well as the state its in, should be made '
'clear from the corresponding inline label.';
const String _switchCode = 'selectioncontrols_switch';
class SelectionControlsDemo extends StatefulWidget {
static const String routeName = '/material/selection-controls';
@override
_SelectionControlsDemoState createState() => _SelectionControlsDemoState();
}
class _SelectionControlsDemoState extends State<SelectionControlsDemo> {
@override
Widget build(BuildContext context) {
final List<ComponentDemoTabData> demos = <ComponentDemoTabData>[
ComponentDemoTabData(
tabName: 'CHECKBOX',
description: _checkboxText,
demoWidget: buildCheckbox(),
exampleCodeTag: _checkboxCode,
documentationUrl: 'https://docs.flutter.io/flutter/material/Checkbox-class.html',
),
ComponentDemoTabData(
tabName: 'RADIO',
description: _radioText,
demoWidget: buildRadio(),
exampleCodeTag: _radioCode,
documentationUrl: 'https://docs.flutter.io/flutter/material/Radio-class.html',
),
ComponentDemoTabData(
tabName: 'SWITCH',
description: _switchText,
demoWidget: buildSwitch(),
exampleCodeTag: _switchCode,
documentationUrl: 'https://docs.flutter.io/flutter/material/Switch-class.html',
),
];
return TabbedComponentDemoScaffold(
title: 'Selection controls',
demos: demos,
);
}
bool checkboxValueA = true;
bool checkboxValueB = false;
bool checkboxValueC;
int radioValue = 0;
bool switchValue = false;
void handleRadioValueChanged(int value) {
setState(() {
radioValue = value;
});
}
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,
),
],
),
],
),
);
}
Widget buildSwitch() {
return Align(
alignment: const Alignment(0.0, -0.2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Switch.adaptive(
value: switchValue,
onChanged: (bool value) {
setState(() {
switchValue = value;
});
},
),
// Disabled switches
const Switch.adaptive(value: true, onChanged: null),
const Switch.adaptive(value: false, onChanged: null),
],
),
);
}
}

@ -1,441 +0,0 @@
// Copyright 2015 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/material.dart';
import '../../gallery/demo.dart';
class SliderDemo extends StatefulWidget {
static const String routeName = '/material/slider';
@override
_SliderDemoState createState() => _SliderDemoState();
}
Path _downTriangle(double size, Offset thumbCenter, { bool invert = false }) {
final Path thumbPath = Path();
final double height = math.sqrt(3.0) / 2.0;
final double centerHeight = size * height / 3.0;
final double halfSize = size / 2.0;
final double sign = invert ? -1.0 : 1.0;
thumbPath.moveTo(thumbCenter.dx - halfSize, thumbCenter.dy + sign * centerHeight);
thumbPath.lineTo(thumbCenter.dx, thumbCenter.dy - 2.0 * sign * centerHeight);
thumbPath.lineTo(thumbCenter.dx + halfSize, thumbCenter.dy + sign * centerHeight);
thumbPath.close();
return thumbPath;
}
Path _rightTriangle(double size, Offset thumbCenter, { bool invert = false }) {
final Path thumbPath = Path();
final double halfSize = size / 2.0;
final double sign = invert ? -1.0 : 1.0;
thumbPath.moveTo(thumbCenter.dx + halfSize * sign, thumbCenter.dy);
thumbPath.lineTo(thumbCenter.dx - halfSize * sign, thumbCenter.dy - size);
thumbPath.lineTo(thumbCenter.dx - halfSize * sign, thumbCenter.dy + size);
thumbPath.close();
return thumbPath;
}
Path _upTriangle(double size, Offset thumbCenter) => _downTriangle(size, thumbCenter, invert: true);
Path _leftTriangle(double size, Offset thumbCenter) => _rightTriangle(size, thumbCenter, invert: true);
class _CustomRangeThumbShape extends RangeSliderThumbShape {
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 center, {
@required Animation<double> activationAnimation,
@required Animation<double> enableAnimation,
bool isDiscrete = false,
bool isEnabled = false,
bool isOnTop,
@required SliderThemeData sliderTheme,
TextDirection textDirection,
Thumb thumb,
}) {
final Canvas canvas = context.canvas;
final ColorTween colorTween = ColorTween(
begin: sliderTheme.disabledThumbColor,
end: sliderTheme.thumbColor,
);
final double size = _thumbSize * sizeTween.evaluate(enableAnimation);
Path thumbPath;
switch (textDirection) {
case TextDirection.rtl:
switch (thumb) {
case Thumb.start:
thumbPath = _rightTriangle(size, center);
break;
case Thumb.end:
thumbPath = _leftTriangle(size, center);
break;
}
break;
case TextDirection.ltr:
switch (thumb) {
case Thumb.start:
thumbPath = _leftTriangle(size, center);
break;
case Thumb.end:
thumbPath = _rightTriangle(size, center);
break;
}
break;
}
canvas.drawPath(thumbPath, Paint()..color = colorTween.evaluate(enableAnimation));
}
}
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 = _downTriangle(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 = _upTriangle(size, thumbCenter + slideUpOffset);
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> {
@override
Widget build(BuildContext context) {
final List<ComponentDemoTabData> demos = <ComponentDemoTabData>[
ComponentDemoTabData(
tabName: 'SINGLE',
description: 'Sliders containing 1 thumb',
demoWidget: _Sliders(),
documentationUrl: 'https://docs.flutter.io/flutter/material/Slider-class.html',
),
ComponentDemoTabData(
tabName: 'RANGE',
description: 'Sliders containing 2 thumbs',
demoWidget: _RangeSliders(),
documentationUrl: 'https://docs.flutter.io/flutter/material/RangeSlider-class.html',
),
];
return TabbedComponentDemoScaffold(
title: 'Sliders',
demos: demos,
isScrollable: false,
showExampleCodeAction: false,
);
}
}
class _Sliders extends StatefulWidget {
@override
_SlidersState createState() => _SlidersState();
}
class _SlidersState extends State<_Sliders> {
double _continuousValue = 25.0;
double _discreteValue = 20.0;
double _discreteCustomValue = 25.0;
@override
Widget build(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>[
Semantics(
label: 'Editable numerical value',
child: SizedBox(
width: 64,
height: 48,
child: TextField(
textAlign: TextAlign.center,
onSubmitted: (String value) {
final double newValue = double.tryParse(value);
if (newValue != null && newValue != _continuousValue) {
setState(() {
_continuousValue = newValue.clamp(0, 100);
});
}
},
keyboardType: TextInputType.number,
controller: TextEditingController(
text: _continuousValue.toStringAsFixed(0),
),
),
),
),
Slider.adaptive(
value: _continuousValue,
min: 0.0,
max: 100.0,
onChanged: (double value) {
setState(() {
_continuousValue = value;
});
},
),
const Text('Continuous with Editable Numerical Value'),
],
),
Column(
mainAxisSize: MainAxisSize.min,
children: const <Widget>[
Slider.adaptive(value: 0.25, onChanged: null),
Text('Disabled'),
],
),
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Slider.adaptive(
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: theme.colorScheme.onSurface.withOpacity(0.5),
activeTickMarkColor: theme.colorScheme.onSurface.withOpacity(0.7),
inactiveTickMarkColor: theme.colorScheme.surface.withOpacity(0.7),
overlayColor: theme.colorScheme.onSurface.withOpacity(0.12),
thumbColor: Colors.deepPurple,
valueIndicatorColor: Colors.deepPurpleAccent,
thumbShape: _CustomThumbShape(),
valueIndicatorShape: _CustomValueIndicatorShape(),
valueIndicatorTextStyle: theme.accentTextTheme.body2.copyWith(color: theme.colorScheme.onSurface),
),
child: Slider(
value: _discreteCustomValue,
min: 0.0,
max: 200.0,
divisions: 5,
semanticFormatterCallback: (double value) => value.round().toString(),
label: '${_discreteCustomValue.round()}',
onChanged: (double value) {
setState(() {
_discreteCustomValue = value;
});
},
),
),
const Text('Discrete with Custom Theme'),
],
),
],
),
);
}
}
class _RangeSliders extends StatefulWidget {
@override
_RangeSlidersState createState() => _RangeSlidersState();
}
class _RangeSlidersState extends State<_RangeSliders> {
RangeValues _continuousValues = const RangeValues(25.0, 75.0);
RangeValues _discreteValues = const RangeValues(40.0, 120.0);
RangeValues _discreteCustomValues = const RangeValues(40.0, 160.0);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 40.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
RangeSlider(
values: _continuousValues,
min: 0.0,
max: 100.0,
onChanged: (RangeValues values) {
setState(() {
_continuousValues = values;
});
},
),
const Text('Continuous'),
],
),
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
RangeSlider(values: const RangeValues(0.25, 0.75), onChanged: null),
const Text('Disabled'),
],
),
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
RangeSlider(
values: _discreteValues,
min: 0.0,
max: 200.0,
divisions: 5,
labels: RangeLabels('${_discreteValues.start.round()}', '${_discreteValues.end.round()}'),
onChanged: (RangeValues values) {
setState(() {
_discreteValues = values;
});
},
),
const Text('Discrete'),
],
),
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SliderTheme(
data: SliderThemeData(
activeTrackColor: Colors.deepPurple,
inactiveTrackColor: Colors.black26,
activeTickMarkColor: Colors.white70,
inactiveTickMarkColor: Colors.black,
overlayColor: Colors.black12,
thumbColor: Colors.deepPurple,
rangeThumbShape: _CustomRangeThumbShape(),
showValueIndicator: ShowValueIndicator.never,
),
child: RangeSlider(
values: _discreteCustomValues,
min: 0.0,
max: 200.0,
divisions: 5,
labels: RangeLabels('${_discreteCustomValues.start.round()}', '${_discreteCustomValues.end.round()}'),
onChanged: (RangeValues values) {
setState(() {
_discreteCustomValues = values;
});
},
),
),
const Text('Discrete with Custom Theme'),
],
),
],
),
);
}
}

@ -1,95 +0,0 @@
// Copyright 2016 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/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: 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
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
tooltip: 'Create',
onPressed: () {
print('Floating Action Button was pressed');
},
),
);
}
}

@ -1,216 +0,0 @@
// Copyright 2015 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/material.dart';
import '../../gallery/demo.dart';
// Each TabBarView contains a _Page and for each _Page there is a list
// of _CardData objects. Each _CardData object is displayed by a _CardItem.
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: 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>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
child: 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>[
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
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(),
),
),
),
);
}
}

@ -1,160 +0,0 @@
// Copyright 2015 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/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<void>((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, semanticLabel: 'Toggle extended buttons'),
onPressed: () {
setState(() {
_extendedButtons = !_extendedButtons;
});
},
),
],
),
floatingActionButton: buildFloatingActionButton(_selectedPage),
body: TabBarView(
controller: _controller,
children: _allPages.map<Widget>(buildTabView).toList(),
),
);
}
}

@ -1,344 +0,0 @@
// Copyright 2016 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/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior;
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(
dragStartBehavior: DragStartBehavior.down,
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(
drawerDragStartBehavior: DragStartBehavior.down,
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: Scrollbar(
child: SingleChildScrollView(
dragStartBehavior: DragStartBehavior.down,
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),
);
}
}

@ -1,75 +0,0 @@
// Copyright 2016 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/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(),
),
);
}
),
);
}
}

@ -1,715 +0,0 @@
// Copyright 2016 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/material.dart';
import 'package:flutter/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 String _kGalleryAssetsPackage = 'flutter_gallery_assets';
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 = <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: const Rect.fromLTWH(0.0, kLogoHeight, kLogoWidth, kTextHeight),
end: const Rect.fromLTWH(0.0, kImageHeight, kLogoWidth, kTextHeight),
);
final Curve _textOpacity = const Interval(0.4, 1.0, curve: Curves.easeInOut);
final RectTween _imageRectTween = RectTween(
begin: const Rect.fromLTWH(0.0, 0.0, kLogoWidth, kLogoHeight),
end: const 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,
package: _kGalleryAssetsPackage,
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: 'packages/$_kGalleryAssetsPackage/${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: 'packages/$_kGalleryAssetsPackage/${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),
),
]
),
...recipe.ingredients.map<TableRow>((RecipeIngredient ingredient) {
return _buildItemRow(ingredient.amount, ingredient.description);
}),
TableRow(
children: <Widget>[
const SizedBox(),
Padding(
padding: const EdgeInsets.only(top: 24.0, bottom: 4.0),
child: Text('Steps', style: headingStyle),
),
]
),
...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',
ingredientsImagePackage: _kGalleryAssetsPackage,
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',
imagePackage: _kGalleryAssetsPackage,
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',
ingredientsImagePackage: _kGalleryAssetsPackage,
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',
imagePackage: _kGalleryAssetsPackage,
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',
ingredientsImagePackage: _kGalleryAssetsPackage,
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',
imagePackage: _kGalleryAssetsPackage,
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',
ingredientsImagePackage: _kGalleryAssetsPackage,
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',
imagePackage: _kGalleryAssetsPackage,
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',
ingredientsImagePackage: _kGalleryAssetsPackage,
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',
imagePackage: _kGalleryAssetsPackage,
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',
ingredientsImagePackage: _kGalleryAssetsPackage,
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',
imagePackage: _kGalleryAssetsPackage,
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',
ingredientsImagePackage: _kGalleryAssetsPackage,
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',
imagePackage: _kGalleryAssetsPackage,
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'),
],
),
];

@ -1,138 +0,0 @@
// Copyright 2018-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter/material.dart';
import 'package:flutter_gallery/demo/shrine/backdrop.dart';
import 'package:flutter_gallery/demo/shrine/category_menu_page.dart';
import 'package:flutter_gallery/demo/shrine/colors.dart';
import 'package:flutter_gallery/demo/shrine/expanding_bottom_sheet.dart';
import 'package:flutter_gallery/demo/shrine/home.dart';
import 'package:flutter_gallery/demo/shrine/login.dart';
import 'package:flutter_gallery/demo/shrine/supplemental/cut_corners_border.dart';
class ShrineApp extends StatefulWidget {
@override
_ShrineAppState createState() => _ShrineAppState();
}
class _ShrineAppState extends State<ShrineApp> with SingleTickerProviderStateMixin {
// Controller to coordinate both the opening/closing of backdrop and sliding
// of expanding bottom sheet
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 450),
value: 1.0,
);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Shrine',
home: HomePage(
backdrop: Backdrop(
frontLayer: const ProductPage(),
backLayer: CategoryMenuPage(onCategoryTap: () => _controller.forward()),
frontTitle: const Text('SHRINE'),
backTitle: const Text('MENU'),
controller: _controller,
),
expandingBottomSheet: ExpandingBottomSheet(hideController: _controller),
),
initialRoute: '/login',
onGenerateRoute: _getRoute,
// Copy the platform from the main theme in order to support platform
// toggling from the Gallery options menu.
theme: _kShrineTheme.copyWith(platform: Theme.of(context).platform),
);
}
}
Route<dynamic> _getRoute(RouteSettings settings) {
if (settings.name != '/login') {
return null;
}
return MaterialPageRoute<void>(
settings: settings,
builder: (BuildContext context) => LoginPage(),
fullscreenDialog: true,
);
}
final ThemeData _kShrineTheme = _buildShrineTheme();
IconThemeData _customIconTheme(IconThemeData original) {
return original.copyWith(color: kShrineBrown900);
}
ThemeData _buildShrineTheme() {
final ThemeData base = ThemeData.light();
return base.copyWith(
colorScheme: kShrineColorScheme,
accentColor: kShrineBrown900,
primaryColor: kShrinePink100,
buttonColor: kShrinePink100,
scaffoldBackgroundColor: kShrineBackgroundWhite,
cardColor: kShrineBackgroundWhite,
textSelectionColor: kShrinePink100,
errorColor: kShrineErrorRed,
buttonTheme: const ButtonThemeData(
colorScheme: kShrineColorScheme,
textTheme: ButtonTextTheme.normal,
),
primaryIconTheme: _customIconTheme(base.iconTheme),
inputDecorationTheme: const InputDecorationTheme(border: CutCornersBorder()),
textTheme: _buildShrineTextTheme(base.textTheme),
primaryTextTheme: _buildShrineTextTheme(base.primaryTextTheme),
accentTextTheme: _buildShrineTextTheme(base.accentTextTheme),
iconTheme: _customIconTheme(base.iconTheme),
);
}
TextTheme _buildShrineTextTheme(TextTheme base) {
return base.copyWith(
headline: base.headline.copyWith(fontWeight: FontWeight.w500),
title: base.title.copyWith(fontSize: 18.0),
caption: base.caption.copyWith(fontWeight: FontWeight.w400, fontSize: 14.0),
body2: base.body2.copyWith(fontWeight: FontWeight.w500, fontSize: 16.0),
button: base.button.copyWith(fontWeight: FontWeight.w500, fontSize: 14.0),
).apply(
fontFamily: 'Raleway',
displayColor: kShrineBrown900,
bodyColor: kShrineBrown900,
);
}
const ColorScheme kShrineColorScheme = ColorScheme(
primary: kShrinePink100,
primaryVariant: kShrineBrown900,
secondary: kShrinePink50,
secondaryVariant: kShrineBrown900,
surface: kShrineSurfaceWhite,
background: kShrineBackgroundWhite,
error: kShrineErrorRed,
onPrimary: kShrineBrown900,
onSecondary: kShrineBrown900,
onSurface: kShrineBrown900,
onBackground: kShrineBrown900,
onError: kShrineSurfaceWhite,
brightness: Brightness.light,
);

@ -1,397 +0,0 @@
// Copyright 2018-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
import 'package:flutter_gallery/demo/shrine/login.dart';
const Cubic _kAccelerateCurve = Cubic(0.548, 0.0, 0.757, 0.464);
const Cubic _kDecelerateCurve = Cubic(0.23, 0.94, 0.41, 1.0);
const double _kPeakVelocityTime = 0.248210;
const double _kPeakVelocityProgress = 0.379146;
class _TappableWhileStatusIs extends StatefulWidget {
const _TappableWhileStatusIs(
this.status, {
Key key,
this.controller,
this.child,
}) : super(key: key);
final AnimationController controller;
final AnimationStatus status;
final Widget child;
@override
_TappableWhileStatusIsState createState() => _TappableWhileStatusIsState();
}
class _TappableWhileStatusIsState extends State<_TappableWhileStatusIs> {
bool _active;
@override
void initState() {
super.initState();
widget.controller.addStatusListener(_handleStatusChange);
_active = widget.controller.status == widget.status;
}
@override
void dispose() {
widget.controller.removeStatusListener(_handleStatusChange);
super.dispose();
}
void _handleStatusChange(AnimationStatus status) {
final bool value = widget.controller.status == widget.status;
if (_active != value) {
setState(() {
_active = value;
});
}
}
@override
Widget build(BuildContext context) {
Widget child = AbsorbPointer(
absorbing: !_active,
child: widget.child,
);
if (!_active) {
child = FocusScope(
canRequestFocus: false,
debugLabel: '$_TappableWhileStatusIs',
child: child,
);
}
return child;
}
}
class _FrontLayer extends StatelessWidget {
const _FrontLayer({
Key key,
this.onTap,
this.child,
}) : super(key: key);
final VoidCallback onTap;
final Widget child;
@override
Widget build(BuildContext context) {
return Material(
elevation: 16.0,
shape: const BeveledRectangleBorder(
borderRadius: BorderRadius.only(topLeft: Radius.circular(46.0)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Container(
height: 40.0,
alignment: AlignmentDirectional.centerStart,
),
),
Expanded(
child: child,
),
],
),
);
}
}
class _BackdropTitle extends AnimatedWidget {
const _BackdropTitle({
Key key,
Listenable listenable,
this.onPress,
@required this.frontTitle,
@required this.backTitle,
}) : assert(frontTitle != null),
assert(backTitle != null),
super(key: key, listenable: listenable);
final Function onPress;
final Widget frontTitle;
final Widget backTitle;
@override
Widget build(BuildContext context) {
final Animation<double> animation = CurvedAnimation(
parent: listenable,
curve: const Interval(0.0, 0.78),
);
return DefaultTextStyle(
style: Theme.of(context).primaryTextTheme.title,
softWrap: false,
overflow: TextOverflow.ellipsis,
child: Row(children: <Widget>[
// branded icon
SizedBox(
width: 72.0,
child: IconButton(
padding: const EdgeInsets.only(right: 8.0),
onPressed: onPress,
icon: Stack(children: <Widget>[
Opacity(
opacity: animation.value,
child: const ImageIcon(AssetImage('packages/shrine_images/slanted_menu.png')),
),
FractionalTranslation(
translation: Tween<Offset>(
begin: Offset.zero,
end: const Offset(1.0, 0.0),
).evaluate(animation),
child: const ImageIcon(AssetImage('packages/shrine_images/diamond.png')),
),
]),
),
),
// Here, we do a custom cross fade between backTitle and frontTitle.
// This makes a smooth animation between the two texts.
Stack(
children: <Widget>[
Opacity(
opacity: CurvedAnimation(
parent: ReverseAnimation(animation),
curve: const Interval(0.5, 1.0),
).value,
child: FractionalTranslation(
translation: Tween<Offset>(
begin: Offset.zero,
end: const Offset(0.5, 0.0),
).evaluate(animation),
child: backTitle,
),
),
Opacity(
opacity: CurvedAnimation(
parent: animation,
curve: const Interval(0.5, 1.0),
).value,
child: FractionalTranslation(
translation: Tween<Offset>(
begin: const Offset(-0.25, 0.0),
end: Offset.zero,
).evaluate(animation),
child: frontTitle,
),
),
],
),
]),
);
}
}
/// Builds a Backdrop.
///
/// A Backdrop widget has two layers, front and back. The front layer is shown
/// by default, and slides down to show the back layer, from which a user
/// can make a selection. The user can also configure the titles for when the
/// front or back layer is showing.
class Backdrop extends StatefulWidget {
const Backdrop({
@required this.frontLayer,
@required this.backLayer,
@required this.frontTitle,
@required this.backTitle,
@required this.controller,
}) : assert(frontLayer != null),
assert(backLayer != null),
assert(frontTitle != null),
assert(backTitle != null),
assert(controller != null);
final Widget frontLayer;
final Widget backLayer;
final Widget frontTitle;
final Widget backTitle;
final AnimationController controller;
@override
_BackdropState createState() => _BackdropState();
}
class _BackdropState extends State<Backdrop> with SingleTickerProviderStateMixin {
final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop');
AnimationController _controller;
Animation<RelativeRect> _layerAnimation;
@override
void initState() {
super.initState();
_controller = widget.controller;
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
bool get _frontLayerVisible {
final AnimationStatus status = _controller.status;
return status == AnimationStatus.completed || status == AnimationStatus.forward;
}
void _toggleBackdropLayerVisibility() {
// Call setState here to update layerAnimation if that's necessary
setState(() {
_frontLayerVisible ? _controller.reverse() : _controller.forward();
});
}
// _layerAnimation animates the front layer between open and close.
// _getLayerAnimation adjusts the values in the TweenSequence so the
// curve and timing are correct in both directions.
Animation<RelativeRect> _getLayerAnimation(Size layerSize, double layerTop) {
Curve firstCurve; // Curve for first TweenSequenceItem
Curve secondCurve; // Curve for second TweenSequenceItem
double firstWeight; // Weight of first TweenSequenceItem
double secondWeight; // Weight of second TweenSequenceItem
Animation<double> animation; // Animation on which TweenSequence runs
if (_frontLayerVisible) {
firstCurve = _kAccelerateCurve;
secondCurve = _kDecelerateCurve;
firstWeight = _kPeakVelocityTime;
secondWeight = 1.0 - _kPeakVelocityTime;
animation = CurvedAnimation(
parent: _controller.view,
curve: const Interval(0.0, 0.78),
);
} else {
// These values are only used when the controller runs from t=1.0 to t=0.0
firstCurve = _kDecelerateCurve.flipped;
secondCurve = _kAccelerateCurve.flipped;
firstWeight = 1.0 - _kPeakVelocityTime;
secondWeight = _kPeakVelocityTime;
animation = _controller.view;
}
return TweenSequence<RelativeRect>(
<TweenSequenceItem<RelativeRect>>[
TweenSequenceItem<RelativeRect>(
tween: RelativeRectTween(
begin: RelativeRect.fromLTRB(
0.0,
layerTop,
0.0,
layerTop - layerSize.height,
),
end: RelativeRect.fromLTRB(
0.0,
layerTop * _kPeakVelocityProgress,
0.0,
(layerTop - layerSize.height) * _kPeakVelocityProgress,
),
).chain(CurveTween(curve: firstCurve)),
weight: firstWeight,
),
TweenSequenceItem<RelativeRect>(
tween: RelativeRectTween(
begin: RelativeRect.fromLTRB(
0.0,
layerTop * _kPeakVelocityProgress,
0.0,
(layerTop - layerSize.height) * _kPeakVelocityProgress,
),
end: RelativeRect.fill,
).chain(CurveTween(curve: secondCurve)),
weight: secondWeight,
),
],
).animate(animation);
}
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
const double layerTitleHeight = 48.0;
final Size layerSize = constraints.biggest;
final double layerTop = layerSize.height - layerTitleHeight;
_layerAnimation = _getLayerAnimation(layerSize, layerTop);
return Stack(
key: _backdropKey,
children: <Widget>[
_TappableWhileStatusIs(
AnimationStatus.dismissed,
controller: _controller,
child: widget.backLayer,
),
PositionedTransition(
rect: _layerAnimation,
child: _FrontLayer(
onTap: _toggleBackdropLayerVisibility,
child: _TappableWhileStatusIs(
AnimationStatus.completed,
controller: _controller,
child: widget.frontLayer,
),
),
),
],
);
}
@override
Widget build(BuildContext context) {
final AppBar appBar = AppBar(
brightness: Brightness.light,
elevation: 0.0,
titleSpacing: 0.0,
title: _BackdropTitle(
listenable: _controller.view,
onPress: _toggleBackdropLayerVisibility,
frontTitle: widget.frontTitle,
backTitle: widget.backTitle,
),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.search, semanticLabel: 'login'),
onPressed: () {
Navigator.push<void>(
context,
MaterialPageRoute<void>(builder: (BuildContext context) => LoginPage()),
);
},
),
IconButton(
icon: const Icon(Icons.tune, semanticLabel: 'login'),
onPressed: () {
Navigator.push<void>(
context,
MaterialPageRoute<void>(builder: (BuildContext context) => LoginPage()),
);
},
),
],
);
return Scaffold(
appBar: appBar,
body: LayoutBuilder(
builder: _buildStack,
),
);
}
}

@ -1,85 +0,0 @@
// Copyright 2018-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:flutter_gallery/demo/shrine/colors.dart';
import 'package:flutter_gallery/demo/shrine/model/app_state_model.dart';
import 'package:flutter_gallery/demo/shrine/model/product.dart';
class CategoryMenuPage extends StatelessWidget {
const CategoryMenuPage({
Key key,
this.onCategoryTap,
}) : super(key: key);
final VoidCallback onCategoryTap;
Widget _buildCategory(Category category, BuildContext context) {
final String categoryString = category.toString().replaceAll('Category.', '').toUpperCase();
final ThemeData theme = Theme.of(context);
return ScopedModelDescendant<AppStateModel>(
builder: (BuildContext context, Widget child, AppStateModel model) =>
GestureDetector(
onTap: () {
model.setCategory(category);
if (onCategoryTap != null) {
onCategoryTap();
}
},
child: model.selectedCategory == category
? Column(
children: <Widget>[
const SizedBox(height: 16.0),
Text(
categoryString,
style: theme.textTheme.body2,
textAlign: TextAlign.center,
),
const SizedBox(height: 14.0),
Container(
width: 70.0,
height: 2.0,
color: kShrinePink400,
),
],
)
: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Text(
categoryString,
style: theme.textTheme.body2.copyWith(
color: kShrineBrown900.withAlpha(153)
),
textAlign: TextAlign.center,
),
),
),
);
}
@override
Widget build(BuildContext context) {
return Center(
child: Container(
padding: const EdgeInsets.only(top: 40.0),
color: kShrinePink100,
child: ListView(
children: Category.values.map((Category c) => _buildCategory(c, context)).toList(),
),
),
);
}
}

@ -1,28 +0,0 @@
// Copyright 2018-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter/material.dart';
const Color kShrinePink50 = Color(0xFFFEEAE6);
const Color kShrinePink100 = Color(0xFFFEDBD0);
const Color kShrinePink300 = Color(0xFFFBB8AC);
const Color kShrinePink400 = Color(0xFFEAA4A4);
const Color kShrineBrown900 = Color(0xFF442B2D);
const Color kShrineBrown600 = Color(0xFF7D4F52);
const Color kShrineErrorRed = Color(0xFFC5032B);
const Color kShrineSurfaceWhite = Color(0xFFFFFBFA);
const Color kShrineBackgroundWhite = Colors.white;

@ -1,656 +0,0 @@
// Copyright 2018-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:meta/meta.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:flutter_gallery/demo/shrine/colors.dart';
import 'package:flutter_gallery/demo/shrine/model/app_state_model.dart';
import 'package:flutter_gallery/demo/shrine/model/product.dart';
import 'package:flutter_gallery/demo/shrine/shopping_cart.dart';
// These curves define the emphasized easing curve.
const Cubic _kAccelerateCurve = Cubic(0.548, 0.0, 0.757, 0.464);
const Cubic _kDecelerateCurve = Cubic(0.23, 0.94, 0.41, 1.0);
// The time at which the accelerate and decelerate curves switch off
const double _kPeakVelocityTime = 0.248210;
// Percent (as a decimal) of animation that should be completed at _peakVelocityTime
const double _kPeakVelocityProgress = 0.379146;
const double _kCartHeight = 56.0;
// Radius of the shape on the top left of the sheet.
const double _kCornerRadius = 24.0;
// Width for just the cart icon and no thumbnails.
const double _kWidthForCartIcon = 64.0;
class ExpandingBottomSheet extends StatefulWidget {
const ExpandingBottomSheet({Key key, @required this.hideController})
: assert(hideController != null),
super(key: key);
final AnimationController hideController;
@override
_ExpandingBottomSheetState createState() => _ExpandingBottomSheetState();
static _ExpandingBottomSheetState of(BuildContext context, {bool isNullOk = false}) {
assert(isNullOk != null);
assert(context != null);
final _ExpandingBottomSheetState result = context.ancestorStateOfType(
const TypeMatcher<_ExpandingBottomSheetState>()
);
if (isNullOk || result != null) {
return result;
}
throw FlutterError(
'ExpandingBottomSheet.of() called with a context that does not contain a ExpandingBottomSheet.\n');
}
}
// Emphasized Easing is a motion curve that has an organic, exciting feeling.
// It's very fast to begin with and then very slow to finish. Unlike standard
// curves, like [Curves.fastOutSlowIn], it can't be expressed in a cubic bezier
// curve formula. It's quintic, not cubic. But it _can_ be expressed as one
// curve followed by another, which we do here.
Animation<T> _getEmphasizedEasingAnimation<T>({
@required T begin,
@required T peak,
@required T end,
@required bool isForward,
@required Animation<double> parent,
}) {
Curve firstCurve;
Curve secondCurve;
double firstWeight;
double secondWeight;
if (isForward) {
firstCurve = _kAccelerateCurve;
secondCurve = _kDecelerateCurve;
firstWeight = _kPeakVelocityTime;
secondWeight = 1.0 - _kPeakVelocityTime;
} else {
firstCurve = _kDecelerateCurve.flipped;
secondCurve = _kAccelerateCurve.flipped;
firstWeight = 1.0 - _kPeakVelocityTime;
secondWeight = _kPeakVelocityTime;
}
return TweenSequence<T>(
<TweenSequenceItem<T>>[
TweenSequenceItem<T>(
weight: firstWeight,
tween: Tween<T>(
begin: begin,
end: peak,
).chain(CurveTween(curve: firstCurve)),
),
TweenSequenceItem<T>(
weight: secondWeight,
tween: Tween<T>(
begin: peak,
end: end,
).chain(CurveTween(curve: secondCurve)),
),
],
).animate(parent);
}
// Calculates the value where two double Animations should be joined. Used by
// callers of _getEmphasisedEasing<double>().
double _getPeakPoint({double begin, double end}) {
return begin + (end - begin) * _kPeakVelocityProgress;
}
class _ExpandingBottomSheetState extends State<ExpandingBottomSheet> with TickerProviderStateMixin {
final GlobalKey _expandingBottomSheetKey = GlobalKey(debugLabel: 'Expanding bottom sheet');
// The width of the Material, calculated by _widthFor() & based on the number
// of products in the cart. 64.0 is the width when there are 0 products
// (_kWidthForZeroProducts)
double _width = _kWidthForCartIcon;
// Controller for the opening and closing of the ExpandingBottomSheet
AnimationController _controller;
// Animations for the opening and closing of the ExpandingBottomSheet
Animation<double> _widthAnimation;
Animation<double> _heightAnimation;
Animation<double> _thumbnailOpacityAnimation;
Animation<double> _cartOpacityAnimation;
Animation<double> _shapeAnimation;
Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Animation<double> _getWidthAnimation(double screenWidth) {
if (_controller.status == AnimationStatus.forward) {
// Opening animation
return Tween<double>(begin: _width, end: screenWidth).animate(
CurvedAnimation(
parent: _controller.view,
curve: const Interval(0.0, 0.3, curve: Curves.fastOutSlowIn),
),
);
} else {
// Closing animation
return _getEmphasizedEasingAnimation(
begin: _width,
peak: _getPeakPoint(begin: _width, end: screenWidth),
end: screenWidth,
isForward: false,
parent: CurvedAnimation(parent: _controller.view, curve: const Interval(0.0, 0.87)),
);
}
}
Animation<double> _getHeightAnimation(double screenHeight) {
if (_controller.status == AnimationStatus.forward) {
// Opening animation
return _getEmphasizedEasingAnimation(
begin: _kCartHeight,
peak: _kCartHeight + (screenHeight - _kCartHeight) * _kPeakVelocityProgress,
end: screenHeight,
isForward: true,
parent: _controller.view,
);
} else {
// Closing animation
return Tween<double>(
begin: _kCartHeight,
end: screenHeight,
).animate(
CurvedAnimation(
parent: _controller.view,
curve: const Interval(0.434, 1.0, curve: Curves.linear), // not used
// only the reverseCurve will be used
reverseCurve: Interval(0.434, 1.0, curve: Curves.fastOutSlowIn.flipped),
),
);
}
}
// Animation of the cut corner. It's cut when closed and not cut when open.
Animation<double> _getShapeAnimation() {
if (_controller.status == AnimationStatus.forward) {
return Tween<double>(begin: _kCornerRadius, end: 0.0).animate(
CurvedAnimation(
parent: _controller.view,
curve: const Interval(0.0, 0.3, curve: Curves.fastOutSlowIn),
),
);
} else {
return _getEmphasizedEasingAnimation(
begin: _kCornerRadius,
peak: _getPeakPoint(begin: _kCornerRadius, end: 0.0),
end: 0.0,
isForward: false,
parent: _controller.view,
);
}
}
Animation<double> _getThumbnailOpacityAnimation() {
return Tween<double>(begin: 1.0, end: 0.0).animate(
CurvedAnimation(
parent: _controller.view,
curve: _controller.status == AnimationStatus.forward
? const Interval(0.0, 0.3)
: const Interval(0.532, 0.766),
),
);
}
Animation<double> _getCartOpacityAnimation() {
return CurvedAnimation(
parent: _controller.view,
curve: _controller.status == AnimationStatus.forward
? const Interval(0.3, 0.6)
: const Interval(0.766, 1.0),
);
}
// Returns the correct width of the ExpandingBottomSheet based on the number of
// products in the cart.
double _widthFor(int numProducts) {
switch (numProducts) {
case 0:
return _kWidthForCartIcon;
case 1:
return 136.0;
case 2:
return 192.0;
case 3:
return 248.0;
default:
return 278.0;
}
}
// Returns true if the cart is open or opening and false otherwise.
bool get _isOpen {
final AnimationStatus status = _controller.status;
return status == AnimationStatus.completed || status == AnimationStatus.forward;
}
// Opens the ExpandingBottomSheet if it's closed, otherwise does nothing.
void open() {
if (!_isOpen) {
_controller.forward();
}
}
// Closes the ExpandingBottomSheet if it's open or opening, otherwise does nothing.
void close() {
if (_isOpen) {
_controller.reverse();
}
}
// Changes the padding between the start edge of the Material and the cart icon
// based on the number of products in the cart (padding increases when > 0
// products.)
EdgeInsetsDirectional _cartPaddingFor(int numProducts) {
return (numProducts == 0)
? const EdgeInsetsDirectional.only(start: 20.0, end: 8.0)
: const EdgeInsetsDirectional.only(start: 32.0, end: 8.0);
}
bool get _cartIsVisible => _thumbnailOpacityAnimation.value == 0.0;
Widget _buildThumbnails(int numProducts) {
return ExcludeSemantics(
child: Opacity(
opacity: _thumbnailOpacityAnimation.value,
child: Column(
children: <Widget>[
Row(
children: <Widget>[
AnimatedPadding(
padding: _cartPaddingFor(numProducts),
child: const Icon(Icons.shopping_cart),
duration: const Duration(milliseconds: 225),
),
Container(
// Accounts for the overflow number
width: numProducts > 3 ? _width - 94.0 : _width - 64.0,
height: _kCartHeight,
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: ProductThumbnailRow(),
),
ExtraProductsNumber(),
],
),
],
),
),
);
}
Widget _buildShoppingCartPage() {
return Opacity(
opacity: _cartOpacityAnimation.value,
child: ShoppingCartPage(),
);
}
Widget _buildCart(BuildContext context, Widget child) {
// numProducts is the number of different products in the cart (does not
// include multiples of the same product).
final AppStateModel model = ScopedModel.of<AppStateModel>(context);
final int numProducts = model.productsInCart.keys.length;
final int totalCartQuantity = model.totalCartQuantity;
final Size screenSize = MediaQuery.of(context).size;
final double screenWidth = screenSize.width;
final double screenHeight = screenSize.height;
_width = _widthFor(numProducts);
_widthAnimation = _getWidthAnimation(screenWidth);
_heightAnimation = _getHeightAnimation(screenHeight);
_shapeAnimation = _getShapeAnimation();
_thumbnailOpacityAnimation = _getThumbnailOpacityAnimation();
_cartOpacityAnimation = _getCartOpacityAnimation();
return Semantics(
button: true,
value: 'Shopping cart, $totalCartQuantity items',
child: Container(
width: _widthAnimation.value,
height: _heightAnimation.value,
child: Material(
animationDuration: const Duration(milliseconds: 0),
shape: BeveledRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(_shapeAnimation.value),
),
),
elevation: 4.0,
color: kShrinePink50,
child: _cartIsVisible
? _buildShoppingCartPage()
: _buildThumbnails(numProducts),
),
),
);
}
// Builder for the hide and reveal animation when the backdrop opens and closes
Widget _buildSlideAnimation(BuildContext context, Widget child) {
_slideAnimation = _getEmphasizedEasingAnimation(
begin: const Offset(1.0, 0.0),
peak: const Offset(_kPeakVelocityProgress, 0.0),
end: const Offset(0.0, 0.0),
isForward: widget.hideController.status == AnimationStatus.forward,
parent: widget.hideController,
);
return SlideTransition(
position: _slideAnimation,
child: child,
);
}
// Closes the cart if the cart is open, otherwise exits the app (this should
// only be relevant for Android).
Future<bool> _onWillPop() async {
if (!_isOpen) {
await SystemNavigator.pop();
return true;
}
close();
return true;
}
@override
Widget build(BuildContext context) {
return AnimatedSize(
key: _expandingBottomSheetKey,
duration: const Duration(milliseconds: 225),
curve: Curves.easeInOut,
vsync: this,
alignment: FractionalOffset.topLeft,
child: WillPopScope(
onWillPop: _onWillPop,
child: AnimatedBuilder(
animation: widget.hideController,
builder: _buildSlideAnimation,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: open,
child: ScopedModelDescendant<AppStateModel>(
builder: (BuildContext context, Widget child, AppStateModel model) {
return AnimatedBuilder(
builder: _buildCart,
animation: _controller,
);
},
),
),
),
),
);
}
}
class ProductThumbnailRow extends StatefulWidget {
@override
_ProductThumbnailRowState createState() => _ProductThumbnailRowState();
}
class _ProductThumbnailRowState extends State<ProductThumbnailRow> {
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
// _list represents what's currently on screen. If _internalList updates,
// it will need to be updated to match it.
_ListModel _list;
// _internalList represents the list as it is updated by the AppStateModel.
List<int> _internalList;
@override
void initState() {
super.initState();
_list = _ListModel(
listKey: _listKey,
initialItems: ScopedModel.of<AppStateModel>(context).productsInCart.keys.toList(),
removedItemBuilder: _buildRemovedThumbnail,
);
_internalList = List<int>.from(_list.list);
}
Product _productWithId(int productId) {
final AppStateModel model = ScopedModel.of<AppStateModel>(context);
final Product product = model.getProductById(productId);
assert(product != null);
return product;
}
Widget _buildRemovedThumbnail(int item, BuildContext context, Animation<double> animation) {
return ProductThumbnail(animation, animation, _productWithId(item));
}
Widget _buildThumbnail(BuildContext context, int index, Animation<double> animation) {
final Animation<double> thumbnailSize = Tween<double>(begin: 0.8, end: 1.0).animate(
CurvedAnimation(
curve: const Interval(0.33, 1.0, curve: Curves.easeIn),
parent: animation,
),
);
final Animation<double> opacity = CurvedAnimation(
curve: const Interval(0.33, 1.0, curve: Curves.linear),
parent: animation,
);
return ProductThumbnail(thumbnailSize, opacity, _productWithId(_list[index]));
}
// If the lists are the same length, assume nothing has changed.
// If the internalList is shorter than the ListModel, an item has been removed.
// If the internalList is longer, then an item has been added.
void _updateLists() {
// Update _internalList based on the model
_internalList = ScopedModel.of<AppStateModel>(context).productsInCart.keys.toList();
final Set<int> internalSet = Set<int>.from(_internalList);
final Set<int> listSet = Set<int>.from(_list.list);
final Set<int> difference = internalSet.difference(listSet);
if (difference.isEmpty) {
return;
}
for (int product in difference) {
if (_internalList.length < _list.length) {
_list.remove(product);
} else if (_internalList.length > _list.length) {
_list.add(product);
}
}
while (_internalList.length != _list.length) {
int index = 0;
// Check bounds and that the list elements are the same
while (_internalList.isNotEmpty &&
_list.length > 0 &&
index < _internalList.length &&
index < _list.length &&
_internalList[index] == _list[index]) {
index++;
}
}
}
Widget _buildAnimatedList() {
return AnimatedList(
key: _listKey,
shrinkWrap: true,
itemBuilder: _buildThumbnail,
initialItemCount: _list.length,
scrollDirection: Axis.horizontal,
physics: const NeverScrollableScrollPhysics(), // Cart shouldn't scroll
);
}
@override
Widget build(BuildContext context) {
_updateLists();
return ScopedModelDescendant<AppStateModel>(
builder: (BuildContext context, Widget child, AppStateModel model) => _buildAnimatedList(),
);
}
}
class ExtraProductsNumber extends StatelessWidget {
// Calculates the number to be displayed at the end of the row if there are
// more than three products in the cart. This calculates overflow products,
// including their duplicates (but not duplicates of products shown as
// thumbnails).
int _calculateOverflow(AppStateModel model) {
final Map<int, int> productMap = model.productsInCart;
// List created to be able to access products by index instead of ID.
// Order is guaranteed because productsInCart returns a LinkedHashMap.
final List<int> products = productMap.keys.toList();
int overflow = 0;
final int numProducts = products.length;
if (numProducts > 3) {
for (int i = 3; i < numProducts; i++) {
overflow += productMap[products[i]];
}
}
return overflow;
}
Widget _buildOverflow(AppStateModel model, BuildContext context) {
if (model.productsInCart.length <= 3)
return Container();
final int numOverflowProducts = _calculateOverflow(model);
// Maximum of 99 so padding doesn't get messy.
final int displayedOverflowProducts = numOverflowProducts <= 99 ? numOverflowProducts : 99;
return Container(
child: Text(
'+$displayedOverflowProducts',
style: Theme.of(context).primaryTextTheme.button,
),
);
}
@override
Widget build(BuildContext context) {
return ScopedModelDescendant<AppStateModel>(
builder: (BuildContext builder, Widget child, AppStateModel model) => _buildOverflow(model, context),
);
}
}
class ProductThumbnail extends StatelessWidget {
const ProductThumbnail(this.animation, this.opacityAnimation, this.product);
final Animation<double> animation;
final Animation<double> opacityAnimation;
final Product product;
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: opacityAnimation,
child: ScaleTransition(
scale: animation,
child: Container(
width: 40.0,
height: 40.0,
decoration: BoxDecoration(
image: DecorationImage(
image: ExactAssetImage(
product.assetName, // asset name
package: product.assetPackage, // asset package
),
fit: BoxFit.cover,
),
borderRadius: const BorderRadius.all(Radius.circular(10.0)),
),
margin: const EdgeInsets.only(left: 16.0),
),
),
);
}
}
// _ListModel manipulates an internal list and an AnimatedList
class _ListModel {
_ListModel({
@required this.listKey,
@required this.removedItemBuilder,
Iterable<int> initialItems,
}) : assert(listKey != null),
assert(removedItemBuilder != null),
_items = initialItems?.toList() ?? <int>[];
final GlobalKey<AnimatedListState> listKey;
final dynamic removedItemBuilder;
final List<int> _items;
AnimatedListState get _animatedList => listKey.currentState;
void add(int product) {
_insert(_items.length, product);
}
void _insert(int index, int item) {
_items.insert(index, item);
_animatedList.insertItem(index, duration: const Duration(milliseconds: 225));
}
void remove(int product) {
final int index = _items.indexOf(product);
if (index >= 0) {
_removeAt(index);
}
}
void _removeAt(int index) {
final int removedItem = _items.removeAt(index);
if (removedItem != null) {
_animatedList.removeItem(index, (BuildContext context, Animation<double> animation) {
return removedItemBuilder(removedItem, context, animation);
});
}
}
int get length => _items.length;
int operator [](int index) => _items[index];
int indexOf(int item) => _items.indexOf(item);
List<int> get list => _items;
}

@ -1,57 +0,0 @@
// Copyright 2018-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:flutter_gallery/demo/shrine/backdrop.dart';
import 'package:flutter_gallery/demo/shrine/expanding_bottom_sheet.dart';
import 'package:flutter_gallery/demo/shrine/model/app_state_model.dart';
import 'package:flutter_gallery/demo/shrine/model/product.dart';
import 'package:flutter_gallery/demo/shrine/supplemental/asymmetric_view.dart';
class ProductPage extends StatelessWidget {
const ProductPage({this.category = Category.all});
final Category category;
@override
Widget build(BuildContext context) {
return ScopedModelDescendant<AppStateModel>(
builder: (BuildContext context, Widget child, AppStateModel model) {
return AsymmetricView(products: model.getProducts());
});
}
}
class HomePage extends StatelessWidget {
const HomePage({
this.expandingBottomSheet,
this.backdrop,
Key key,
}) : super(key: key);
final ExpandingBottomSheet expandingBottomSheet;
final Backdrop backdrop;
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
backdrop,
Align(child: expandingBottomSheet, alignment: Alignment.bottomRight),
],
);
}
}

@ -1,144 +0,0 @@
// Copyright 2018-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter/material.dart';
import 'package:flutter_gallery/demo/shrine/colors.dart';
class LoginPage extends StatefulWidget {
@override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
static const ShapeDecoration _decoration = ShapeDecoration(
shape: BeveledRectangleBorder(
side: BorderSide(color: kShrineBrown900, width: 0.5),
borderRadius: BorderRadius.all(Radius.circular(7.0)),
),
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 0.0,
backgroundColor: Colors.white,
brightness: Brightness.light,
leading: IconButton(
icon: const BackButtonIcon(),
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
onPressed: () {
// The login screen is immediately displayed on top of the Shrine
// home screen using onGenerateRoute and so rootNavigator must be
// set to true in order to get out of Shrine completely.
Navigator.of(context, rootNavigator: true).pop();
},
),
),
body: SafeArea(
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
children: <Widget>[
const SizedBox(height: 80.0),
Column(
children: <Widget>[
Image.asset('packages/shrine_images/diamond.png'),
const SizedBox(height: 16.0),
Text(
'SHRINE',
style: Theme.of(context).textTheme.headline,
),
],
),
const SizedBox(height: 120.0),
PrimaryColorOverride(
color: kShrineBrown900,
child: Container(
decoration: _decoration,
child: TextField(
controller: _usernameController,
decoration: const InputDecoration(
labelText: 'Username',
),
),
),
),
const SizedBox(height: 12.0),
PrimaryColorOverride(
color: kShrineBrown900,
child: Container(
decoration: _decoration,
child: TextField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Password',
),
),
),
),
Wrap(
children: <Widget>[
ButtonBar(
children: <Widget>[
FlatButton(
child: const Text('CANCEL'),
shape: const BeveledRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(7.0)),
),
onPressed: () {
// The login screen is immediately displayed on top of
// the Shrine home screen using onGenerateRoute and so
// rootNavigator must be set to true in order to get out
// of Shrine completely.
Navigator.of(context, rootNavigator: true).pop();
},
),
RaisedButton(
child: const Text('NEXT'),
elevation: 8.0,
shape: const BeveledRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(7.0)),
),
onPressed: () {
Navigator.pop(context);
},
),
],
),
],
),
],
),
),
);
}
}
class PrimaryColorOverride extends StatelessWidget {
const PrimaryColorOverride({Key key, this.color, this.child}) : super(key: key);
final Color color;
final Widget child;
@override
Widget build(BuildContext context) {
return Theme(
child: child,
data: Theme.of(context).copyWith(primaryColor: color),
);
}
}

@ -1,123 +0,0 @@
// Copyright 2018-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:scoped_model/scoped_model.dart';
import 'package:flutter_gallery/demo/shrine/model/product.dart';
import 'package:flutter_gallery/demo/shrine/model/products_repository.dart';
double _salesTaxRate = 0.06;
double _shippingCostPerItem = 7.0;
class AppStateModel extends Model {
// All the available products.
List<Product> _availableProducts;
// The currently selected category of products.
Category _selectedCategory = Category.all;
// The IDs and quantities of products currently in the cart.
final Map<int, int> _productsInCart = <int, int>{};
Map<int, int> get productsInCart => Map<int, int>.from(_productsInCart);
// Total number of items in the cart.
int get totalCartQuantity => _productsInCart.values.fold(0, (int v, int e) => v + e);
Category get selectedCategory => _selectedCategory;
// Totaled prices of the items in the cart.
double get subtotalCost {
return _productsInCart.keys
.map((int id) => _availableProducts[id].price * _productsInCart[id])
.fold(0.0, (double sum, int e) => sum + e);
}
// Total shipping cost for the items in the cart.
double get shippingCost {
return _shippingCostPerItem * _productsInCart.values.fold(0.0, (num sum, int e) => sum + e);
}
// Sales tax for the items in the cart
double get tax => subtotalCost * _salesTaxRate;
// Total cost to order everything in the cart.
double get totalCost => subtotalCost + shippingCost + tax;
// Returns a copy of the list of available products, filtered by category.
List<Product> getProducts() {
if (_availableProducts == null) {
return <Product>[];
}
if (_selectedCategory == Category.all) {
return List<Product>.from(_availableProducts);
} else {
return _availableProducts
.where((Product p) => p.category == _selectedCategory)
.toList();
}
}
// Adds a product to the cart.
void addProductToCart(int productId) {
if (!_productsInCart.containsKey(productId)) {
_productsInCart[productId] = 1;
} else {
_productsInCart[productId]++;
}
notifyListeners();
}
// Removes an item from the cart.
void removeItemFromCart(int productId) {
if (_productsInCart.containsKey(productId)) {
if (_productsInCart[productId] == 1) {
_productsInCart.remove(productId);
} else {
_productsInCart[productId]--;
}
}
notifyListeners();
}
// Returns the Product instance matching the provided id.
Product getProductById(int id) {
return _availableProducts.firstWhere((Product p) => p.id == id);
}
// Removes everything from the cart.
void clearCart() {
_productsInCart.clear();
notifyListeners();
}
// Loads the list of available products from the repo.
void loadProducts() {
_availableProducts = ProductsRepository.loadProducts(Category.all);
notifyListeners();
}
void setCategory(Category newCategory) {
_selectedCategory = newCategory;
notifyListeners();
}
@override
String toString() {
return 'AppStateModel(totalCost: $totalCost)';
}
}

@ -1,48 +0,0 @@
// Copyright 2018-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter/foundation.dart';
enum Category {
all,
accessories,
clothing,
home,
}
class Product {
const Product({
@required this.category,
@required this.id,
@required this.isFeatured,
@required this.name,
@required this.price,
}) : assert(category != null),
assert(id != null),
assert(isFeatured != null),
assert(name != null),
assert(price != null);
final Category category;
final int id;
final bool isFeatured;
final String name;
final int price;
String get assetName => '$id-0.jpg';
String get assetPackage => 'shrine_images';
@override
String toString() => '$name (id=$id)';
}

@ -1,293 +0,0 @@
// Copyright 2018-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter_gallery/demo/shrine/model/product.dart';
class ProductsRepository {
static List<Product> loadProducts(Category category) {
const List<Product> allProducts = <Product>[
Product(
category: Category.accessories,
id: 0,
isFeatured: true,
name: 'Vagabond sack',
price: 120,
),
Product(
category: Category.accessories,
id: 1,
isFeatured: true,
name: 'Stella sunglasses',
price: 58,
),
Product(
category: Category.accessories,
id: 2,
isFeatured: false,
name: 'Whitney belt',
price: 35,
),
Product(
category: Category.accessories,
id: 3,
isFeatured: true,
name: 'Garden strand',
price: 98,
),
Product(
category: Category.accessories,
id: 4,
isFeatured: false,
name: 'Strut earrings',
price: 34,
),
Product(
category: Category.accessories,
id: 5,
isFeatured: false,
name: 'Varsity socks',
price: 12,
),
Product(
category: Category.accessories,
id: 6,
isFeatured: false,
name: 'Weave keyring',
price: 16,
),
Product(
category: Category.accessories,
id: 7,
isFeatured: true,
name: 'Gatsby hat',
price: 40,
),
Product(
category: Category.accessories,
id: 8,
isFeatured: true,
name: 'Shrug bag',
price: 198,
),
Product(
category: Category.home,
id: 9,
isFeatured: true,
name: 'Gilt desk trio',
price: 58,
),
Product(
category: Category.home,
id: 10,
isFeatured: false,
name: 'Copper wire rack',
price: 18,
),
Product(
category: Category.home,
id: 11,
isFeatured: false,
name: 'Soothe ceramic set',
price: 28,
),
Product(
category: Category.home,
id: 12,
isFeatured: false,
name: 'Hurrahs tea set',
price: 34,
),
Product(
category: Category.home,
id: 13,
isFeatured: true,
name: 'Blue stone mug',
price: 18,
),
Product(
category: Category.home,
id: 14,
isFeatured: true,
name: 'Rainwater tray',
price: 27,
),
Product(
category: Category.home,
id: 15,
isFeatured: true,
name: 'Chambray napkins',
price: 16,
),
Product(
category: Category.home,
id: 16,
isFeatured: true,
name: 'Succulent planters',
price: 16,
),
Product(
category: Category.home,
id: 17,
isFeatured: false,
name: 'Quartet table',
price: 175,
),
Product(
category: Category.home,
id: 18,
isFeatured: true,
name: 'Kitchen quattro',
price: 129,
),
Product(
category: Category.clothing,
id: 19,
isFeatured: false,
name: 'Clay sweater',
price: 48,
),
Product(
category: Category.clothing,
id: 20,
isFeatured: false,
name: 'Sea tunic',
price: 45,
),
Product(
category: Category.clothing,
id: 21,
isFeatured: false,
name: 'Plaster tunic',
price: 38,
),
Product(
category: Category.clothing,
id: 22,
isFeatured: false,
name: 'White pinstripe shirt',
price: 70,
),
Product(
category: Category.clothing,
id: 23,
isFeatured: false,
name: 'Chambray shirt',
price: 70,
),
Product(
category: Category.clothing,
id: 24,
isFeatured: true,
name: 'Seabreeze sweater',
price: 60,
),
Product(
category: Category.clothing,
id: 25,
isFeatured: false,
name: 'Gentry jacket',
price: 178,
),
Product(
category: Category.clothing,
id: 26,
isFeatured: false,
name: 'Navy trousers',
price: 74,
),
Product(
category: Category.clothing,
id: 27,
isFeatured: true,
name: 'Walter henley (white)',
price: 38,
),
Product(
category: Category.clothing,
id: 28,
isFeatured: true,
name: 'Surf and perf shirt',
price: 48,
),
Product(
category: Category.clothing,
id: 29,
isFeatured: true,
name: 'Ginger scarf',
price: 98,
),
Product(
category: Category.clothing,
id: 30,
isFeatured: true,
name: 'Ramona crossover',
price: 68,
),
Product(
category: Category.clothing,
id: 31,
isFeatured: false,
name: 'Chambray shirt',
price: 38,
),
Product(
category: Category.clothing,
id: 32,
isFeatured: false,
name: 'Classic white collar',
price: 58,
),
Product(
category: Category.clothing,
id: 33,
isFeatured: true,
name: 'Cerise scallop tee',
price: 42,
),
Product(
category: Category.clothing,
id: 34,
isFeatured: false,
name: 'Shoulder rolls tee',
price: 27,
),
Product(
category: Category.clothing,
id: 35,
isFeatured: false,
name: 'Grey slouch tank',
price: 24,
),
Product(
category: Category.clothing,
id: 36,
isFeatured: false,
name: 'Sunshirt dress',
price: 58,
),
Product(
category: Category.clothing,
id: 37,
isFeatured: true,
name: 'Fine lines tee',
price: 58,
),
];
if (category == Category.all) {
return allProducts;
} else {
return allProducts.where((Product p) => p.category == category).toList();
}
}
}

@ -1,275 +0,0 @@
// Copyright 2018-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:flutter_gallery/demo/shrine/colors.dart';
import 'package:flutter_gallery/demo/shrine/expanding_bottom_sheet.dart';
import 'package:flutter_gallery/demo/shrine/model/app_state_model.dart';
import 'package:flutter_gallery/demo/shrine/model/product.dart';
const double _leftColumnWidth = 60.0;
class ShoppingCartPage extends StatefulWidget {
@override
_ShoppingCartPageState createState() => _ShoppingCartPageState();
}
class _ShoppingCartPageState extends State<ShoppingCartPage> {
List<Widget> _createShoppingCartRows(AppStateModel model) {
return model.productsInCart.keys
.map((int id) => ShoppingCartRow(
product: model.getProductById(id),
quantity: model.productsInCart[id],
onPressed: () {
model.removeItemFromCart(id);
},
),
)
.toList();
}
@override
Widget build(BuildContext context) {
final ThemeData localTheme = Theme.of(context);
return Scaffold(
backgroundColor: kShrinePink50,
body: SafeArea(
child: Container(
child: ScopedModelDescendant<AppStateModel>(
builder: (BuildContext context, Widget child, AppStateModel model) {
return Stack(
children: <Widget>[
ListView(
children: <Widget>[
Row(
children: <Widget>[
SizedBox(
width: _leftColumnWidth,
child: IconButton(
icon: const Icon(Icons.keyboard_arrow_down),
onPressed: () => ExpandingBottomSheet.of(context).close(),
),
),
Text(
'CART',
style: localTheme.textTheme.subhead.copyWith(fontWeight: FontWeight.w600),
),
const SizedBox(width: 16.0),
Text('${model.totalCartQuantity} ITEMS'),
],
),
const SizedBox(height: 16.0),
Column(
children: _createShoppingCartRows(model),
),
ShoppingCartSummary(model: model),
const SizedBox(height: 100.0),
],
),
Positioned(
bottom: 16.0,
left: 16.0,
right: 16.0,
child: RaisedButton(
shape: const BeveledRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(7.0)),
),
color: kShrinePink100,
splashColor: kShrineBrown600,
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 12.0),
child: Text('CLEAR CART'),
),
onPressed: () {
model.clearCart();
ExpandingBottomSheet.of(context).close();
},
),
),
],
);
},
),
),
),
);
}
}
class ShoppingCartSummary extends StatelessWidget {
const ShoppingCartSummary({this.model});
final AppStateModel model;
@override
Widget build(BuildContext context) {
final TextStyle smallAmountStyle = Theme.of(context).textTheme.body1.copyWith(color: kShrineBrown600);
final TextStyle largeAmountStyle = Theme.of(context).textTheme.display1;
final NumberFormat formatter = NumberFormat.simpleCurrency(
decimalDigits: 2,
locale: Localizations.localeOf(context).toString(),
);
return Row(
children: <Widget>[
const SizedBox(width: _leftColumnWidth),
Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Column(
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
const Expanded(
child: Text('TOTAL'),
),
Text(
formatter.format(model.totalCost),
style: largeAmountStyle,
),
],
),
const SizedBox(height: 16.0),
Row(
children: <Widget>[
const Expanded(
child: Text('Subtotal:'),
),
Text(
formatter.format(model.subtotalCost),
style: smallAmountStyle,
),
],
),
const SizedBox(height: 4.0),
Row(
children: <Widget>[
const Expanded(
child: Text('Shipping:'),
),
Text(
formatter.format(model.shippingCost),
style: smallAmountStyle,
),
],
),
const SizedBox(height: 4.0),
Row(
children: <Widget>[
const Expanded(
child: Text('Tax:'),
),
Text(
formatter.format(model.tax),
style: smallAmountStyle,
),
],
),
],
),
),
),
],
);
}
}
class ShoppingCartRow extends StatelessWidget {
const ShoppingCartRow({
@required this.product,
@required this.quantity,
this.onPressed,
});
final Product product;
final int quantity;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
final NumberFormat formatter = NumberFormat.simpleCurrency(
decimalDigits: 0,
locale: Localizations.localeOf(context).toString(),
);
final ThemeData localTheme = Theme.of(context);
return Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Row(
key: ValueKey<int>(product.id),
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SizedBox(
width: _leftColumnWidth,
child: IconButton(
icon: const Icon(Icons.remove_circle_outline),
onPressed: onPressed,
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Column(
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Image.asset(
product.assetName,
package: product.assetPackage,
fit: BoxFit.cover,
width: 75.0,
height: 75.0,
),
const SizedBox(width: 16.0),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: Text('Quantity: $quantity'),
),
Text('x ${formatter.format(product.price)}'),
],
),
Text(
product.name,
style: localTheme.textTheme.subhead.copyWith(fontWeight: FontWeight.w600),
),
],
),
),
],
),
const SizedBox(height: 16.0),
const Divider(
color: kShrineBrown900,
height: 10.0,
),
],
),
),
),
],
),
);
}
}

@ -1,95 +0,0 @@
// Copyright 2018-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter/material.dart';
import 'package:flutter_gallery/demo/shrine/model/product.dart';
import 'package:flutter_gallery/demo/shrine/supplemental/product_columns.dart';
class AsymmetricView extends StatelessWidget {
const AsymmetricView({Key key, this.products}) : super(key: key);
final List<Product> products;
List<Container> _buildColumns(BuildContext context) {
if (products == null || products.isEmpty) {
return const <Container>[];
}
// This will return a list of columns. It will oscillate between the two
// kinds of columns. Even cases of the index (0, 2, 4, etc) will be
// TwoProductCardColumn and the odd cases will be OneProductCardColumn.
//
// Each pair of columns will advance us 3 products forward (2 + 1). That's
// some kinda awkward math so we use _evenCasesIndex and _oddCasesIndex as
// helpers for creating the index of the product list that will correspond
// to the index of the list of columns.
return List<Container>.generate(_listItemCount(products.length), (int index) {
double width = .59 * MediaQuery.of(context).size.width;
Widget column;
if (index % 2 == 0) {
/// Even cases
final int bottom = _evenCasesIndex(index);
column = TwoProductCardColumn(
bottom: products[bottom],
top: products.length - 1 >= bottom + 1
? products[bottom + 1]
: null,
);
width += 32.0;
} else {
/// Odd cases
column = OneProductCardColumn(
product: products[_oddCasesIndex(index)],
);
}
return Container(
width: width,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: column,
),
);
}).toList();
}
int _evenCasesIndex(int input) {
// The operator ~/ is a cool one. It's the truncating division operator. It
// divides the number and if there's a remainder / decimal, it cuts it off.
// This is like dividing and then casting the result to int. Also, it's
// functionally equivalent to floor() in this case.
return input ~/ 2 * 3;
}
int _oddCasesIndex(int input) {
assert(input > 0);
return (input / 2).ceil() * 3 - 1;
}
int _listItemCount(int totalItems) {
return (totalItems % 3 == 0)
? totalItems ~/ 3 * 2
: (totalItems / 3).ceil() * 2 - 1;
}
@override
Widget build(BuildContext context) {
return ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.fromLTRB(0.0, 34.0, 16.0, 44.0),
children: _buildColumns(context),
physics: const AlwaysScrollableScrollPhysics(),
);
}
}

@ -1,134 +0,0 @@
// Copyright 2018-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:ui' show lerpDouble;
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class CutCornersBorder extends OutlineInputBorder {
const CutCornersBorder({
BorderSide borderSide = BorderSide.none,
BorderRadius borderRadius = const BorderRadius.all(Radius.circular(2.0)),
this.cut = 7.0,
double gapPadding = 2.0,
}) : super(
borderSide: borderSide,
borderRadius: borderRadius,
gapPadding: gapPadding,
);
@override
CutCornersBorder copyWith({
BorderSide borderSide,
BorderRadius borderRadius,
double gapPadding,
double cut,
}) {
return CutCornersBorder(
borderSide: borderSide ?? this.borderSide,
borderRadius: borderRadius ?? this.borderRadius,
gapPadding: gapPadding ?? this.gapPadding,
cut: cut ?? this.cut,
);
}
final double cut;
@override
ShapeBorder lerpFrom(ShapeBorder a, double t) {
if (a is CutCornersBorder) {
final CutCornersBorder outline = a;
return CutCornersBorder(
borderRadius: BorderRadius.lerp(outline.borderRadius, borderRadius, t),
borderSide: BorderSide.lerp(outline.borderSide, borderSide, t),
cut: cut,
gapPadding: outline.gapPadding,
);
}
return super.lerpFrom(a, t);
}
@override
ShapeBorder lerpTo(ShapeBorder b, double t) {
if (b is CutCornersBorder) {
final CutCornersBorder outline = b;
return CutCornersBorder(
borderRadius: BorderRadius.lerp(borderRadius, outline.borderRadius, t),
borderSide: BorderSide.lerp(borderSide, outline.borderSide, t),
cut: cut,
gapPadding: outline.gapPadding,
);
}
return super.lerpTo(b, t);
}
Path _notchedCornerPath(Rect center, [double start = 0.0, double extent = 0.0]) {
final Path path = Path();
if (start > 0.0 || extent > 0.0) {
path.relativeMoveTo(extent + start, center.top);
_notchedSidesAndBottom(center, path);
path..lineTo(center.left + cut, center.top)..lineTo(start, center.top);
} else {
path.moveTo(center.left + cut, center.top);
_notchedSidesAndBottom(center, path);
path.lineTo(center.left + cut, center.top);
}
return path;
}
Path _notchedSidesAndBottom(Rect center, Path path) {
return path
..lineTo(center.right - cut, center.top)
..lineTo(center.right, center.top + cut)
..lineTo(center.right, center.top + center.height - cut)
..lineTo(center.right - cut, center.top + center.height)
..lineTo(center.left + cut, center.top + center.height)
..lineTo(center.left, center.top + center.height - cut)
..lineTo(center.left, center.top + cut);
}
@override
void paint(
Canvas canvas,
Rect rect, {
double gapStart,
double gapExtent = 0.0,
double gapPercentage = 0.0,
TextDirection textDirection,
}) {
assert(gapExtent != null);
assert(gapPercentage >= 0.0 && gapPercentage <= 1.0);
final Paint paint = borderSide.toPaint();
final RRect outer = borderRadius.toRRect(rect);
if (gapStart == null || gapExtent <= 0.0 || gapPercentage == 0.0) {
canvas.drawPath(_notchedCornerPath(outer.middleRect), paint);
} else {
final double extent = lerpDouble(0.0, gapExtent + gapPadding * 2.0, gapPercentage);
switch (textDirection) {
case TextDirection.rtl: {
final Path path = _notchedCornerPath(outer.middleRect, gapStart + gapPadding - extent, extent);
canvas.drawPath(path, paint);
break;
}
case TextDirection.ltr: {
final Path path = _notchedCornerPath(outer.middleRect, gapStart - gapPadding, extent);
canvas.drawPath(path, paint);
break;
}
}
}
}
}

@ -1,97 +0,0 @@
// Copyright 2018-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:flutter_gallery/demo/shrine/model/app_state_model.dart';
import 'package:flutter_gallery/demo/shrine/model/product.dart';
class ProductCard extends StatelessWidget {
const ProductCard({ this.imageAspectRatio = 33 / 49, this.product })
: assert(imageAspectRatio == null || imageAspectRatio > 0);
final double imageAspectRatio;
final Product product;
static const double kTextBoxHeight = 65.0;
@override
Widget build(BuildContext context) {
final NumberFormat formatter = NumberFormat.simpleCurrency(
decimalDigits: 0,
locale: Localizations.localeOf(context).toString(),
);
final ThemeData theme = Theme.of(context);
final Image imageWidget = Image.asset(
product.assetName,
package: product.assetPackage,
fit: BoxFit.cover,
);
return ScopedModelDescendant<AppStateModel>(
builder: (BuildContext context, Widget child, AppStateModel model) {
return GestureDetector(
onTap: () {
model.addProductToCart(product.id);
},
child: child,
);
},
child: Stack(
children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
AspectRatio(
aspectRatio: imageAspectRatio,
child: imageWidget,
),
SizedBox(
height: kTextBoxHeight * MediaQuery.of(context).textScaleFactor,
width: 121.0,
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
product == null ? '' : product.name,
style: theme.textTheme.button,
softWrap: false,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
const SizedBox(height: 4.0),
Text(
product == null ? '' : formatter.format(product.price),
style: theme.textTheme.caption,
),
],
),
),
],
),
const Padding(
padding: EdgeInsets.all(16.0),
child: Icon(Icons.add_shopping_cart),
),
],
),
);
}
}

@ -1,89 +0,0 @@
// Copyright 2018-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter/material.dart';
import 'package:flutter_gallery/demo/shrine/model/product.dart';
import 'package:flutter_gallery/demo/shrine/supplemental/product_card.dart';
class TwoProductCardColumn extends StatelessWidget {
const TwoProductCardColumn({
@required this.bottom,
this.top,
}) : assert(bottom != null);
final Product bottom, top;
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
const double spacerHeight = 44.0;
final double heightOfCards = (constraints.biggest.height - spacerHeight) / 2.0;
final double availableHeightForImages = heightOfCards - ProductCard.kTextBoxHeight;
// Ensure the cards take up the available space as long as the screen is
// sufficiently tall, otherwise fallback on a constant aspect ratio.
final double imageAspectRatio = availableHeightForImages >= 0.0
? constraints.biggest.width / availableHeightForImages
: 49.0 / 33.0;
return ListView(
physics: const ClampingScrollPhysics(),
children: <Widget>[
Padding(
padding: const EdgeInsetsDirectional.only(start: 28.0),
child: top != null
? ProductCard(
imageAspectRatio: imageAspectRatio,
product: top,
)
: SizedBox(
height: heightOfCards > 0 ? heightOfCards : spacerHeight,
),
),
const SizedBox(height: spacerHeight),
Padding(
padding: const EdgeInsetsDirectional.only(end: 28.0),
child: ProductCard(
imageAspectRatio: imageAspectRatio,
product: bottom,
),
),
],
);
});
}
}
class OneProductCardColumn extends StatelessWidget {
const OneProductCardColumn({this.product});
final Product product;
@override
Widget build(BuildContext context) {
return ListView(
physics: const ClampingScrollPhysics(),
reverse: true,
children: <Widget>[
const SizedBox(
height: 40.0,
),
ProductCard(
product: product,
),
],
);
}
}

@ -1,15 +0,0 @@
// Copyright 2016 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/material.dart';
import 'package:flutter_gallery/demo/shrine/app.dart';
class ShrineDemo extends StatelessWidget {
const ShrineDemo({ Key key }) : super(key: key);
static const String routeName = '/shrine'; // Used by the Gallery app.
@override
Widget build(BuildContext context) => ShrineApp();
}

@ -1,194 +0,0 @@
// Copyright 2014 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:ui' show Vertices;
import 'package:flutter/material.dart';
import 'transformations_demo_board.dart';
import 'transformations_demo_edit_board_point.dart';
import 'transformations_demo_gesture_transformable.dart';
class TransformationsDemo extends StatefulWidget {
const TransformationsDemo({ Key key }) : super(key: key);
static const String routeName = '/transformations';
@override _TransformationsDemoState createState() => _TransformationsDemoState();
}
class _TransformationsDemoState extends State<TransformationsDemo> {
// The radius of a hexagon tile in pixels.
static const double _kHexagonRadius = 32.0;
// The margin between hexagons.
static const double _kHexagonMargin = 1.0;
// The radius of the entire board in hexagons, not including the center.
static const int _kBoardRadius = 8;
bool _reset = false;
Board _board = Board(
boardRadius: _kBoardRadius,
hexagonRadius: _kHexagonRadius,
hexagonMargin: _kHexagonMargin,
);
@override
Widget build (BuildContext context) {
final BoardPainter painter = BoardPainter(
board: _board,
);
// The scene is drawn by a CustomPaint, but user interaction is handled by
// the GestureTransformable parent widget.
return Scaffold(
appBar: AppBar(
title: const Text('2D Tranformations'),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.help),
tooltip: 'Help',
onPressed: () {
showDialog<Column>(
context: context,
builder: (BuildContext context) => instructionDialog,
);
},
),
],
),
body: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
// Draw the scene as big as is available, but allow the user to
// translate beyond that to a visibleSize that's a bit bigger.
final Size size = Size(constraints.maxWidth, constraints.maxHeight);
final Size visibleSize = Size(size.width * 3, size.height * 2);
return GestureTransformable(
reset: _reset,
onResetEnd: () {
setState(() {
_reset = false;
});
},
child: CustomPaint(
painter: painter,
),
boundaryRect: Rect.fromLTWH(
-visibleSize.width / 2,
-visibleSize.height / 2,
visibleSize.width,
visibleSize.height,
),
// Center the board in the middle of the screen. It's drawn centered
// at the origin, which is the top left corner of the
// GestureTransformable.
initialTranslation: Offset(size.width / 2, size.height / 2),
onTapUp: _onTapUp,
size: size,
);
},
),
floatingActionButton: _board.selected == null ? resetButton : editButton,
);
}
Widget get instructionDialog {
return AlertDialog(
title: const Text('2D Transformations'),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: const <Widget>[
Text('Tap to edit hex tiles, and use gestures to move around the scene:\n'),
Text('- Drag to pan.'),
Text('- Pinch to zoom.'),
Text('- Rotate with two fingers.'),
Text('\nYou can always press the home button to return to the starting orientation!'),
],
),
actions: <Widget>[
FlatButton(
child: const Text('OK'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
}
FloatingActionButton get resetButton {
return FloatingActionButton(
onPressed: () {
setState(() {
_reset = true;
});
},
tooltip: 'Reset Transform',
backgroundColor: Theme.of(context).primaryColor,
child: const Icon(Icons.home),
);
}
FloatingActionButton get editButton {
return FloatingActionButton(
onPressed: () {
if (_board.selected == null) {
return;
}
showModalBottomSheet<Widget>(context: context, builder: (BuildContext context) {
return Container(
width: double.infinity,
height: 150,
padding: const EdgeInsets.all(12.0),
child: EditBoardPoint(
boardPoint: _board.selected,
onColorSelection: (Color color) {
setState(() {
_board = _board.copyWithBoardPointColor(_board.selected, color);
Navigator.pop(context);
});
},
),
);
});
},
tooltip: 'Edit Tile',
child: const Icon(Icons.edit),
);
}
void _onTapUp(TapUpDetails details) {
final Offset scenePoint = details.globalPosition;
final BoardPoint boardPoint = _board.pointToBoardPoint(scenePoint);
setState(() {
_board = _board.copyWithSelected(boardPoint);
});
}
}
// CustomPainter is what is passed to CustomPaint and actually draws the scene
// when its `paint` method is called.
class BoardPainter extends CustomPainter {
const BoardPainter({
this.board,
});
final Board board;
@override
void paint(Canvas canvas, Size size) {
void drawBoardPoint(BoardPoint boardPoint) {
final Color color = boardPoint.color.withOpacity(
board.selected == boardPoint ? 0.2 : 1.0,
);
final Vertices vertices = board.getVerticesForBoardPoint(boardPoint, color);
canvas.drawVertices(vertices, BlendMode.color, Paint());
}
board.forEach(drawBoardPoint);
}
// We should repaint whenever the board changes, such as board.selected.
@override
bool shouldRepaint(BoardPainter oldDelegate) {
return oldDelegate.board != board;
}
}

@ -1,290 +0,0 @@
// Copyright 2014 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:collection' show IterableMixin;
import 'dart:math';
import 'dart:ui' show Vertices;
import 'package:flutter/material.dart' hide Gradient;
import 'package:vector_math/vector_math_64.dart' show Vector3;
// The entire state of the hex board and abstraction to get information about
// it. Iterable so that all BoardPoints on the board can be iterated over.
@immutable
class Board extends Object with IterableMixin<BoardPoint> {
Board({
@required this.boardRadius,
@required this.hexagonRadius,
@required this.hexagonMargin,
this.selected,
List<BoardPoint> boardPoints,
}) : assert(boardRadius > 0),
assert(hexagonRadius > 0),
assert(hexagonMargin >= 0) {
// Set up the positions for the center hexagon where the entire board is
// centered on the origin.
// Start point of hexagon (top vertex).
final Point<double> hexStart = Point<double>(0, -hexagonRadius);
final double hexagonRadiusPadded = hexagonRadius - hexagonMargin;
final double centerToFlat = sqrt(3) / 2 * hexagonRadiusPadded;
positionsForHexagonAtOrigin.addAll(<Offset>[
Offset(hexStart.x, hexStart.y),
Offset(hexStart.x + centerToFlat, hexStart.y + 0.5 * hexagonRadiusPadded),
Offset(hexStart.x + centerToFlat, hexStart.y + 1.5 * hexagonRadiusPadded),
Offset(hexStart.x + centerToFlat, hexStart.y + 1.5 * hexagonRadiusPadded),
Offset(hexStart.x, hexStart.y + 2 * hexagonRadiusPadded),
Offset(hexStart.x, hexStart.y + 2 * hexagonRadiusPadded),
Offset(hexStart.x - centerToFlat, hexStart.y + 1.5 * hexagonRadiusPadded),
Offset(hexStart.x - centerToFlat, hexStart.y + 1.5 * hexagonRadiusPadded),
Offset(hexStart.x - centerToFlat, hexStart.y + 0.5 * hexagonRadiusPadded),
]);
if (boardPoints != null) {
_boardPoints.addAll(boardPoints);
} else {
// Generate boardPoints for a fresh board.
BoardPoint boardPoint = _getNextBoardPoint(null);
while (boardPoint != null) {
_boardPoints.add(boardPoint);
boardPoint = _getNextBoardPoint(boardPoint);
}
}
}
final int boardRadius; // Number of hexagons from center to edge.
final double hexagonRadius; // Pixel radius of a hexagon (center to vertex).
final double hexagonMargin; // Margin between hexagons.
final List<Offset> positionsForHexagonAtOrigin = <Offset>[];
final BoardPoint selected;
final List<BoardPoint> _boardPoints = <BoardPoint>[];
@override
Iterator<BoardPoint> get iterator => _BoardIterator(_boardPoints);
// For a given q axial coordinate, get the range of possible r values
// See the definition of BoardPoint for more information about hex grids and
// axial coordinates.
_Range _getRRangeForQ(int q) {
int rStart;
int rEnd;
if (q <= 0) {
rStart = -boardRadius - q;
rEnd = boardRadius;
} else {
rEnd = boardRadius - q;
rStart = -boardRadius;
}
return _Range(rStart, rEnd);
}
// Get the BoardPoint that comes after the given BoardPoint. If given null,
// returns the origin BoardPoint. If given BoardPoint is the last, returns
// null.
BoardPoint _getNextBoardPoint (BoardPoint boardPoint) {
// If before the first element.
if (boardPoint == null) {
return BoardPoint(-boardRadius, 0);
}
final _Range rRange = _getRRangeForQ(boardPoint.q);
// If at or after the last element.
if (boardPoint.q >= boardRadius && boardPoint.r >= rRange.max) {
return null;
}
// If wrapping from one q to the next.
if (boardPoint.r >= rRange.max) {
return BoardPoint(boardPoint.q + 1, _getRRangeForQ(boardPoint.q + 1).min);
}
// Otherwise we're just incrementing r.
return BoardPoint(boardPoint.q, boardPoint.r + 1);
}
// Check if the board point is actually on the board.
bool _validateBoardPoint(BoardPoint boardPoint) {
const BoardPoint center = BoardPoint(0, 0);
final int distanceFromCenter = getDistance(center, boardPoint);
return distanceFromCenter <= boardRadius;
}
// Get the distance between two BoardPoins.
static int getDistance(BoardPoint a, BoardPoint b) {
final Vector3 a3 = a.cubeCoordinates;
final Vector3 b3 = b.cubeCoordinates;
return
((a3.x - b3.x).abs() + (a3.y - b3.y).abs() + (a3.z - b3.z).abs()) ~/ 2;
}
// Return the q,r BoardPoint for a point in the scene, where the origin is in
// the center of the board in both coordinate systems. If no BoardPoint at the
// location, return null.
BoardPoint pointToBoardPoint(Offset point) {
final BoardPoint boardPoint = BoardPoint(
((sqrt(3) / 3 * point.dx - 1 / 3 * point.dy) / hexagonRadius).round(),
((2 / 3 * point.dy) / hexagonRadius).round(),
);
if (!_validateBoardPoint(boardPoint)) {
return null;
}
return _boardPoints.firstWhere((BoardPoint boardPointI) {
return boardPointI.q == boardPoint.q && boardPointI.r == boardPoint.r;
});
}
// Return a scene point for the center of a hexagon given its q,r point.
Point<double> boardPointToPoint(BoardPoint boardPoint) {
return Point<double>(
sqrt(3) * hexagonRadius * boardPoint.q + sqrt(3) / 2 * hexagonRadius * boardPoint.r,
1.5 * hexagonRadius * boardPoint.r,
);
}
// Get Vertices that can be drawn to a Canvas for the given BoardPoint.
Vertices getVerticesForBoardPoint(BoardPoint boardPoint, Color color) {
final Point<double> centerOfHexZeroCenter = boardPointToPoint(boardPoint);
final List<Offset> positions = positionsForHexagonAtOrigin.map((Offset offset) {
return offset.translate(centerOfHexZeroCenter.x, centerOfHexZeroCenter.y);
}).toList();
return Vertices(
VertexMode.triangleFan,
positions,
colors: List<Color>.filled(positions.length, color),
);
}
// Return a new board with the given BoardPoint selected.
Board copyWithSelected(BoardPoint boardPoint) {
if (selected == boardPoint) {
return this;
}
final Board nextBoard = Board(
boardRadius: boardRadius,
hexagonRadius: hexagonRadius,
hexagonMargin: hexagonMargin,
selected: boardPoint,
boardPoints: _boardPoints,
);
return nextBoard;
}
// Return a new board where boardPoint has the given color.
Board copyWithBoardPointColor(BoardPoint boardPoint, Color color) {
final BoardPoint nextBoardPoint = boardPoint.copyWithColor(color);
final int boardPointIndex = _boardPoints.indexWhere((BoardPoint boardPointI) =>
boardPointI.q == boardPoint.q && boardPointI.r == boardPoint.r
);
if (elementAt(boardPointIndex) == boardPoint && boardPoint.color == color) {
return this;
}
final List<BoardPoint> nextBoardPoints = List<BoardPoint>.from(_boardPoints);
nextBoardPoints[boardPointIndex] = nextBoardPoint;
final BoardPoint selectedBoardPoint = boardPoint == selected
? nextBoardPoint
: selected;
return Board(
boardRadius: boardRadius,
hexagonRadius: hexagonRadius,
hexagonMargin: hexagonMargin,
selected: selectedBoardPoint,
boardPoints: nextBoardPoints,
);
}
}
class _BoardIterator extends Iterator<BoardPoint> {
_BoardIterator(this.boardPoints);
final List<BoardPoint> boardPoints;
int currentIndex;
@override
BoardPoint current;
@override
bool moveNext() {
if (currentIndex == null) {
currentIndex = 0;
} else {
currentIndex++;
}
if (currentIndex >= boardPoints.length) {
current = null;
return false;
}
current = boardPoints[currentIndex];
return true;
}
}
// A range of q/r board coordinate values.
@immutable
class _Range {
const _Range(this.min, this.max)
: assert(min != null),
assert(max != null),
assert(min <= max);
final int min;
final int max;
}
final Set<Color> boardPointColors = <Color>{
Colors.grey,
Colors.black,
Colors.red,
Colors.blue,
};
// A location on the board in axial coordinates.
// Axial coordinates use two integers, q and r, to locate a hexagon on a grid.
// https://www.redblobgames.com/grids/hexagons/#coordinates-axial
@immutable
class BoardPoint {
const BoardPoint(this.q, this.r, {
this.color = Colors.grey,
});
final int q;
final int r;
final Color color;
@override
String toString() {
return 'BoardPoint($q, $r, $color)';
}
// Only compares by location.
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType) {
return false;
}
final BoardPoint boardPoint = other;
return boardPoint.q == q && boardPoint.r == r;
}
@override
int get hashCode => hashValues(q, r);
BoardPoint copyWithColor(Color nextColor) => BoardPoint(q, r, color: nextColor);
// Convert from q,r axial coords to x,y,z cube coords.
Vector3 get cubeCoordinates {
return Vector3(
q.toDouble(),
r.toDouble(),
(-q - r).toDouble(),
);
}
}

@ -1,74 +0,0 @@
// Copyright 2014 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/material.dart';
// A generic widget for a list of selectable colors.
@immutable
class ColorPicker extends StatelessWidget {
const ColorPicker({
@required this.colors,
@required this.selectedColor,
this.onColorSelection,
}) : assert(colors != null),
assert(selectedColor != null);
final Set<Color> colors;
final Color selectedColor;
final ValueChanged<Color> onColorSelection;
@override
Widget build (BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: colors.map((Color color) {
return _ColorPickerSwatch(
color: color,
selected: color == selectedColor,
onTap: () {
if (onColorSelection != null) {
onColorSelection(color);
}
},
);
}).toList(),
);
}
}
// A single selectable color widget in the ColorPicker.
@immutable
class _ColorPickerSwatch extends StatelessWidget {
const _ColorPickerSwatch({
@required this.color,
@required this.selected,
this.onTap,
}) : assert(color != null),
assert(selected != null);
final Color color;
final bool selected;
final Function onTap;
@override
Widget build (BuildContext context) {
return Container(
width: 60.0,
height: 60.0,
padding: const EdgeInsets.fromLTRB(2.0, 0.0, 2.0, 0.0),
child: RawMaterialButton(
fillColor: color,
onPressed: () {
if (onTap != null) {
onTap();
}
},
child: !selected ? null : const Icon(
Icons.check,
color: Colors.white,
),
),
);
}
}

@ -1,40 +0,0 @@
// Copyright 2014 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/material.dart';
import 'transformations_demo_board.dart';
import 'transformations_demo_color_picker.dart';
// The panel for editing a board point.
@immutable
class EditBoardPoint extends StatelessWidget {
const EditBoardPoint({
Key key,
@required this.boardPoint,
this.onColorSelection,
}) : assert(boardPoint != null),
super(key: key);
final BoardPoint boardPoint;
final ValueChanged<Color> onColorSelection;
@override
Widget build (BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
'${boardPoint.q}, ${boardPoint.r}',
textAlign: TextAlign.right,
style: const TextStyle(fontWeight: FontWeight.bold),
),
ColorPicker(
colors: boardPointColors,
selectedColor: boardPoint.color,
onColorSelection: onColorSelection,
),
],
);
}
}

@ -1,575 +0,0 @@
// Copyright 2014 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/material.dart';
import 'package:vector_math/vector_math_64.dart' show Vector3;
import 'transformations_demo_inertial_motion.dart';
// This widget allows 2D transform interactions on its child in relation to its
// parent. The user can transform the child by dragging to pan or pinching to
// zoom and rotate. All event callbacks for GestureDetector are supported, and
// the coordinates that are given are untransformed and in relation to the
// original position of the child.
@immutable
class GestureTransformable extends StatefulWidget {
const GestureTransformable({
Key key,
// The child to perform the transformations on.
@required this.child,
// The desired visible size of the widget and the area that is receptive to
// gestures. If a widget that's as big as possible is desired, then wrap
// this in a LayoutBuilder and pass
// `Size(constraints.maxWidth, constraints.maxHeight)`.
@required this.size,
// The scale will be clamped to between these values. A maxScale of null has
// no bounds. minScale must be greater than zero.
this.maxScale = 2.5,
this.minScale = 0.8,
// Transforms will be limited so that the viewport can not view beyond this
// Rect. The Rect does not rotate with the rest of the scene, so it is
// always aligned with the viewport. A null boundaryRect results in no
// limits to the distance that the viewport can be transformed to see.
this.boundaryRect,
// Initial values for the transform can be provided.
this.initialTranslation,
this.initialScale,
this.initialRotation,
// Any and all of the possible transformations can be disabled.
this.disableTranslation = false,
this.disableScale = false,
this.disableRotation = false,
// If set to true, this widget will animate back to its initial transform
// and call onResetEnd when done. When utilizing reset, onResetEnd should
// also be implemented, and it should set reset to false when called.
this.reset = false,
// Access to event callbacks from GestureDetector. Called with untransformed
// coordinates in an Offset.
this.onTapDown,
this.onTapUp,
this.onTap,
this.onTapCancel,
this.onDoubleTap,
this.onLongPress,
this.onLongPressUp,
this.onVerticalDragDown,
this.onVerticalDragStart,
this.onVerticalDragUpdate,
this.onVerticalDragEnd,
this.onVerticalDragCancel,
this.onHorizontalDragDown,
this.onHorizontalDragStart,
this.onHorizontalDragUpdate,
this.onHorizontalDragEnd,
this.onHorizontalDragCancel,
this.onPanDown,
this.onPanStart,
this.onPanUpdate,
this.onPanEnd,
this.onPanCancel,
this.onResetEnd,
this.onScaleStart,
this.onScaleUpdate,
this.onScaleEnd,
}) : assert(child != null),
assert(size != null),
assert(minScale != null),
assert(minScale > 0),
assert(disableTranslation != null),
assert(disableScale != null),
assert(disableRotation != null),
assert(reset != null),
assert(
!reset || onResetEnd != null,
'Must implement onResetEnd to use reset.',
),
super(key: key);
final Widget child;
final Size size;
final bool reset;
final GestureTapDownCallback onTapDown;
final GestureTapUpCallback onTapUp;
final GestureTapCallback onTap;
final GestureTapCancelCallback onTapCancel;
final GestureTapCallback onDoubleTap;
final GestureLongPressCallback onLongPress;
final GestureLongPressUpCallback onLongPressUp;
final GestureDragDownCallback onVerticalDragDown;
final GestureDragStartCallback onVerticalDragStart;
final GestureDragUpdateCallback onVerticalDragUpdate;
final GestureDragEndCallback onVerticalDragEnd;
final GestureDragCancelCallback onVerticalDragCancel;
final GestureDragDownCallback onHorizontalDragDown;
final GestureDragStartCallback onHorizontalDragStart;
final GestureDragUpdateCallback onHorizontalDragUpdate;
final GestureDragEndCallback onHorizontalDragEnd;
final GestureDragCancelCallback onHorizontalDragCancel;
final GestureDragDownCallback onPanDown;
final GestureDragStartCallback onPanStart;
final GestureDragUpdateCallback onPanUpdate;
final GestureDragEndCallback onPanEnd;
final GestureDragCancelCallback onPanCancel;
final VoidCallback onResetEnd;
final GestureScaleStartCallback onScaleStart;
final GestureScaleUpdateCallback onScaleUpdate;
final GestureScaleEndCallback onScaleEnd;
final double maxScale;
final double minScale;
final Rect boundaryRect;
final bool disableTranslation;
final bool disableScale;
final bool disableRotation;
final Offset initialTranslation;
final double initialScale;
final double initialRotation;
@override _GestureTransformableState createState() => _GestureTransformableState();
}
// A single user event can only represent one of these gestures. The user can't
// do multiple at the same time, which results in more precise transformations.
enum _GestureType {
translate,
scale,
rotate,
}
// This is public only for access from a unit test.
class _GestureTransformableState extends State<GestureTransformable> with TickerProviderStateMixin {
Animation<Offset> _animation;
AnimationController _controller;
Animation<Matrix4> _animationReset;
AnimationController _controllerReset;
// The translation that will be applied to the scene (not viewport).
// A positive x offset moves the scene right, viewport left.
// A positive y offset moves the scene down, viewport up.
Offset _translateFromScene; // Point where a single translation began.
double _scaleStart; // Scale value at start of scaling gesture.
double _rotationStart = 0.0; // Rotation at start of rotation gesture.
Rect _boundaryRect;
Matrix4 _transform = Matrix4.identity();
double _currentRotation = 0.0;
_GestureType gestureType;
// The transformation matrix that gives the initial home position.
Matrix4 get _initialTransform {
Matrix4 matrix = Matrix4.identity();
if (widget.initialTranslation != null) {
matrix = matrixTranslate(matrix, widget.initialTranslation);
}
if (widget.initialScale != null) {
matrix = matrixScale(matrix, widget.initialScale);
}
if (widget.initialRotation != null) {
matrix = matrixRotate(matrix, widget.initialRotation, Offset.zero);
}
return matrix;
}
// Return the scene point at the given viewport point.
static Offset fromViewport(Offset viewportPoint, Matrix4 transform) {
// On viewportPoint, perform the inverse transformation of the scene to get
// where the point would be in the scene before the transformation.
final Matrix4 inverseMatrix = Matrix4.inverted(transform);
final Vector3 untransformed = inverseMatrix.transform3(Vector3(
viewportPoint.dx,
viewportPoint.dy,
0,
));
return Offset(untransformed.x, untransformed.y);
}
// Get the offset of the current widget from the global screen coordinates.
// TODO(justinmc): Protect against calling this during first build.
static Offset getOffset(BuildContext context) {
final RenderBox renderObject = context.findRenderObject();
return renderObject.localToGlobal(Offset.zero);
}
@override
void initState() {
super.initState();
_boundaryRect = widget.boundaryRect ?? Offset.zero & widget.size;
_transform = _initialTransform;
_controller = AnimationController(
vsync: this,
);
_controllerReset = AnimationController(
vsync: this,
);
if (widget.reset) {
_animateResetInitialize();
}
}
@override
void didUpdateWidget(GestureTransformable oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.reset && !oldWidget.reset && _animationReset == null) {
_animateResetInitialize();
} else if (!widget.reset && oldWidget.reset && _animationReset != null) {
_animateResetStop();
}
}
@override
Widget build(BuildContext context) {
// A GestureDetector allows the detection of panning and zooming gestures on
// its child, which is the CustomPaint.
return GestureDetector(
behavior: HitTestBehavior.opaque, // Necessary when translating off screen
onTapDown: widget.onTapDown == null ? null : (TapDownDetails details) {
widget.onTapDown(TapDownDetails(
globalPosition: fromViewport(details.globalPosition - getOffset(context), _transform),
));
},
onTapUp: widget.onTapUp == null ? null : (TapUpDetails details) {
widget.onTapUp(TapUpDetails(
globalPosition: fromViewport(details.globalPosition - getOffset(context), _transform),
));
},
onTap: widget.onTap,
onTapCancel: widget.onTapCancel,
onDoubleTap: widget.onDoubleTap,
onLongPress: widget.onLongPress,
onLongPressUp: widget.onLongPressUp,
onVerticalDragDown: widget.onVerticalDragDown == null ? null : (DragDownDetails details) {
widget.onVerticalDragDown(DragDownDetails(
globalPosition: fromViewport(details.globalPosition - getOffset(context), _transform),
));
},
onVerticalDragStart: widget.onVerticalDragStart == null ? null : (DragStartDetails details) {
widget.onVerticalDragStart(DragStartDetails(
globalPosition: fromViewport(details.globalPosition - getOffset(context), _transform),
));
},
onVerticalDragUpdate: widget.onVerticalDragUpdate == null ? null : (DragUpdateDetails details) {
widget.onVerticalDragUpdate(DragUpdateDetails(
globalPosition: fromViewport(details.globalPosition - getOffset(context), _transform),
));
},
onVerticalDragEnd: widget.onVerticalDragEnd,
onVerticalDragCancel: widget.onVerticalDragCancel,
onHorizontalDragDown: widget.onHorizontalDragDown == null ? null : (DragDownDetails details) {
widget.onHorizontalDragDown(DragDownDetails(
globalPosition: fromViewport(details.globalPosition - getOffset(context), _transform),
));
},
onHorizontalDragStart: widget.onHorizontalDragStart == null ? null : (DragStartDetails details) {
widget.onHorizontalDragStart(DragStartDetails(
globalPosition: fromViewport(details.globalPosition - getOffset(context), _transform),
));
},
onHorizontalDragUpdate: widget.onHorizontalDragUpdate == null ? null : (DragUpdateDetails details) {
widget.onHorizontalDragUpdate(DragUpdateDetails(
globalPosition: fromViewport(details.globalPosition - getOffset(context), _transform),
));
},
onHorizontalDragEnd: widget.onHorizontalDragEnd,
onHorizontalDragCancel: widget.onHorizontalDragCancel,
onPanDown: widget.onPanDown == null ? null : (DragDownDetails details) {
widget.onPanDown(DragDownDetails(
globalPosition: fromViewport(details.globalPosition - getOffset(context), _transform),
));
},
onPanStart: widget.onPanStart == null ? null : (DragStartDetails details) {
widget.onPanStart(DragStartDetails(
globalPosition: fromViewport(details.globalPosition - getOffset(context), _transform),
));
},
onPanUpdate: widget.onPanUpdate == null ? null : (DragUpdateDetails details) {
widget.onPanUpdate(DragUpdateDetails(
globalPosition: fromViewport(details.globalPosition - getOffset(context), _transform),
));
},
onPanEnd: widget.onPanEnd,
onPanCancel: widget.onPanCancel,
onScaleEnd: _onScaleEnd,
onScaleStart: _onScaleStart,
onScaleUpdate: _onScaleUpdate,
child: ClipRect(
// The scene is panned/zoomed/rotated using this Transform widget.
child: Transform(
transform: _transform,
child: Container(
child: widget.child,
height: widget.size.height,
width: widget.size.width,
),
),
),
);
}
// Return a new matrix representing the given matrix after applying the given
// translation.
Matrix4 matrixTranslate(Matrix4 matrix, Offset translation) {
if (widget.disableTranslation || translation == Offset.zero) {
return matrix;
}
// Clamp translation so the viewport remains inside _boundaryRect.
final double scale = _transform.getMaxScaleOnAxis();
final Size scaledSize = widget.size / scale;
final Rect viewportBoundaries = Rect.fromLTRB(
_boundaryRect.left,
_boundaryRect.top,
_boundaryRect.right - scaledSize.width,
_boundaryRect.bottom - scaledSize.height,
);
// Translation is reversed (a positive translation moves the scene to the
// right, viewport to the left).
final Rect translationBoundaries = Rect.fromLTRB(
-scale * viewportBoundaries.right,
-scale * viewportBoundaries.bottom,
-scale * viewportBoundaries.left,
-scale * viewportBoundaries.top,
);
final Matrix4 nextMatrix = matrix.clone()..translate(
translation.dx,
translation.dy,
);
final Vector3 nextTranslationVector = nextMatrix.getTranslation();
final Offset nextTranslation = Offset(
nextTranslationVector.x,
nextTranslationVector.y,
);
final bool inBoundaries = translationBoundaries.contains(
Offset(nextTranslation.dx, nextTranslation.dy),
);
if (!inBoundaries) {
// TODO(justinmc): Instead of canceling translation when it goes out of
// bounds, stop translation at boundary.
return matrix;
}
return nextMatrix;
}
// Return a new matrix representing the given matrix after applying the given
// scale transform.
Matrix4 matrixScale(Matrix4 matrix, double scale) {
if (widget.disableScale || scale == 1) {
return matrix;
}
assert(scale != 0);
// Don't allow a scale that moves the viewport outside of _boundaryRect.
final Offset tl = fromViewport(const Offset(0, 0), _transform);
final Offset tr = fromViewport(Offset(widget.size.width, 0), _transform);
final Offset bl = fromViewport(Offset(0, widget.size.height), _transform);
final Offset br = fromViewport(
Offset(widget.size.width, widget.size.height),
_transform,
);
if (!_boundaryRect.contains(tl)
|| !_boundaryRect.contains(tr)
|| !_boundaryRect.contains(bl)
|| !_boundaryRect.contains(br)) {
return matrix;
}
// Don't allow a scale that results in an overall scale beyond min/max
// scale.
final double currentScale = _transform.getMaxScaleOnAxis();
final double totalScale = currentScale * scale;
final double clampedTotalScale = totalScale.clamp(
widget.minScale,
widget.maxScale,
);
final double clampedScale = clampedTotalScale / currentScale;
return matrix..scale(clampedScale);
}
// Return a new matrix representing the given matrix after applying the given
// rotation transform.
// Rotating the scene cannot cause the viewport to view beyond _boundaryRect.
Matrix4 matrixRotate(Matrix4 matrix, double rotation, Offset focalPoint) {
if (widget.disableRotation || rotation == 0) {
return matrix;
}
final Offset focalPointScene = fromViewport(focalPoint, matrix);
return matrix
..translate(focalPointScene.dx, focalPointScene.dy)
..rotateZ(-rotation)
..translate(-focalPointScene.dx, -focalPointScene.dy);
}
// Handle the start of a gesture of _GestureType.
void _onScaleStart(ScaleStartDetails details) {
if (widget.onScaleStart != null) {
widget.onScaleStart(details);
}
if (_controller.isAnimating) {
_controller.stop();
_controller.reset();
_animation?.removeListener(_onAnimate);
_animation = null;
}
if (_controllerReset.isAnimating) {
_animateResetStop();
}
gestureType = null;
setState(() {
_scaleStart = _transform.getMaxScaleOnAxis();
_translateFromScene = fromViewport(details.focalPoint, _transform);
_rotationStart = _currentRotation;
});
}
// Handle an update to an ongoing gesture of _GestureType.
void _onScaleUpdate(ScaleUpdateDetails details) {
double scale = _transform.getMaxScaleOnAxis();
if (widget.onScaleUpdate != null) {
widget.onScaleUpdate(ScaleUpdateDetails(
focalPoint: fromViewport(details.focalPoint, _transform),
scale: details.scale,
rotation: details.rotation,
));
}
final Offset focalPointScene = fromViewport(
details.focalPoint,
_transform,
);
if (gestureType == null) {
// Decide which type of gesture this is by comparing the amount of scale
// and rotation in the gesture, if any. Scale starts at 1 and rotation
// starts at 0. Translate will have 0 scale and 0 rotation because it uses
// only one finger.
if ((details.scale - 1).abs() > details.rotation.abs()) {
gestureType = _GestureType.scale;
} else if (details.rotation != 0) {
gestureType = _GestureType.rotate;
} else {
gestureType = _GestureType.translate;
}
}
setState(() {
if (gestureType == _GestureType.scale && _scaleStart != null) {
// details.scale gives us the amount to change the scale as of the
// start of this gesture, so calculate the amount to scale as of the
// previous call to _onScaleUpdate.
final double desiredScale = _scaleStart * details.scale;
final double scaleChange = desiredScale / scale;
_transform = matrixScale(_transform, scaleChange);
scale = _transform.getMaxScaleOnAxis();
// While scaling, translate such that the user's two fingers stay on the
// same places in the scene. That means that the focal point of the
// scale should be on the same place in the scene before and after the
// scale.
final Offset focalPointSceneNext = fromViewport(
details.focalPoint,
_transform,
);
_transform = matrixTranslate(_transform, focalPointSceneNext - focalPointScene);
} else if (gestureType == _GestureType.rotate && details.rotation != 0.0) {
final double desiredRotation = _rotationStart + details.rotation;
_transform = matrixRotate(_transform, _currentRotation - desiredRotation, details.focalPoint);
_currentRotation = desiredRotation;
} else if (_translateFromScene != null && details.scale == 1.0) {
// Translate so that the same point in the scene is underneath the
// focal point before and after the movement.
final Offset translationChange = focalPointScene - _translateFromScene;
_transform = matrixTranslate(_transform, translationChange);
_translateFromScene = fromViewport(details.focalPoint, _transform);
}
});
}
// Handle the end of a gesture of _GestureType.
void _onScaleEnd(ScaleEndDetails details) {
if (widget.onScaleEnd != null) {
widget.onScaleEnd(details);
}
setState(() {
_scaleStart = null;
_rotationStart = null;
_translateFromScene = null;
});
_animation?.removeListener(_onAnimate);
_controller.reset();
// If the scale ended with velocity, animate inertial movement
final double velocityTotal = details.velocity.pixelsPerSecond.dx.abs()
+ details.velocity.pixelsPerSecond.dy.abs();
if (velocityTotal == 0) {
return;
}
final Vector3 translationVector = _transform.getTranslation();
final Offset translation = Offset(translationVector.x, translationVector.y);
final InertialMotion inertialMotion = InertialMotion(details.velocity, translation);
_animation = Tween<Offset>(
begin: translation,
end: inertialMotion.finalPosition,
).animate(_controller);
_controller.duration = Duration(milliseconds: inertialMotion.duration.toInt());
_animation.addListener(_onAnimate);
_controller.fling();
}
// Handle inertia drag animation.
void _onAnimate() {
setState(() {
// Translate _transform such that the resulting translation is
// _animation.value.
final Vector3 translationVector = _transform.getTranslation();
final Offset translation = Offset(translationVector.x, translationVector.y);
final Offset translationScene = fromViewport(translation, _transform);
final Offset animationScene = fromViewport(_animation.value, _transform);
final Offset translationChangeScene = animationScene - translationScene;
_transform = matrixTranslate(_transform, translationChangeScene);
});
if (!_controller.isAnimating) {
_animation?.removeListener(_onAnimate);
_animation = null;
_controller.reset();
}
}
// Handle reset to home transform animation.
void _onAnimateReset() {
setState(() {
_transform = _animationReset.value;
});
if (!_controllerReset.isAnimating) {
_animationReset?.removeListener(_onAnimateReset);
_animationReset = null;
_controllerReset.reset();
widget.onResetEnd();
}
}
// Initialize the reset to home transform animation.
void _animateResetInitialize() {
_controllerReset.reset();
_animationReset = Matrix4Tween(
begin: _transform,
end: _initialTransform,
).animate(_controllerReset);
_controllerReset.duration = const Duration(milliseconds: 400);
_animationReset.addListener(_onAnimateReset);
_controllerReset.forward();
}
// Stop a running reset to home transform animation.
void _animateResetStop() {
_controllerReset.stop();
_animationReset?.removeListener(_onAnimateReset);
_animationReset = null;
_controllerReset.reset();
widget.onResetEnd();
}
@override
void dispose() {
_controller.dispose();
_controllerReset.dispose();
super.dispose();
}
}

@ -1,72 +0,0 @@
// Copyright 2014 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';
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math.dart' show Vector2;
// Provides calculations for an object moving with inertia and friction using
// the equation of motion from physics.
// https://en.wikipedia.org/wiki/Equations_of_motion#Constant_translational_acceleration_in_a_straight_line
// TODO(justinmc): Can this be replaced with friction_simulation.dart?
@immutable
class InertialMotion {
const InertialMotion(this._initialVelocity, this._initialPosition);
static const double _kFrictionalAcceleration = 0.01; // How quickly to stop
final Velocity _initialVelocity;
final Offset _initialPosition;
// The position when the motion stops.
Offset get finalPosition {
return _getPositionAt(Duration(milliseconds: duration.toInt()));
}
// The total time that the animation takes start to stop in milliseconds.
double get duration {
return (_initialVelocity.pixelsPerSecond.dx / 1000 / _acceleration.x).abs();
}
// The acceleration opposing the initial velocity in x and y components.
Vector2 get _acceleration {
// TODO(justinmc): Find actual velocity instead of summing?
final double velocityTotal = _initialVelocity.pixelsPerSecond.dx.abs()
+ _initialVelocity.pixelsPerSecond.dy.abs();
final double vRatioX = _initialVelocity.pixelsPerSecond.dx / velocityTotal;
final double vRatioY = _initialVelocity.pixelsPerSecond.dy / velocityTotal;
return Vector2(
_kFrictionalAcceleration * vRatioX,
_kFrictionalAcceleration * vRatioY,
);
}
// The position at a given time.
Offset _getPositionAt(Duration time) {
final double xf = _getPosition(
r0: _initialPosition.dx,
v0: _initialVelocity.pixelsPerSecond.dx / 1000,
t: time.inMilliseconds,
a: _acceleration.x,
);
final double yf = _getPosition(
r0: _initialPosition.dy,
v0: _initialVelocity.pixelsPerSecond.dy / 1000,
t: time.inMilliseconds,
a: _acceleration.y,
);
return Offset(xf, yf);
}
// Solve the equation of motion to find the position at a given point in time
// in one dimension.
double _getPosition({double r0, double v0, int t, double a}) {
// Stop movement when it would otherwise reverse direction.
final double stopTime = (v0 / a).abs();
if (t > stopTime) {
t = stopTime.toInt();
}
return r0 + v0 * t + 0.5 * a * pow(t, 2);
}
}

@ -1,74 +0,0 @@
// Copyright 2016 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/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>[
if (MediaQuery.of(context).size.width > 500.0)
TextStyleItem(name: 'Display 4', style: textTheme.display4, text: 'Light 112sp'),
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'),
];
return Scaffold(
appBar: AppBar(title: const Text('Typography')),
body: SafeArea(
top: false,
bottom: false,
child: Scrollbar(child: ListView(children: styleItems)),
),
);
}
}

@ -1,434 +0,0 @@
// Copyright 2017 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 'dart:io';
import 'package:connectivity/connectivity.dart';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'package:device_info/device_info.dart';
class VideoCard extends StatelessWidget {
const VideoCard({ Key key, this.controller, this.title, this.subtitle }) : super(key: key);
final VideoPlayerController controller;
final String title;
final String subtitle;
Widget _buildInlineVideo() {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 30.0),
child: Center(
child: AspectRatio(
aspectRatio: 3 / 2,
child: Hero(
tag: controller,
child: VideoPlayerLoading(controller),
),
),
),
);
}
Widget _buildFullScreenVideo() {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: AspectRatio(
aspectRatio: 3 / 2,
child: Hero(
tag: controller,
child: VideoPlayPause(controller),
),
),
),
);
}
@override
Widget build(BuildContext context) {
Widget fullScreenRoutePageBuilder(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return _buildFullScreenVideo();
}
void pushFullScreenWidget() {
final TransitionRoute<void> route = PageRouteBuilder<void>(
settings: RouteSettings(name: title, isInitialRoute: false),
pageBuilder: fullScreenRoutePageBuilder,
);
route.completed.then((void value) {
controller.setVolume(0.0);
});
controller.setVolume(1.0);
Navigator.of(context).push(route);
}
return SafeArea(
top: false,
bottom: false,
child: Card(
child: Column(
children: <Widget>[
ListTile(title: Text(title), subtitle: Text(subtitle)),
GestureDetector(
onTap: pushFullScreenWidget,
child: _buildInlineVideo(),
),
],
),
),
);
}
}
class VideoPlayerLoading extends StatefulWidget {
const VideoPlayerLoading(this.controller);
final VideoPlayerController controller;
@override
_VideoPlayerLoadingState createState() => _VideoPlayerLoadingState();
}
class _VideoPlayerLoadingState extends State<VideoPlayerLoading> {
bool _initialized;
@override
void initState() {
super.initState();
_initialized = widget.controller.value.initialized;
widget.controller.addListener(() {
if (!mounted) {
return;
}
final bool controllerInitialized = widget.controller.value.initialized;
if (_initialized != controllerInitialized) {
setState(() {
_initialized = controllerInitialized;
});
}
});
}
@override
Widget build(BuildContext context) {
if (_initialized) {
return VideoPlayer(widget.controller);
}
return Stack(
children: <Widget>[
VideoPlayer(widget.controller),
const Center(child: CircularProgressIndicator()),
],
fit: StackFit.expand,
);
}
}
class VideoPlayPause extends StatefulWidget {
const VideoPlayPause(this.controller);
final VideoPlayerController controller;
@override
State createState() => _VideoPlayPauseState();
}
class _VideoPlayPauseState extends State<VideoPlayPause> {
_VideoPlayPauseState() {
listener = () {
if (mounted)
setState(() { });
};
}
FadeAnimation imageFadeAnimation;
VoidCallback listener;
VideoPlayerController get controller => widget.controller;
@override
void initState() {
super.initState();
controller.addListener(listener);
}
@override
void deactivate() {
controller.removeListener(listener);
super.deactivate();
}
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.bottomCenter,
fit: StackFit.expand,
children: <Widget>[
GestureDetector(
child: VideoPlayerLoading(controller),
onTap: () {
if (!controller.value.initialized) {
return;
}
if (controller.value.isPlaying) {
imageFadeAnimation = const FadeAnimation(
child: Icon(Icons.pause, size: 100.0),
);
controller.pause();
} else {
imageFadeAnimation = const FadeAnimation(
child: Icon(Icons.play_arrow, size: 100.0),
);
controller.play();
}
},
),
Center(child: imageFadeAnimation),
],
);
}
}
class FadeAnimation extends StatefulWidget {
const FadeAnimation({
this.child,
this.duration = const Duration(milliseconds: 500),
});
final Widget child;
final Duration duration;
@override
_FadeAnimationState createState() => _FadeAnimationState();
}
class _FadeAnimationState extends State<FadeAnimation> with SingleTickerProviderStateMixin {
AnimationController animationController;
@override
void initState() {
super.initState();
animationController = AnimationController(
duration: widget.duration,
vsync: this,
);
animationController.addListener(() {
if (mounted) {
setState(() { });
}
});
animationController.forward(from: 0.0);
}
@override
void deactivate() {
animationController.stop();
super.deactivate();
}
@override
void didUpdateWidget(FadeAnimation oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.child != widget.child) {
animationController.forward(from: 0.0);
}
}
@override
void dispose() {
animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return animationController.isAnimating
? Opacity(
opacity: 1.0 - animationController.value,
child: widget.child,
)
: Container();
}
}
class ConnectivityOverlay extends StatefulWidget {
const ConnectivityOverlay({
this.child,
this.connectedCompleter,
this.scaffoldKey,
});
final Widget child;
final Completer<void> connectedCompleter;
final GlobalKey<ScaffoldState> scaffoldKey;
@override
_ConnectivityOverlayState createState() => _ConnectivityOverlayState();
}
class _ConnectivityOverlayState extends State<ConnectivityOverlay> {
StreamSubscription<ConnectivityResult> connectivitySubscription;
bool connected = true;
static const Widget errorSnackBar = SnackBar(
backgroundColor: Colors.red,
content: ListTile(
title: Text('No network'),
subtitle: Text(
'To load the videos you must have an active network connection',
),
),
);
Stream<ConnectivityResult> connectivityStream() async* {
final Connectivity connectivity = Connectivity();
ConnectivityResult previousResult = await connectivity.checkConnectivity();
yield previousResult;
await for (ConnectivityResult result
in connectivity.onConnectivityChanged) {
if (result != previousResult) {
yield result;
previousResult = result;
}
}
}
@override
void initState() {
super.initState();
connectivitySubscription = connectivityStream().listen(
(ConnectivityResult connectivityResult) {
if (!mounted) {
return;
}
if (connectivityResult == ConnectivityResult.none) {
widget.scaffoldKey.currentState.showSnackBar(errorSnackBar);
} else {
if (!widget.connectedCompleter.isCompleted) {
widget.connectedCompleter.complete(null);
}
}
},
);
}
@override
void dispose() {
connectivitySubscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) => widget.child;
}
class VideoDemo extends StatefulWidget {
const VideoDemo({ Key key }) : super(key: key);
static const String routeName = '/video';
@override
_VideoDemoState createState() => _VideoDemoState();
}
final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
Future<bool> isIOSSimulator() async {
return Platform.isIOS && !(await deviceInfoPlugin.iosInfo).isPhysicalDevice;
}
class _VideoDemoState extends State<VideoDemo> with SingleTickerProviderStateMixin {
final VideoPlayerController butterflyController = VideoPlayerController.asset(
'videos/butterfly.mp4',
package: 'flutter_gallery_assets',
);
// TODO(sigurdm): This should not be stored here.
static const String beeUri = 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4';
final VideoPlayerController beeController = VideoPlayerController.network(beeUri);
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
final Completer<void> connectedCompleter = Completer<void>();
bool isSupported = true;
bool isDisposed = false;
@override
void initState() {
super.initState();
Future<void> initController(VideoPlayerController controller, String name) async {
print('> VideoDemo initController "$name" ${isDisposed ? "DISPOSED" : ""}');
controller.setLooping(true);
controller.setVolume(0.0);
controller.play();
await connectedCompleter.future;
await controller.initialize();
if (mounted) {
print('< VideoDemo initController "$name" done ${isDisposed ? "DISPOSED" : ""}');
setState(() { });
}
}
initController(butterflyController, 'butterfly');
initController(beeController, 'bee');
isIOSSimulator().then<void>((bool result) {
isSupported = !result;
});
}
@override
void dispose() {
print('> VideoDemo dispose');
isDisposed = true;
butterflyController.dispose();
beeController.dispose();
print('< VideoDemo dispose');
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: scaffoldKey,
appBar: AppBar(
title: const Text('Videos'),
),
body: isSupported
? ConnectivityOverlay(
child: Scrollbar(
child: ListView(
children: <Widget>[
VideoCard(
title: 'Butterfly',
subtitle: '… flutters by',
controller: butterflyController,
),
VideoCard(
title: 'Bee',
subtitle: '… gently buzzing',
controller: beeController,
),
],
),
),
connectedCompleter: connectedCompleter,
scaffoldKey: scaffoldKey,
)
: const Center(
child: Text(
'Video playback not supported on the iOS Simulator.',
),
),
);
}
}

@ -1,83 +0,0 @@
// 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/gestures.dart';
import 'package:flutter/foundation.dart' show defaultTargetPlatform;
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
class _LinkTextSpan extends TextSpan {
// Beware!
//
// This class is only safe because the TapGestureRecognizer is not
// given a deadline and therefore never allocates any resources.
//
// In any other situation -- setting a deadline, using any of the less trivial
// recognizers, etc -- you would have to manage the gesture recognizer's
// lifetime and call dispose() when the TextSpan was no longer being rendered.
//
// Since TextSpan itself is @immutable, this means that you would have to
// manage the recognizer from outside the TextSpan, e.g. in the State of a
// stateful widget that then hands the recognizer to the TextSpan.
_LinkTextSpan({ TextStyle style, String url, String text }) : super(
style: style,
text: text ?? url,
recognizer: TapGestureRecognizer()..onTap = () {
launch(url, forceSafariVC: false);
}
);
}
void showGalleryAboutDialog(BuildContext context) {
final ThemeData themeData = Theme.of(context);
final TextStyle aboutTextStyle = themeData.textTheme.body2;
final TextStyle linkStyle = themeData.textTheme.body2.copyWith(color: themeData.accentColor);
showAboutDialog(
context: context,
applicationVersion: 'January 2019',
applicationIcon: const FlutterLogo(),
applicationLegalese: '© 2019 The Chromium Authors',
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 24.0),
child: RichText(
text: TextSpan(
children: <TextSpan>[
TextSpan(
style: aboutTextStyle,
text: 'Flutter is an open-source project to help developers '
'build high-performance, high-fidelity, mobile apps for '
'${defaultTargetPlatform == TargetPlatform.iOS ? 'multiple platforms' : 'iOS and Android'} '
'from a single codebase. This design lab is a playground '
"and showcase of Flutter's many widgets, behaviors, "
'animations, layouts, and more. Learn more about Flutter at ',
),
_LinkTextSpan(
style: linkStyle,
url: 'https://flutter.dev',
),
TextSpan(
style: aboutTextStyle,
text: '.\n\nTo see the source code for this app, please visit the ',
),
_LinkTextSpan(
style: linkStyle,
url: 'https://goo.gl/iv1p4G',
text: 'flutter github repo',
),
TextSpan(
style: aboutTextStyle,
text: '.',
),
],
),
),
),
],
);
}

@ -1,168 +0,0 @@
// Copyright 2015 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/cupertino.dart';
import 'package:flutter/foundation.dart' show defaultTargetPlatform;
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart' show timeDilation;
import 'package:flutter_gallery/demo/shrine/model/app_state_model.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:url_launcher/url_launcher.dart';
import 'demos.dart';
import 'home.dart';
import 'options.dart';
import 'scales.dart';
import 'themes.dart';
import 'updater.dart';
class GalleryApp extends StatefulWidget {
const GalleryApp({
Key key,
this.updateUrlFetcher,
this.enablePerformanceOverlay = true,
this.enableRasterCacheImagesCheckerboard = true,
this.enableOffscreenLayersCheckerboard = true,
this.onSendFeedback,
this.testMode = false,
}) : super(key: key);
final UpdateUrlFetcher updateUrlFetcher;
final bool enablePerformanceOverlay;
final bool enableRasterCacheImagesCheckerboard;
final bool enableOffscreenLayersCheckerboard;
final VoidCallback onSendFeedback;
final bool testMode;
@override
_GalleryAppState createState() => _GalleryAppState();
}
class _GalleryAppState extends State<GalleryApp> {
GalleryOptions _options;
Timer _timeDilationTimer;
AppStateModel model;
Map<String, WidgetBuilder> _buildRoutes() {
// For a different example of how to set up an application routing table
// using named routes, consider the example in the Navigator class documentation:
// https://docs.flutter.io/flutter/widgets/Navigator-class.html
return Map<String, WidgetBuilder>.fromIterable(
kAllGalleryDemos,
key: (dynamic demo) => '${demo.routeName}',
value: (dynamic demo) => demo.buildRoute,
);
}
@override
void initState() {
super.initState();
_options = GalleryOptions(
themeMode: ThemeMode.system,
textScaleFactor: kAllGalleryTextScaleValues[0],
timeDilation: timeDilation,
platform: defaultTargetPlatform,
);
model = AppStateModel()..loadProducts();
}
@override
void dispose() {
_timeDilationTimer?.cancel();
_timeDilationTimer = null;
super.dispose();
}
void _handleOptionsChanged(GalleryOptions newOptions) {
setState(() {
if (_options.timeDilation != newOptions.timeDilation) {
_timeDilationTimer?.cancel();
_timeDilationTimer = null;
if (newOptions.timeDilation > 1.0) {
// We delay the time dilation change long enough that the user can see
// that UI has started reacting and then we slam on the brakes so that
// they see that the time is in fact now dilated.
_timeDilationTimer = Timer(const Duration(milliseconds: 150), () {
timeDilation = newOptions.timeDilation;
});
} else {
timeDilation = newOptions.timeDilation;
}
}
_options = newOptions;
});
}
Widget _applyTextScaleFactor(Widget child) {
return Builder(
builder: (BuildContext context) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaleFactor: _options.textScaleFactor.scale,
),
child: child,
);
},
);
}
@override
Widget build(BuildContext context) {
Widget home = GalleryHome(
testMode: widget.testMode,
optionsPage: GalleryOptionsPage(
options: _options,
onOptionsChanged: _handleOptionsChanged,
onSendFeedback: widget.onSendFeedback ?? () {
launch('https://github.com/flutter/flutter/issues/new/choose', forceSafariVC: false);
},
),
);
if (widget.updateUrlFetcher != null) {
home = Updater(
updateUrlFetcher: widget.updateUrlFetcher,
child: home,
);
}
return ScopedModel<AppStateModel>(
model: model,
child: MaterialApp(
theme: kLightGalleryTheme.copyWith(platform: _options.platform),
darkTheme: kDarkGalleryTheme.copyWith(platform: _options.platform),
themeMode: _options.themeMode,
title: 'Flutter Gallery',
color: Colors.grey,
showPerformanceOverlay: _options.showPerformanceOverlay,
checkerboardOffscreenLayers: _options.showOffscreenLayersCheckerboard,
checkerboardRasterCacheImages: _options.showRasterCacheImagesCheckerboard,
routes: _buildRoutes(),
builder: (BuildContext context, Widget child) {
return Directionality(
textDirection: _options.textDirection,
child: _applyTextScaleFactor(
// Specifically use a blank Cupertino theme here and do not transfer
// over the Material primary color etc except the brightness to
// showcase standard iOS looks.
Builder(builder: (BuildContext context) {
return CupertinoTheme(
data: CupertinoThemeData(
brightness: Theme.of(context).brightness,
),
child: child,
);
}),
),
);
},
home: home,
),
);
}
}

@ -1,355 +0,0 @@
// 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/rendering.dart';
import 'package:flutter/material.dart';
const double _kFrontHeadingHeight = 32.0; // front layer beveled rectangle
const double _kFrontClosedHeight = 92.0; // front layer height when closed
const double _kBackAppBarHeight = 56.0; // back layer (options) appbar height
// The size of the front layer heading's left and right beveled corners.
final Animatable<BorderRadius> _kFrontHeadingBevelRadius = BorderRadiusTween(
begin: const BorderRadius.only(
topLeft: Radius.circular(12.0),
topRight: Radius.circular(12.0),
),
end: const BorderRadius.only(
topLeft: Radius.circular(_kFrontHeadingHeight),
topRight: Radius.circular(_kFrontHeadingHeight),
),
);
class _TappableWhileStatusIs extends StatefulWidget {
const _TappableWhileStatusIs(
this.status, {
Key key,
this.controller,
this.child,
}) : super(key: key);
final AnimationController controller;
final AnimationStatus status;
final Widget child;
@override
_TappableWhileStatusIsState createState() => _TappableWhileStatusIsState();
}
class _TappableWhileStatusIsState extends State<_TappableWhileStatusIs> {
bool _active;
@override
void initState() {
super.initState();
widget.controller.addStatusListener(_handleStatusChange);
_active = widget.controller.status == widget.status;
}
@override
void dispose() {
widget.controller.removeStatusListener(_handleStatusChange);
super.dispose();
}
void _handleStatusChange(AnimationStatus status) {
final bool value = widget.controller.status == widget.status;
if (_active != value) {
setState(() {
_active = value;
});
}
}
@override
Widget build(BuildContext context) {
Widget child = AbsorbPointer(
absorbing: !_active,
child: widget.child,
);
if (!_active) {
child = FocusScope(
canRequestFocus: false,
debugLabel: '$_TappableWhileStatusIs',
child: child,
);
}
return child;
}
}
class _CrossFadeTransition extends AnimatedWidget {
const _CrossFadeTransition({
Key key,
this.alignment = Alignment.center,
Animation<double> progress,
this.child0,
this.child1,
}) : super(key: key, listenable: progress);
final AlignmentGeometry alignment;
final Widget child0;
final Widget child1;
@override
Widget build(BuildContext context) {
final Animation<double> progress = listenable;
final double opacity1 = CurvedAnimation(
parent: ReverseAnimation(progress),
curve: const Interval(0.5, 1.0),
).value;
final double opacity2 = CurvedAnimation(
parent: progress,
curve: const Interval(0.5, 1.0),
).value;
return Stack(
alignment: alignment,
children: <Widget>[
Opacity(
opacity: opacity1,
child: Semantics(
scopesRoute: true,
explicitChildNodes: true,
child: child1,
),
),
Opacity(
opacity: opacity2,
child: Semantics(
scopesRoute: true,
explicitChildNodes: true,
child: child0,
),
),
],
);
}
}
class _BackAppBar extends StatelessWidget {
const _BackAppBar({
Key key,
this.leading = const SizedBox(width: 56.0),
@required this.title,
this.trailing,
}) : assert(leading != null), assert(title != null), super(key: key);
final Widget leading;
final Widget title;
final Widget trailing;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return IconTheme.merge(
data: theme.primaryIconTheme,
child: DefaultTextStyle(
style: theme.primaryTextTheme.title,
child: SizedBox(
height: _kBackAppBarHeight,
child: Row(
children: <Widget>[
Container(
alignment: Alignment.center,
width: 56.0,
child: leading,
),
Expanded(
child: title,
),
if (trailing != null)
Container(
alignment: Alignment.center,
width: 56.0,
child: trailing,
),
],
),
),
),
);
}
}
class Backdrop extends StatefulWidget {
const Backdrop({
this.frontAction,
this.frontTitle,
this.frontHeading,
this.frontLayer,
this.backTitle,
this.backLayer,
});
final Widget frontAction;
final Widget frontTitle;
final Widget frontLayer;
final Widget frontHeading;
final Widget backTitle;
final Widget backLayer;
@override
_BackdropState createState() => _BackdropState();
}
class _BackdropState extends State<Backdrop> with SingleTickerProviderStateMixin {
final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop');
AnimationController _controller;
Animation<double> _frontOpacity;
static final Animatable<double> _frontOpacityTween = Tween<double>(begin: 0.2, end: 1.0)
.chain(CurveTween(curve: const Interval(0.0, 0.4, curve: Curves.easeInOut)));
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
value: 1.0,
vsync: this,
);
_frontOpacity = _controller.drive(_frontOpacityTween);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
double get _backdropHeight {
// Warning: this can be safely called from the event handlers but it may
// not be called at build time.
final RenderBox renderBox = _backdropKey.currentContext.findRenderObject();
return math.max(0.0, renderBox.size.height - _kBackAppBarHeight - _kFrontClosedHeight);
}
void _handleDragUpdate(DragUpdateDetails details) {
_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);
}
void _toggleFrontLayer() {
final AnimationStatus status = _controller.status;
final bool isOpen = status == AnimationStatus.completed || status == AnimationStatus.forward;
_controller.fling(velocity: isOpen ? -2.0 : 2.0);
}
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
final Animation<RelativeRect> frontRelativeRect = _controller.drive(RelativeRectTween(
begin: RelativeRect.fromLTRB(0.0, constraints.biggest.height - _kFrontClosedHeight, 0.0, 0.0),
end: const RelativeRect.fromLTRB(0.0, _kBackAppBarHeight, 0.0, 0.0),
));
return Stack(
key: _backdropKey,
children: <Widget>[
// Back layer
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
_BackAppBar(
leading: widget.frontAction,
title: _CrossFadeTransition(
progress: _controller,
alignment: AlignmentDirectional.centerStart,
child0: Semantics(namesRoute: true, child: widget.frontTitle),
child1: Semantics(namesRoute: true, child: widget.backTitle),
),
trailing: IconButton(
onPressed: _toggleFrontLayer,
tooltip: 'Toggle options page',
icon: AnimatedIcon(
icon: AnimatedIcons.close_menu,
progress: _controller,
),
),
),
Expanded(
child: _TappableWhileStatusIs(
AnimationStatus.dismissed,
controller: _controller,
child: Visibility(
child: widget.backLayer,
visible: _controller.status != AnimationStatus.completed,
maintainState: true,
),
),
),
],
),
// Front layer
PositionedTransition(
rect: frontRelativeRect,
child: AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget child) {
return PhysicalShape(
elevation: 12.0,
color: Theme.of(context).canvasColor,
clipper: ShapeBorderClipper(
shape: BeveledRectangleBorder(
borderRadius: _kFrontHeadingBevelRadius.transform(_controller.value),
),
),
clipBehavior: Clip.antiAlias,
child: child,
);
},
child: _TappableWhileStatusIs(
AnimationStatus.completed,
controller: _controller,
child: FadeTransition(
opacity: _frontOpacity,
child: widget.frontLayer,
),
),
),
),
// The front "heading" is a (typically transparent) widget that's stacked on
// top of, and at the top of, the front layer. It adds support for dragging
// the front layer up and down and for opening and closing the front layer
// with a tap. It may obscure part of the front layer's topmost child.
if (widget.frontHeading != null)
PositionedTransition(
rect: frontRelativeRect,
child: ExcludeSemantics(
child: Container(
alignment: Alignment.topLeft,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _toggleFrontLayer,
onVerticalDragUpdate: _handleDragUpdate,
onVerticalDragEnd: _handleDragEnd,
child: widget.frontHeading,
),
),
),
),
],
);
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: _buildStack);
}
}

@ -1,261 +0,0 @@
// Copyright 2016 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/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import 'demos.dart';
import 'example_code_parser.dart';
import 'syntax_highlighter.dart';
class ComponentDemoTabData {
ComponentDemoTabData({
this.demoWidget,
this.exampleCodeTag,
this.description,
this.tabName,
this.documentationUrl,
});
final Widget demoWidget;
final String exampleCodeTag;
final String description;
final String tabName;
final String documentationUrl;
@override
bool operator==(Object other) {
if (other.runtimeType != runtimeType)
return false;
final ComponentDemoTabData typedOther = other;
return typedOther.tabName == tabName
&& typedOther.description == description
&& typedOther.documentationUrl == documentationUrl;
}
@override
int get hashCode => hashValues(tabName, description, documentationUrl);
}
class TabbedComponentDemoScaffold extends StatelessWidget {
const TabbedComponentDemoScaffold({
this.title,
this.demos,
this.actions,
this.isScrollable = true,
this.showExampleCodeAction = true,
});
final List<ComponentDemoTabData> demos;
final String title;
final List<Widget> actions;
final bool isScrollable;
final bool showExampleCodeAction;
void _showExampleCode(BuildContext context) {
final String tag = demos[DefaultTabController.of(context).index].exampleCodeTag;
if (tag != null) {
Navigator.push(context, MaterialPageRoute<FullScreenCodeDialog>(
builder: (BuildContext context) => FullScreenCodeDialog(exampleCodeTag: tag)
));
}
}
Future<void> _showApiDocumentation(BuildContext context) async {
final String url = demos[DefaultTabController.of(context).index].documentationUrl;
if (url == null)
return;
if (await canLaunch(url)) {
await launch(url);
} else {
showDialog<void>(
context: context,
builder: (BuildContext context) {
return SimpleDialog(
title: const Text('Couldn\'t display URL:'),
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(url),
),
],
);
},
);
}
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: demos.length,
child: Scaffold(
appBar: AppBar(
title: Text(title),
actions: <Widget>[
...?actions,
Builder(
builder: (BuildContext context) {
return IconButton(
icon: const Icon(Icons.library_books, semanticLabel: 'Show documentation'),
onPressed: () => _showApiDocumentation(context),
);
},
),
if (showExampleCodeAction)
Builder(
builder: (BuildContext context) {
return IconButton(
icon: const Icon(Icons.code),
tooltip: 'Show example code',
onPressed: () => _showExampleCode(context),
);
},
),
],
bottom: TabBar(
isScrollable: isScrollable,
tabs: demos.map<Widget>((ComponentDemoTabData data) => Tab(text: data.tabName)).toList(),
),
),
body: TabBarView(
children: demos.map<Widget>((ComponentDemoTabData demo) {
return SafeArea(
top: false,
bottom: false,
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(demo.description,
style: Theme.of(context).textTheme.subhead,
),
),
Expanded(child: demo.demoWidget),
],
),
);
}).toList(),
),
),
);
}
}
class FullScreenCodeDialog extends StatefulWidget {
const FullScreenCodeDialog({ this.exampleCodeTag });
final String exampleCodeTag;
@override
FullScreenCodeDialogState createState() => FullScreenCodeDialogState();
}
class FullScreenCodeDialogState extends State<FullScreenCodeDialog> {
String _exampleCode;
@override
void didChangeDependencies() {
getExampleCode(widget.exampleCodeTag, DefaultAssetBundle.of(context)).then<void>((String code) {
if (mounted) {
setState(() {
_exampleCode = code ?? 'Example code not found';
});
}
});
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
final SyntaxHighlighterStyle style = Theme.of(context).brightness == Brightness.dark
? SyntaxHighlighterStyle.darkThemeStyle()
: SyntaxHighlighterStyle.lightThemeStyle();
Widget body;
if (_exampleCode == null) {
body = const Center(
child: CircularProgressIndicator(),
);
} else {
body = SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: RichText(
text: TextSpan(
style: const TextStyle(fontFamily: 'monospace', fontSize: 10.0),
children: <TextSpan>[
DartSyntaxHighlighter(style).format(_exampleCode),
],
),
),
),
);
}
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(
Icons.clear,
semanticLabel: 'Close',
),
onPressed: () { Navigator.pop(context); },
),
title: const Text('Example code'),
),
body: body,
);
}
}
class MaterialDemoDocumentationButton extends StatelessWidget {
MaterialDemoDocumentationButton(String routeName, { Key key })
: documentationUrl = kDemoDocumentationUrl[routeName],
assert(
kDemoDocumentationUrl[routeName] != null,
'A documentation URL was not specified for demo route $routeName in kAllGalleryDemos',
),
super(key: key);
final String documentationUrl;
@override
Widget build(BuildContext context) {
return IconButton(
icon: const Icon(Icons.library_books),
tooltip: 'API documentation',
onPressed: () => launch(documentationUrl, forceWebView: true),
);
}
}
class CupertinoDemoDocumentationButton extends StatelessWidget {
CupertinoDemoDocumentationButton(String routeName, { Key key })
: documentationUrl = kDemoDocumentationUrl[routeName],
assert(
kDemoDocumentationUrl[routeName] != null,
'A documentation URL was not specified for demo route $routeName in kAllGalleryDemos',
),
super(key: key);
final String documentationUrl;
@override
Widget build(BuildContext context) {
return CupertinoButton(
padding: EdgeInsets.zero,
child: Semantics(
label: 'API documentation',
child: const Icon(CupertinoIcons.book),
),
onPressed: () => launch(documentationUrl, forceWebView: true),
);
}
}

@ -1,587 +0,0 @@
// 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/material.dart';
import '../demo/all.dart';
import 'icons.dart';
class GalleryDemoCategory {
const GalleryDemoCategory._({
@required this.name,
@required this.icon,
});
final String name;
final IconData icon;
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (runtimeType != other.runtimeType)
return false;
final GalleryDemoCategory typedOther = other;
return typedOther.name == name && typedOther.icon == icon;
}
@override
int get hashCode => hashValues(name, icon);
@override
String toString() {
return '$runtimeType($name)';
}
}
const GalleryDemoCategory _kDemos = GalleryDemoCategory._(
name: 'Studies',
icon: GalleryIcons.animation,
);
const GalleryDemoCategory _kStyle = GalleryDemoCategory._(
name: 'Style',
icon: GalleryIcons.custom_typography,
);
const GalleryDemoCategory _kMaterialComponents = GalleryDemoCategory._(
name: 'Material',
icon: GalleryIcons.category_mdc,
);
const GalleryDemoCategory _kCupertinoComponents = GalleryDemoCategory._(
name: 'Cupertino',
icon: GalleryIcons.phone_iphone,
);
const GalleryDemoCategory _kMedia = GalleryDemoCategory._(
name: 'Media',
icon: GalleryIcons.drive_video,
);
class GalleryDemo {
const GalleryDemo({
@required this.title,
@required this.icon,
this.subtitle,
@required this.category,
@required this.routeName,
this.documentationUrl,
@required this.buildRoute,
}) : assert(title != null),
assert(category != null),
assert(routeName != null),
assert(buildRoute != null);
final String title;
final IconData icon;
final String subtitle;
final GalleryDemoCategory category;
final String routeName;
final WidgetBuilder buildRoute;
final String documentationUrl;
@override
String toString() {
return '$runtimeType($title $routeName)';
}
}
List<GalleryDemo> _buildGalleryDemos() {
final List<GalleryDemo> galleryDemos = <GalleryDemo>[
// Demos
GalleryDemo(
title: 'Shrine',
subtitle: 'Basic shopping app',
icon: GalleryIcons.shrine,
category: _kDemos,
routeName: ShrineDemo.routeName,
buildRoute: (BuildContext context) => const ShrineDemo(),
),
GalleryDemo(
title: 'Fortnightly',
subtitle: 'Newspaper typography app',
icon: GalleryIcons.custom_typography,
category: _kDemos,
routeName: FortnightlyDemo.routeName,
buildRoute: (BuildContext context) => FortnightlyDemo(),
),
GalleryDemo(
title: 'Contact profile',
subtitle: 'Address book entry with a flexible appbar',
icon: GalleryIcons.account_box,
category: _kDemos,
routeName: ContactsDemo.routeName,
buildRoute: (BuildContext context) => ContactsDemo(),
),
GalleryDemo(
title: 'Animation',
subtitle: 'Section organizer',
icon: GalleryIcons.animation,
category: _kDemos,
routeName: AnimationDemo.routeName,
buildRoute: (BuildContext context) => const AnimationDemo(),
),
GalleryDemo(
title: '2D Transformations',
subtitle: 'Pan, Zoom, Rotate',
icon: GalleryIcons.grid_on,
category: _kDemos,
routeName: TransformationsDemo.routeName,
buildRoute: (BuildContext context) => const TransformationsDemo(),
),
GalleryDemo(
title: 'Pesto',
subtitle: 'Simple recipe browser',
icon: Icons.adjust,
category: _kDemos,
routeName: PestoDemo.routeName,
buildRoute: (BuildContext context) => const PestoDemo(),
),
// Style
GalleryDemo(
title: 'Colors',
subtitle: 'All of the predefined colors',
icon: GalleryIcons.colors,
category: _kStyle,
routeName: ColorsDemo.routeName,
buildRoute: (BuildContext context) => ColorsDemo(),
),
GalleryDemo(
title: 'Typography',
subtitle: 'All of the predefined text styles',
icon: GalleryIcons.custom_typography,
category: _kStyle,
routeName: TypographyDemo.routeName,
buildRoute: (BuildContext context) => TypographyDemo(),
),
// Material Components
GalleryDemo(
title: 'Backdrop',
subtitle: 'Select a front layer from back layer',
icon: GalleryIcons.backdrop,
category: _kMaterialComponents,
routeName: BackdropDemo.routeName,
buildRoute: (BuildContext context) => BackdropDemo(),
),
GalleryDemo(
title: 'Banner',
subtitle: 'Displaying a banner within a list',
icon: GalleryIcons.lists_leave_behind,
category: _kMaterialComponents,
routeName: BannerDemo.routeName,
documentationUrl: 'https://api.flutter.dev/flutter/material/MaterialBanner-class.html',
buildRoute: (BuildContext context) => const BannerDemo(),
),
GalleryDemo(
title: 'Bottom app bar',
subtitle: 'Optional floating action button notch',
icon: GalleryIcons.bottom_app_bar,
category: _kMaterialComponents,
routeName: BottomAppBarDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/BottomAppBar-class.html',
buildRoute: (BuildContext context) => BottomAppBarDemo(),
),
GalleryDemo(
title: 'Bottom navigation',
subtitle: 'Bottom navigation with cross-fading views',
icon: GalleryIcons.bottom_navigation,
category: _kMaterialComponents,
routeName: BottomNavigationDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/BottomNavigationBar-class.html',
buildRoute: (BuildContext context) => BottomNavigationDemo(),
),
GalleryDemo(
title: 'Bottom sheet: Modal',
subtitle: 'A dismissible bottom sheet',
icon: GalleryIcons.bottom_sheets,
category: _kMaterialComponents,
routeName: ModalBottomSheetDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/showModalBottomSheet.html',
buildRoute: (BuildContext context) => ModalBottomSheetDemo(),
),
GalleryDemo(
title: 'Bottom sheet: Persistent',
subtitle: 'A bottom sheet that sticks around',
icon: GalleryIcons.bottom_sheet_persistent,
category: _kMaterialComponents,
routeName: PersistentBottomSheetDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/ScaffoldState/showBottomSheet.html',
buildRoute: (BuildContext context) => PersistentBottomSheetDemo(),
),
GalleryDemo(
title: 'Buttons',
subtitle: 'Flat, raised, dropdown, and more',
icon: GalleryIcons.generic_buttons,
category: _kMaterialComponents,
routeName: ButtonsDemo.routeName,
buildRoute: (BuildContext context) => ButtonsDemo(),
),
GalleryDemo(
title: 'Buttons: Floating Action Button',
subtitle: 'FAB with transitions',
icon: GalleryIcons.buttons,
category: _kMaterialComponents,
routeName: TabsFabDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/FloatingActionButton-class.html',
buildRoute: (BuildContext context) => TabsFabDemo(),
),
GalleryDemo(
title: 'Cards',
subtitle: 'Baseline cards with rounded corners',
icon: GalleryIcons.cards,
category: _kMaterialComponents,
routeName: CardsDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/Card-class.html',
buildRoute: (BuildContext context) => CardsDemo(),
),
GalleryDemo(
title: 'Chips',
subtitle: 'Labeled with delete buttons and avatars',
icon: GalleryIcons.chips,
category: _kMaterialComponents,
routeName: ChipDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/Chip-class.html',
buildRoute: (BuildContext context) => ChipDemo(),
),
GalleryDemo(
title: 'Data tables',
subtitle: 'Rows and columns',
icon: GalleryIcons.data_table,
category: _kMaterialComponents,
routeName: DataTableDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/PaginatedDataTable-class.html',
buildRoute: (BuildContext context) => DataTableDemo(),
),
GalleryDemo(
title: 'Dialogs',
subtitle: 'Simple, alert, and fullscreen',
icon: GalleryIcons.dialogs,
category: _kMaterialComponents,
routeName: DialogDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/showDialog.html',
buildRoute: (BuildContext context) => DialogDemo(),
),
GalleryDemo(
title: 'Elevations',
subtitle: 'Shadow values on cards',
// TODO(larche): Change to custom icon for elevations when one exists.
icon: GalleryIcons.cupertino_progress,
category: _kMaterialComponents,
routeName: ElevationDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/Material/elevation.html',
buildRoute: (BuildContext context) => ElevationDemo(),
),
GalleryDemo(
title: 'Expand/collapse list control',
subtitle: 'A list with one sub-list level',
icon: GalleryIcons.expand_all,
category: _kMaterialComponents,
routeName: ExpansionTileListDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/ExpansionTile-class.html',
buildRoute: (BuildContext context) => ExpansionTileListDemo(),
),
GalleryDemo(
title: 'Expansion panels',
subtitle: 'List of expanding panels',
icon: GalleryIcons.expand_all,
category: _kMaterialComponents,
routeName: ExpansionPanelsDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/ExpansionPanel-class.html',
buildRoute: (BuildContext context) => ExpansionPanelsDemo(),
),
GalleryDemo(
title: 'Grid',
subtitle: 'Row and column layout',
icon: GalleryIcons.grid_on,
category: _kMaterialComponents,
routeName: GridListDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/widgets/GridView-class.html',
buildRoute: (BuildContext context) => const GridListDemo(),
),
GalleryDemo(
title: 'Icons',
subtitle: 'Enabled and disabled icons with opacity',
icon: GalleryIcons.sentiment_very_satisfied,
category: _kMaterialComponents,
routeName: IconsDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/IconButton-class.html',
buildRoute: (BuildContext context) => IconsDemo(),
),
GalleryDemo(
title: 'Lists',
subtitle: 'Scrolling list layouts',
icon: GalleryIcons.list_alt,
category: _kMaterialComponents,
routeName: ListDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/ListTile-class.html',
buildRoute: (BuildContext context) => const ListDemo(),
),
GalleryDemo(
title: 'Lists: leave-behind list items',
subtitle: 'List items with hidden actions',
icon: GalleryIcons.lists_leave_behind,
category: _kMaterialComponents,
routeName: LeaveBehindDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/widgets/Dismissible-class.html',
buildRoute: (BuildContext context) => const LeaveBehindDemo(),
),
GalleryDemo(
title: 'Lists: reorderable',
subtitle: 'Reorderable lists',
icon: GalleryIcons.list_alt,
category: _kMaterialComponents,
routeName: ReorderableListDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/ReorderableListView-class.html',
buildRoute: (BuildContext context) => const ReorderableListDemo(),
),
GalleryDemo(
title: 'Menus',
subtitle: 'Menu buttons and simple menus',
icon: GalleryIcons.more_vert,
category: _kMaterialComponents,
routeName: MenuDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/PopupMenuButton-class.html',
buildRoute: (BuildContext context) => const MenuDemo(),
),
GalleryDemo(
title: 'Navigation drawer',
subtitle: 'Navigation drawer with standard header',
icon: GalleryIcons.menu,
category: _kMaterialComponents,
routeName: DrawerDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/Drawer-class.html',
buildRoute: (BuildContext context) => DrawerDemo(),
),
GalleryDemo(
title: 'Pagination',
subtitle: 'PageView with indicator',
icon: GalleryIcons.page_control,
category: _kMaterialComponents,
routeName: PageSelectorDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/TabBarView-class.html',
buildRoute: (BuildContext context) => PageSelectorDemo(),
),
GalleryDemo(
title: 'Pickers',
subtitle: 'Date and time selection widgets',
icon: GalleryIcons.event,
category: _kMaterialComponents,
routeName: DateAndTimePickerDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/showDatePicker.html',
buildRoute: (BuildContext context) => DateAndTimePickerDemo(),
),
GalleryDemo(
title: 'Progress indicators',
subtitle: 'Linear, circular, indeterminate',
icon: GalleryIcons.progress_activity,
category: _kMaterialComponents,
routeName: ProgressIndicatorDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/LinearProgressIndicator-class.html',
buildRoute: (BuildContext context) => ProgressIndicatorDemo(),
),
GalleryDemo(
title: 'Pull to refresh',
subtitle: 'Refresh indicators',
icon: GalleryIcons.refresh,
category: _kMaterialComponents,
routeName: OverscrollDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/RefreshIndicator-class.html',
buildRoute: (BuildContext context) => const OverscrollDemo(),
),
GalleryDemo(
title: 'Search',
subtitle: 'Expandable search',
icon: Icons.search,
category: _kMaterialComponents,
routeName: SearchDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/showSearch.html',
buildRoute: (BuildContext context) => SearchDemo(),
),
GalleryDemo(
title: 'Selection controls',
subtitle: 'Checkboxes, radio buttons, and switches',
icon: GalleryIcons.check_box,
category: _kMaterialComponents,
routeName: SelectionControlsDemo.routeName,
buildRoute: (BuildContext context) => SelectionControlsDemo(),
),
GalleryDemo(
title: 'Sliders',
subtitle: 'Widgets for selecting a value by swiping',
icon: GalleryIcons.sliders,
category: _kMaterialComponents,
routeName: SliderDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/Slider-class.html',
buildRoute: (BuildContext context) => SliderDemo(),
),
GalleryDemo(
title: 'Snackbar',
subtitle: 'Temporary messaging',
icon: GalleryIcons.snackbar,
category: _kMaterialComponents,
routeName: SnackBarDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/ScaffoldState/showSnackBar.html',
buildRoute: (BuildContext context) => const SnackBarDemo(),
),
GalleryDemo(
title: 'Tabs',
subtitle: 'Tabs with independently scrollable views',
icon: GalleryIcons.tabs,
category: _kMaterialComponents,
routeName: TabsDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/TabBarView-class.html',
buildRoute: (BuildContext context) => TabsDemo(),
),
GalleryDemo(
title: 'Tabs: Scrolling',
subtitle: 'Tab bar that scrolls',
category: _kMaterialComponents,
icon: GalleryIcons.tabs,
routeName: ScrollableTabsDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/TabBar-class.html',
buildRoute: (BuildContext context) => ScrollableTabsDemo(),
),
GalleryDemo(
title: 'Text fields',
subtitle: 'Single line of editable text and numbers',
icon: GalleryIcons.text_fields_alt,
category: _kMaterialComponents,
routeName: TextFormFieldDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/TextFormField-class.html',
buildRoute: (BuildContext context) => const TextFormFieldDemo(),
),
GalleryDemo(
title: 'Tooltips',
subtitle: 'Short message displayed on long-press',
icon: GalleryIcons.tooltip,
category: _kMaterialComponents,
routeName: TooltipDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/material/Tooltip-class.html',
buildRoute: (BuildContext context) => TooltipDemo(),
),
// Cupertino Components
GalleryDemo(
title: 'Activity Indicator',
icon: GalleryIcons.cupertino_progress,
category: _kCupertinoComponents,
routeName: CupertinoProgressIndicatorDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/cupertino/CupertinoActivityIndicator-class.html',
buildRoute: (BuildContext context) => CupertinoProgressIndicatorDemo(),
),
GalleryDemo(
title: 'Alerts',
icon: GalleryIcons.dialogs,
category: _kCupertinoComponents,
routeName: CupertinoAlertDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/cupertino/showCupertinoDialog.html',
buildRoute: (BuildContext context) => CupertinoAlertDemo(),
),
GalleryDemo(
title: 'Buttons',
icon: GalleryIcons.generic_buttons,
category: _kCupertinoComponents,
routeName: CupertinoButtonsDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/cupertino/CupertinoButton-class.html',
buildRoute: (BuildContext context) => CupertinoButtonsDemo(),
),
GalleryDemo(
title: 'Navigation',
icon: GalleryIcons.bottom_navigation,
category: _kCupertinoComponents,
routeName: CupertinoNavigationDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/cupertino/CupertinoTabScaffold-class.html',
buildRoute: (BuildContext context) => CupertinoNavigationDemo(),
),
GalleryDemo(
title: 'Pickers',
icon: GalleryIcons.event,
category: _kCupertinoComponents,
routeName: CupertinoPickerDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/cupertino/CupertinoPicker-class.html',
buildRoute: (BuildContext context) => CupertinoPickerDemo(),
),
GalleryDemo(
title: 'Pull to refresh',
icon: GalleryIcons.cupertino_pull_to_refresh,
category: _kCupertinoComponents,
routeName: CupertinoRefreshControlDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/cupertino/CupertinoSliverRefreshControl-class.html',
buildRoute: (BuildContext context) => CupertinoRefreshControlDemo(),
),
GalleryDemo(
title: 'Segmented Control',
icon: GalleryIcons.tabs,
category: _kCupertinoComponents,
routeName: CupertinoSegmentedControlDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/cupertino/CupertinoSegmentedControl-class.html',
buildRoute: (BuildContext context) => CupertinoSegmentedControlDemo(),
),
GalleryDemo(
title: 'Sliders',
icon: GalleryIcons.sliders,
category: _kCupertinoComponents,
routeName: CupertinoSliderDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/cupertino/CupertinoSlider-class.html',
buildRoute: (BuildContext context) => CupertinoSliderDemo(),
),
GalleryDemo(
title: 'Switches',
icon: GalleryIcons.cupertino_switch,
category: _kCupertinoComponents,
routeName: CupertinoSwitchDemo.routeName,
documentationUrl: 'https://docs.flutter.io/flutter/cupertino/CupertinoSwitch-class.html',
buildRoute: (BuildContext context) => CupertinoSwitchDemo(),
),
GalleryDemo(
title: 'Text Fields',
icon: GalleryIcons.text_fields_alt,
category: _kCupertinoComponents,
routeName: CupertinoTextFieldDemo.routeName,
buildRoute: (BuildContext context) => CupertinoTextFieldDemo(),
),
// Media
GalleryDemo(
title: 'Animated images',
subtitle: 'GIF and WebP animations',
icon: GalleryIcons.animation,
category: _kMedia,
routeName: ImagesDemo.routeName,
buildRoute: (BuildContext context) => ImagesDemo(),
),
GalleryDemo(
title: 'Video',
subtitle: 'Video playback',
icon: GalleryIcons.drive_video,
category: _kMedia,
routeName: VideoDemo.routeName,
buildRoute: (BuildContext context) => const VideoDemo(),
),
];
return galleryDemos;
}
final List<GalleryDemo> kAllGalleryDemos = _buildGalleryDemos();
final Set<GalleryDemoCategory> kAllGalleryDemoCategories =
kAllGalleryDemos.map<GalleryDemoCategory>((GalleryDemo demo) => demo.category).toSet();
final Map<GalleryDemoCategory, List<GalleryDemo>> kGalleryCategoryToDemos =
Map<GalleryDemoCategory, List<GalleryDemo>>.fromIterable(
kAllGalleryDemoCategories,
value: (dynamic category) {
return kAllGalleryDemos.where((GalleryDemo demo) => demo.category == category).toList();
},
);
final Map<String, String> kDemoDocumentationUrl =
Map<String, String>.fromIterable(
kAllGalleryDemos.where((GalleryDemo demo) => demo.documentationUrl != null),
key: (dynamic demo) => demo.routeName,
value: (dynamic demo) => demo.documentationUrl,
);

@ -1,286 +0,0 @@
// Copyright 2016 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.
// Note: This code is not runnable, it contains code snippets displayed in the
// gallery.
import 'package:flutter/material.dart';
class ButtonsDemo {
void setState(VoidCallback callback) { }
BuildContext context;
void buttons() {
// START buttons_raised
// Create a raised button.
RaisedButton(
child: const Text('BUTTON TITLE'),
onPressed: () {
// Perform some action
},
);
// Create a disabled button.
// Buttons are disabled when onPressed isn't
// specified or is null.
const RaisedButton(
child: Text('BUTTON TITLE'),
onPressed: null,
);
// Create a button with an icon and a
// title.
RaisedButton.icon(
icon: const Icon(Icons.add, size: 18.0),
label: const Text('BUTTON TITLE'),
onPressed: () {
// Perform some action
},
);
// END
// START buttons_outline
// Create an outline button.
OutlineButton(
child: const Text('BUTTON TITLE'),
onPressed: () {
// Perform some action
},
);
// Create a disabled button.
// Buttons are disabled when onPressed isn't
// specified or is null.
const OutlineButton(
child: Text('BUTTON TITLE'),
onPressed: null,
);
// Create a button with an icon and a
// title.
OutlineButton.icon(
icon: const Icon(Icons.add, size: 18.0),
label: const Text('BUTTON TITLE'),
onPressed: () {
// Perform some action
},
);
// END
// START buttons_flat
// Create a flat button.
FlatButton(
child: const Text('BUTTON TITLE'),
onPressed: () {
// Perform some action
},
);
// Create a disabled button.
// Buttons are disabled when onPressed isn't
// specified or is null.
const FlatButton(
child: Text('BUTTON TITLE'),
onPressed: null,
);
// END
// START buttons_dropdown
// Member variable holding value.
String dropdownValue;
// Dropdown button with string values.
DropdownButton<String>(
value: dropdownValue,
onChanged: (String newValue) {
// null indicates the user didn't select a
// new value.
setState(() {
if (newValue != null)
dropdownValue = newValue;
});
},
items: <String>['One', 'Two', 'Free', 'Four']
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value));
})
.toList(),
);
// END
// START buttons_icon
// Member variable holding toggle value.
bool value;
// Toggleable icon button.
IconButton(
icon: const Icon(Icons.thumb_up),
onPressed: () {
setState(() => value = !value);
},
color: value ? Theme.of(context).primaryColor : null,
);
// END
// START buttons_action
// Floating action button in Scaffold.
Scaffold(
appBar: AppBar(
title: const Text('Demo'),
),
floatingActionButton: const FloatingActionButton(
child: Icon(Icons.add),
onPressed: null,
),
);
// END
}
}
class SelectionControls {
void setState(VoidCallback callback) { }
void selectionControls() {
// START selectioncontrols_checkbox
// Member variable holding the checkbox's value.
bool checkboxValue = false;
// Create a checkbox.
Checkbox(
value: checkboxValue,
onChanged: (bool value) {
setState(() {
checkboxValue = value;
});
},
);
// Create a tristate checkbox.
Checkbox(
tristate: true,
value: checkboxValue,
onChanged: (bool value) {
setState(() {
checkboxValue = value;
});
},
);
// Create a disabled checkbox.
// Checkboxes are disabled when onChanged isn't
// specified or null.
const Checkbox(value: false, onChanged: null);
// END
// START selectioncontrols_radio
// Member variable holding value.
int radioValue = 0;
// Method setting value.
void handleRadioValueChanged(int value) {
setState(() {
radioValue = value;
});
}
// Creates a set of radio buttons.
Row(
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,
),
],
);
// Creates a disabled radio button.
const Radio<int>(
value: 0,
groupValue: 0,
onChanged: null,
);
// END
// START selectioncontrols_switch
// Member variable holding value.
bool switchValue = false;
// Create a switch.
Switch(
value: switchValue,
onChanged: (bool value) {
setState(() {
switchValue = value;
}
);
});
// Create a disabled switch.
// Switches are disabled when onChanged isn't
// specified or null.
const Switch(value: false, onChanged: null);
// END
}
}
class GridLists {
void gridlists() {
// START gridlists
// Creates a scrollable grid list with images
// loaded from the web.
GridView.count(
crossAxisCount: 3,
childAspectRatio: 1.0,
padding: const EdgeInsets.all(4.0),
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
children: <String>[
'https://example.com/image-0.jpg',
'https://example.com/image-1.jpg',
'https://example.com/image-2.jpg',
'...',
'https://example.com/image-n.jpg',
].map<Widget>((String url) {
return GridTile(
footer: GridTileBar(
title: Text(url),
),
child: Image.network(url, fit: BoxFit.cover),
);
}).toList(),
);
// END
}
}
class AnimatedImage {
void animatedImage() {
// START animated_image
Image.network('https://example.com/animated-image.gif');
// END
}
}

@ -1,55 +0,0 @@
// Copyright 2016 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/services.dart';
const String _kStartTag = '// START ';
const String _kEndTag = '// END';
Map<String, String> _exampleCode;
Future<String> getExampleCode(String tag, AssetBundle bundle) async {
if (_exampleCode == null)
await _parseExampleCode(bundle);
return _exampleCode[tag];
}
Future<void> _parseExampleCode(AssetBundle bundle) async {
final String code = await bundle.loadString('lib/gallery/example_code.dart') ??
'// lib/gallery/example_code.dart not found\n';
_exampleCode = <String, String>{};
final List<String> lines = code.split('\n');
List<String> codeBlock;
String codeTag;
for (String line in lines) {
if (codeBlock == null) {
// Outside a block.
if (line.startsWith(_kStartTag)) {
// Starting a new code block.
codeBlock = <String>[];
codeTag = line.substring(_kStartTag.length).trim();
} else {
// Just skipping the line.
}
} else {
// Inside a block.
if (line.startsWith(_kEndTag)) {
// Add the block.
_exampleCode[codeTag] = codeBlock.join('\n');
codeBlock = null;
codeTag = null;
} else {
// Add to the current block
// trimRight() to remove any \r on Windows
// without removing any useful indentation
codeBlock.add(line.trimRight());
}
}
}
}

@ -1,410 +0,0 @@
// 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 'dart:developer';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'backdrop.dart';
import 'demos.dart';
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
const Color _kFlutterBlue = Color(0xFF003D75);
const double _kDemoItemHeight = 64.0;
const Duration _kFrontLayerSwitchDuration = Duration(milliseconds: 300);
class _FlutterLogo extends StatelessWidget {
const _FlutterLogo({ Key key }) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Container(
width: 34.0,
height: 34.0,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage(
'logos/flutter_white/logo.png',
package: _kGalleryAssetsPackage,
),
),
),
),
);
}
}
class _CategoryItem extends StatelessWidget {
const _CategoryItem({
Key key,
this.category,
this.onTap,
}) : super (key: key);
final GalleryDemoCategory category;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final bool isDark = theme.brightness == Brightness.dark;
// This repaint boundary prevents the entire _CategoriesPage from being
// repainted when the button's ink splash animates.
return RepaintBoundary(
child: RawMaterialButton(
padding: EdgeInsets.zero,
hoverColor: theme.primaryColor.withOpacity(0.05),
splashColor: theme.primaryColor.withOpacity(0.12),
highlightColor: Colors.transparent,
onPressed: onTap,
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(6.0),
child: Icon(
category.icon,
size: 60.0,
color: isDark ? Colors.white : _kFlutterBlue,
),
),
const SizedBox(height: 10.0),
Container(
height: 48.0,
alignment: Alignment.center,
child: Text(
category.name,
textAlign: TextAlign.center,
style: theme.textTheme.subhead.copyWith(
fontFamily: 'GoogleSans',
color: isDark ? Colors.white : _kFlutterBlue,
),
),
),
],
),
),
);
}
}
class _CategoriesPage extends StatelessWidget {
const _CategoriesPage({
Key key,
this.categories,
this.onCategoryTap,
}) : super(key: key);
final Iterable<GalleryDemoCategory> categories;
final ValueChanged<GalleryDemoCategory> onCategoryTap;
@override
Widget build(BuildContext context) {
const double aspectRatio = 160.0 / 180.0;
final List<GalleryDemoCategory> categoriesList = categories.toList();
final int columnCount = (MediaQuery.of(context).orientation == Orientation.portrait) ? 2 : 3;
return Semantics(
scopesRoute: true,
namesRoute: true,
label: 'categories',
explicitChildNodes: true,
child: SingleChildScrollView(
key: const PageStorageKey<String>('categories'),
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final double columnWidth = constraints.biggest.width / columnCount.toDouble();
final double rowHeight = math.min(225.0, columnWidth * aspectRatio);
final int rowCount = (categories.length + columnCount - 1) ~/ columnCount;
// This repaint boundary prevents the inner contents of the front layer
// from repainting when the backdrop toggle triggers a repaint on the
// LayoutBuilder.
return RepaintBoundary(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: List<Widget>.generate(rowCount, (int rowIndex) {
final int columnCountForRow = rowIndex == rowCount - 1
? categories.length - columnCount * math.max(0, rowCount - 1)
: columnCount;
return Row(
children: List<Widget>.generate(columnCountForRow, (int columnIndex) {
final int index = rowIndex * columnCount + columnIndex;
final GalleryDemoCategory category = categoriesList[index];
return SizedBox(
width: columnWidth,
height: rowHeight,
child: _CategoryItem(
category: category,
onTap: () {
onCategoryTap(category);
},
),
);
}),
);
}),
),
);
},
),
),
);
}
}
class _DemoItem extends StatelessWidget {
const _DemoItem({ Key key, this.demo }) : super(key: key);
final GalleryDemo demo;
void _launchDemo(BuildContext context) {
if (demo.routeName != null) {
Timeline.instantSync('Start Transition', arguments: <String, String>{
'from': '/',
'to': demo.routeName,
});
Navigator.pushNamed(context, demo.routeName);
}
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final bool isDark = theme.brightness == Brightness.dark;
final double textScaleFactor = MediaQuery.textScaleFactorOf(context);
return RawMaterialButton(
padding: EdgeInsets.zero,
splashColor: theme.primaryColor.withOpacity(0.12),
highlightColor: Colors.transparent,
onPressed: () {
_launchDemo(context);
},
child: Container(
constraints: BoxConstraints(minHeight: _kDemoItemHeight * textScaleFactor),
child: Row(
children: <Widget>[
Container(
width: 56.0,
height: 56.0,
alignment: Alignment.center,
child: Icon(
demo.icon,
size: 24.0,
color: isDark ? Colors.white : _kFlutterBlue,
),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
demo.title,
style: theme.textTheme.subhead.copyWith(
color: isDark ? Colors.white : const Color(0xFF202124),
),
),
if (demo.subtitle != null)
Text(
demo.subtitle,
style: theme.textTheme.body1.copyWith(
color: isDark ? Colors.white : const Color(0xFF60646B)
),
),
],
),
),
const SizedBox(width: 44.0),
],
),
),
);
}
}
class _DemosPage extends StatelessWidget {
const _DemosPage(this.category);
final GalleryDemoCategory category;
@override
Widget build(BuildContext context) {
// When overriding ListView.padding, it is necessary to manually handle
// safe areas.
final double windowBottomPadding = MediaQuery.of(context).padding.bottom;
return KeyedSubtree(
key: const ValueKey<String>('GalleryDemoList'), // So the tests can find this ListView
child: Semantics(
scopesRoute: true,
namesRoute: true,
label: category.name,
explicitChildNodes: true,
child: ListView(
dragStartBehavior: DragStartBehavior.down,
key: PageStorageKey<String>(category.name),
padding: EdgeInsets.only(top: 8.0, bottom: windowBottomPadding),
children: kGalleryCategoryToDemos[category].map<Widget>((GalleryDemo demo) {
return _DemoItem(demo: demo);
}).toList(),
),
),
);
}
}
class GalleryHome extends StatefulWidget {
const GalleryHome({
Key key,
this.testMode = false,
this.optionsPage,
}) : super(key: key);
final Widget optionsPage;
final bool testMode;
// In checked mode our MaterialApp will show the default "debug" banner.
// Otherwise show the "preview" banner.
static bool showPreviewBanner = true;
@override
_GalleryHomeState createState() => _GalleryHomeState();
}
class _GalleryHomeState extends State<GalleryHome> with SingleTickerProviderStateMixin {
static final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
AnimationController _controller;
GalleryDemoCategory _category;
static Widget _topHomeLayout(Widget currentChild, List<Widget> previousChildren) {
return Stack(
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
],
alignment: Alignment.topCenter,
);
}
static const AnimatedSwitcherLayoutBuilder _centerHomeLayout = AnimatedSwitcher.defaultLayoutBuilder;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 600),
debugLabel: 'preview banner',
vsync: this,
)..forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final bool isDark = theme.brightness == Brightness.dark;
final MediaQueryData media = MediaQuery.of(context);
final bool centerHome = media.orientation == Orientation.portrait && media.size.height < 800.0;
const Curve switchOutCurve = Interval(0.4, 1.0, curve: Curves.fastOutSlowIn);
const Curve switchInCurve = Interval(0.4, 1.0, curve: Curves.fastOutSlowIn);
Widget home = Scaffold(
key: _scaffoldKey,
backgroundColor: isDark ? _kFlutterBlue : theme.primaryColor,
body: SafeArea(
bottom: false,
child: WillPopScope(
onWillPop: () {
// Pop the category page if Android back button is pressed.
if (_category != null) {
setState(() => _category = null);
return Future<bool>.value(false);
}
return Future<bool>.value(true);
},
child: Backdrop(
backTitle: const Text('Options'),
backLayer: widget.optionsPage,
frontAction: AnimatedSwitcher(
duration: _kFrontLayerSwitchDuration,
switchOutCurve: switchOutCurve,
switchInCurve: switchInCurve,
child: _category == null
? const _FlutterLogo()
: IconButton(
icon: const BackButtonIcon(),
tooltip: 'Back',
onPressed: () => setState(() => _category = null),
),
),
frontTitle: AnimatedSwitcher(
duration: _kFrontLayerSwitchDuration,
child: _category == null
? const Text('Flutter gallery')
: Text(_category.name),
),
frontHeading: widget.testMode ? null : Container(height: 24.0),
frontLayer: AnimatedSwitcher(
duration: _kFrontLayerSwitchDuration,
switchOutCurve: switchOutCurve,
switchInCurve: switchInCurve,
layoutBuilder: centerHome ? _centerHomeLayout : _topHomeLayout,
child: _category != null
? _DemosPage(_category)
: _CategoriesPage(
categories: kAllGalleryDemoCategories,
onCategoryTap: (GalleryDemoCategory category) {
setState(() => _category = category);
},
),
),
),
),
),
);
assert(() {
GalleryHome.showPreviewBanner = false;
return true;
}());
if (GalleryHome.showPreviewBanner) {
home = Stack(
fit: StackFit.expand,
children: <Widget>[
home,
FadeTransition(
opacity: CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
child: const Banner(
message: 'PREVIEW',
location: BannerLocation.topEnd,
),
),
],
);
}
home = AnnotatedRegion<SystemUiOverlayStyle>(
child: home,
value: SystemUiOverlayStyle.light,
);
return home;
}
}

@ -1,50 +0,0 @@
// 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/material.dart';
class GalleryIcons {
GalleryIcons._();
static const IconData tooltip = IconData(0xe900, fontFamily: 'GalleryIcons');
static const IconData text_fields_alt = IconData(0xe901, fontFamily: 'GalleryIcons');
static const IconData tabs = IconData(0xe902, fontFamily: 'GalleryIcons');
static const IconData switches = IconData(0xe903, fontFamily: 'GalleryIcons');
static const IconData sliders = IconData(0xe904, fontFamily: 'GalleryIcons');
static const IconData shrine = IconData(0xe905, fontFamily: 'GalleryIcons');
static const IconData sentiment_very_satisfied = IconData(0xe906, fontFamily: 'GalleryIcons');
static const IconData refresh = IconData(0xe907, fontFamily: 'GalleryIcons');
static const IconData progress_activity = IconData(0xe908, fontFamily: 'GalleryIcons');
static const IconData phone_iphone = IconData(0xe909, fontFamily: 'GalleryIcons');
static const IconData page_control = IconData(0xe90a, fontFamily: 'GalleryIcons');
static const IconData more_vert = IconData(0xe90b, fontFamily: 'GalleryIcons');
static const IconData menu = IconData(0xe90c, fontFamily: 'GalleryIcons');
static const IconData list_alt = IconData(0xe90d, fontFamily: 'GalleryIcons');
static const IconData grid_on = IconData(0xe90e, fontFamily: 'GalleryIcons');
static const IconData expand_all = IconData(0xe90f, fontFamily: 'GalleryIcons');
static const IconData event = IconData(0xe910, fontFamily: 'GalleryIcons');
static const IconData drive_video = IconData(0xe911, fontFamily: 'GalleryIcons');
static const IconData dialogs = IconData(0xe912, fontFamily: 'GalleryIcons');
static const IconData data_table = IconData(0xe913, fontFamily: 'GalleryIcons');
static const IconData custom_typography = IconData(0xe914, fontFamily: 'GalleryIcons');
static const IconData colors = IconData(0xe915, fontFamily: 'GalleryIcons');
static const IconData chips = IconData(0xe916, fontFamily: 'GalleryIcons');
static const IconData check_box = IconData(0xe917, fontFamily: 'GalleryIcons');
static const IconData cards = IconData(0xe918, fontFamily: 'GalleryIcons');
static const IconData buttons = IconData(0xe919, fontFamily: 'GalleryIcons');
static const IconData bottom_sheets = IconData(0xe91a, fontFamily: 'GalleryIcons');
static const IconData bottom_navigation = IconData(0xe91b, fontFamily: 'GalleryIcons');
static const IconData animation = IconData(0xe91c, fontFamily: 'GalleryIcons');
static const IconData account_box = IconData(0xe91d, fontFamily: 'GalleryIcons');
static const IconData snackbar = IconData(0xe91e, fontFamily: 'GalleryIcons');
static const IconData category_mdc = IconData(0xe91f, fontFamily: 'GalleryIcons');
static const IconData cupertino_progress = IconData(0xe920, fontFamily: 'GalleryIcons');
static const IconData cupertino_pull_to_refresh = IconData(0xe921, fontFamily: 'GalleryIcons');
static const IconData cupertino_switch = IconData(0xe922, fontFamily: 'GalleryIcons');
static const IconData generic_buttons = IconData(0xe923, fontFamily: 'GalleryIcons');
static const IconData backdrop = IconData(0xe924, fontFamily: 'GalleryIcons');
static const IconData bottom_app_bar = IconData(0xe925, fontFamily: 'GalleryIcons');
static const IconData bottom_sheet_persistent = IconData(0xe926, fontFamily: 'GalleryIcons');
static const IconData lists_leave_behind = IconData(0xe927, fontFamily: 'GalleryIcons');
}

@ -1,487 +0,0 @@
// 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/material.dart';
import 'about.dart';
import 'scales.dart';
class GalleryOptions {
GalleryOptions({
this.themeMode,
this.textScaleFactor,
this.textDirection = TextDirection.ltr,
this.timeDilation = 1.0,
this.platform,
this.showOffscreenLayersCheckerboard = false,
this.showRasterCacheImagesCheckerboard = false,
this.showPerformanceOverlay = false,
});
final ThemeMode themeMode;
final GalleryTextScaleValue textScaleFactor;
final TextDirection textDirection;
final double timeDilation;
final TargetPlatform platform;
final bool showPerformanceOverlay;
final bool showRasterCacheImagesCheckerboard;
final bool showOffscreenLayersCheckerboard;
GalleryOptions copyWith({
ThemeMode themeMode,
GalleryTextScaleValue textScaleFactor,
TextDirection textDirection,
double timeDilation,
TargetPlatform platform,
bool showPerformanceOverlay,
bool showRasterCacheImagesCheckerboard,
bool showOffscreenLayersCheckerboard,
}) {
return GalleryOptions(
themeMode: themeMode ?? this.themeMode,
textScaleFactor: textScaleFactor ?? this.textScaleFactor,
textDirection: textDirection ?? this.textDirection,
timeDilation: timeDilation ?? this.timeDilation,
platform: platform ?? this.platform,
showPerformanceOverlay: showPerformanceOverlay ?? this.showPerformanceOverlay,
showOffscreenLayersCheckerboard: showOffscreenLayersCheckerboard ?? this.showOffscreenLayersCheckerboard,
showRasterCacheImagesCheckerboard: showRasterCacheImagesCheckerboard ?? this.showRasterCacheImagesCheckerboard,
);
}
@override
bool operator ==(dynamic other) {
if (runtimeType != other.runtimeType)
return false;
final GalleryOptions typedOther = other;
return themeMode == typedOther.themeMode
&& textScaleFactor == typedOther.textScaleFactor
&& textDirection == typedOther.textDirection
&& platform == typedOther.platform
&& showPerformanceOverlay == typedOther.showPerformanceOverlay
&& showRasterCacheImagesCheckerboard == typedOther.showRasterCacheImagesCheckerboard
&& showOffscreenLayersCheckerboard == typedOther.showRasterCacheImagesCheckerboard;
}
@override
int get hashCode => hashValues(
themeMode,
textScaleFactor,
textDirection,
timeDilation,
platform,
showPerformanceOverlay,
showRasterCacheImagesCheckerboard,
showOffscreenLayersCheckerboard,
);
@override
String toString() {
return '$runtimeType($themeMode)';
}
}
const double _kItemHeight = 48.0;
const EdgeInsetsDirectional _kItemPadding = EdgeInsetsDirectional.only(start: 56.0);
class _OptionsItem extends StatelessWidget {
const _OptionsItem({ Key key, this.child }) : super(key: key);
final Widget child;
@override
Widget build(BuildContext context) {
final double textScaleFactor = MediaQuery.textScaleFactorOf(context);
return MergeSemantics(
child: Container(
constraints: BoxConstraints(minHeight: _kItemHeight * textScaleFactor),
padding: _kItemPadding,
alignment: AlignmentDirectional.centerStart,
child: DefaultTextStyle(
style: DefaultTextStyle.of(context).style,
maxLines: 2,
overflow: TextOverflow.fade,
child: IconTheme(
data: Theme.of(context).primaryIconTheme,
child: child,
),
),
),
);
}
}
class _BooleanItem extends StatelessWidget {
const _BooleanItem(this.title, this.value, this.onChanged, { this.switchKey });
final String title;
final bool value;
final ValueChanged<bool> onChanged;
// [switchKey] is used for accessing the switch from driver tests.
final Key switchKey;
@override
Widget build(BuildContext context) {
final bool isDark = Theme.of(context).brightness == Brightness.dark;
return _OptionsItem(
child: Row(
children: <Widget>[
Expanded(child: Text(title)),
Switch(
key: switchKey,
value: value,
onChanged: onChanged,
activeColor: const Color(0xFF39CEFD),
activeTrackColor: isDark ? Colors.white30 : Colors.black26,
),
],
),
);
}
}
class _ActionItem extends StatelessWidget {
const _ActionItem(this.text, this.onTap);
final String text;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return _OptionsItem(
child: _FlatButton(
onPressed: onTap,
child: Text(text),
),
);
}
}
class _FlatButton extends StatelessWidget {
const _FlatButton({ Key key, this.onPressed, this.child }) : super(key: key);
final VoidCallback onPressed;
final Widget child;
@override
Widget build(BuildContext context) {
return FlatButton(
padding: EdgeInsets.zero,
onPressed: onPressed,
child: DefaultTextStyle(
style: Theme.of(context).primaryTextTheme.subhead,
child: child,
),
);
}
}
class _Heading extends StatelessWidget {
const _Heading(this.text);
final String text;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return _OptionsItem(
child: DefaultTextStyle(
style: theme.textTheme.body1.copyWith(
fontFamily: 'GoogleSans',
color: theme.accentColor,
),
child: Semantics(
child: Text(text),
header: true,
),
),
);
}
}
class _ThemeModeItem extends StatelessWidget {
const _ThemeModeItem(this.options, this.onOptionsChanged);
final GalleryOptions options;
final ValueChanged<GalleryOptions> onOptionsChanged;
static final Map<ThemeMode, String> modeLabels = <ThemeMode, String>{
ThemeMode.system: 'System Default',
ThemeMode.light: 'Light',
ThemeMode.dark: 'Dark',
};
@override
Widget build(BuildContext context) {
return _OptionsItem(
child: Row(
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Text('Theme'),
Text(
'${modeLabels[options.themeMode]}',
style: Theme.of(context).primaryTextTheme.body1,
),
],
),
),
PopupMenuButton<ThemeMode>(
padding: const EdgeInsetsDirectional.only(end: 16.0),
icon: const Icon(Icons.arrow_drop_down),
initialValue: options.themeMode,
itemBuilder: (BuildContext context) {
return ThemeMode.values.map<PopupMenuItem<ThemeMode>>((ThemeMode mode) {
return PopupMenuItem<ThemeMode>(
value: mode,
child: Text(modeLabels[mode]),
);
}).toList();
},
onSelected: (ThemeMode mode) {
onOptionsChanged(
options.copyWith(themeMode: mode),
);
},
),
],
),
);
}
}
class _TextScaleFactorItem extends StatelessWidget {
const _TextScaleFactorItem(this.options, this.onOptionsChanged);
final GalleryOptions options;
final ValueChanged<GalleryOptions> onOptionsChanged;
@override
Widget build(BuildContext context) {
return _OptionsItem(
child: Row(
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Text('Text size'),
Text(
'${options.textScaleFactor.label}',
style: Theme.of(context).primaryTextTheme.body1,
),
],
),
),
PopupMenuButton<GalleryTextScaleValue>(
padding: const EdgeInsetsDirectional.only(end: 16.0),
icon: const Icon(Icons.arrow_drop_down),
itemBuilder: (BuildContext context) {
return kAllGalleryTextScaleValues.map<PopupMenuItem<GalleryTextScaleValue>>((GalleryTextScaleValue scaleValue) {
return PopupMenuItem<GalleryTextScaleValue>(
value: scaleValue,
child: Text(scaleValue.label),
);
}).toList();
},
onSelected: (GalleryTextScaleValue scaleValue) {
onOptionsChanged(
options.copyWith(textScaleFactor: scaleValue),
);
},
),
],
),
);
}
}
class _TextDirectionItem extends StatelessWidget {
const _TextDirectionItem(this.options, this.onOptionsChanged);
final GalleryOptions options;
final ValueChanged<GalleryOptions> onOptionsChanged;
@override
Widget build(BuildContext context) {
return _BooleanItem(
'Force RTL',
options.textDirection == TextDirection.rtl,
(bool value) {
onOptionsChanged(
options.copyWith(
textDirection: value ? TextDirection.rtl : TextDirection.ltr,
),
);
},
switchKey: const Key('text_direction'),
);
}
}
class _TimeDilationItem extends StatelessWidget {
const _TimeDilationItem(this.options, this.onOptionsChanged);
final GalleryOptions options;
final ValueChanged<GalleryOptions> onOptionsChanged;
@override
Widget build(BuildContext context) {
return _BooleanItem(
'Slow motion',
options.timeDilation != 1.0,
(bool value) {
onOptionsChanged(
options.copyWith(
timeDilation: value ? 20.0 : 1.0,
),
);
},
switchKey: const Key('slow_motion'),
);
}
}
class _PlatformItem extends StatelessWidget {
const _PlatformItem(this.options, this.onOptionsChanged);
final GalleryOptions options;
final ValueChanged<GalleryOptions> onOptionsChanged;
String _platformLabel(TargetPlatform platform) {
switch(platform) {
case TargetPlatform.android:
return 'Mountain View';
case TargetPlatform.fuchsia:
return 'Fuchsia';
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return 'Cupertino';
}
assert(false);
return null;
}
@override
Widget build(BuildContext context) {
return _OptionsItem(
child: Row(
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Text('Platform mechanics'),
Text(
'${_platformLabel(options.platform)}',
style: Theme.of(context).primaryTextTheme.body1,
),
],
),
),
PopupMenuButton<TargetPlatform>(
padding: const EdgeInsetsDirectional.only(end: 16.0),
icon: const Icon(Icons.arrow_drop_down),
itemBuilder: (BuildContext context) {
return TargetPlatform.values.map((TargetPlatform platform) {
return PopupMenuItem<TargetPlatform>(
value: platform,
child: Text(_platformLabel(platform)),
);
}).toList();
},
onSelected: (TargetPlatform platform) {
onOptionsChanged(
options.copyWith(platform: platform),
);
},
),
],
),
);
}
}
class GalleryOptionsPage extends StatelessWidget {
const GalleryOptionsPage({
Key key,
this.options,
this.onOptionsChanged,
this.onSendFeedback,
}) : super(key: key);
final GalleryOptions options;
final ValueChanged<GalleryOptions> onOptionsChanged;
final VoidCallback onSendFeedback;
List<Widget> _enabledDiagnosticItems() {
// Boolean showFoo options with a value of null: don't display
// the showFoo option at all.
if (options.showOffscreenLayersCheckerboard == null &&
options.showRasterCacheImagesCheckerboard == null &&
options.showPerformanceOverlay == null)
return const <Widget>[];
return <Widget>[
const Divider(),
const _Heading('Diagnostics'),
if (options.showOffscreenLayersCheckerboard != null)
_BooleanItem(
'Highlight offscreen layers',
options.showOffscreenLayersCheckerboard,
(bool value) {
onOptionsChanged(options.copyWith(showOffscreenLayersCheckerboard: value));
},
),
if (options.showRasterCacheImagesCheckerboard != null)
_BooleanItem(
'Highlight raster cache images',
options.showRasterCacheImagesCheckerboard,
(bool value) {
onOptionsChanged(options.copyWith(showRasterCacheImagesCheckerboard: value));
},
),
if (options.showPerformanceOverlay != null)
_BooleanItem(
'Show performance overlay',
options.showPerformanceOverlay,
(bool value) {
onOptionsChanged(options.copyWith(showPerformanceOverlay: value));
},
),
];
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return DefaultTextStyle(
style: theme.primaryTextTheme.subhead,
child: ListView(
padding: const EdgeInsets.only(bottom: 124.0),
children: <Widget>[
const _Heading('Display'),
_ThemeModeItem(options, onOptionsChanged),
_TextScaleFactorItem(options, onOptionsChanged),
_TextDirectionItem(options, onOptionsChanged),
_TimeDilationItem(options, onOptionsChanged),
const Divider(),
const _Heading('Platform mechanics'),
_PlatformItem(options, onOptionsChanged),
..._enabledDiagnosticItems(),
const Divider(),
const _Heading('Flutter gallery'),
_ActionItem('About Flutter Gallery', () {
showGalleryAboutDialog(context);
}),
_ActionItem('Send feedback', onSendFeedback),
],
),
);
}
}

@ -1,37 +0,0 @@
// 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/material.dart';
class GalleryTextScaleValue {
const GalleryTextScaleValue(this.scale, this.label);
final double scale;
final String label;
@override
bool operator ==(dynamic other) {
if (runtimeType != other.runtimeType)
return false;
final GalleryTextScaleValue typedOther = other;
return scale == typedOther.scale && label == typedOther.label;
}
@override
int get hashCode => hashValues(scale, label);
@override
String toString() {
return '$runtimeType($label)';
}
}
const List<GalleryTextScaleValue> kAllGalleryTextScaleValues = <GalleryTextScaleValue>[
GalleryTextScaleValue(null, 'System Default'),
GalleryTextScaleValue(0.8, 'Small'),
GalleryTextScaleValue(1.0, 'Normal'),
GalleryTextScaleValue(1.3, 'Large'),
GalleryTextScaleValue(2.0, 'Huge'),
];

@ -1,356 +0,0 @@
// Copyright 2016 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/material.dart';
import 'package:string_scanner/string_scanner.dart';
class SyntaxHighlighterStyle {
SyntaxHighlighterStyle({
this.baseStyle,
this.numberStyle,
this.commentStyle,
this.keywordStyle,
this.stringStyle,
this.punctuationStyle,
this.classStyle,
this.constantStyle,
});
static SyntaxHighlighterStyle lightThemeStyle() {
return SyntaxHighlighterStyle(
baseStyle: const TextStyle(color: Color(0xFF000000)),
numberStyle: const TextStyle(color: Color(0xFF1565C0)),
commentStyle: const TextStyle(color: Color(0xFF9E9E9E)),
keywordStyle: const TextStyle(color: Color(0xFF9C27B0)),
stringStyle: const TextStyle(color: Color(0xFF43A047)),
punctuationStyle: const TextStyle(color: Color(0xFF000000)),
classStyle: const TextStyle(color: Color(0xFF512DA8)),
constantStyle: const TextStyle(color: Color(0xFF795548)),
);
}
static SyntaxHighlighterStyle darkThemeStyle() {
return SyntaxHighlighterStyle(
baseStyle: const TextStyle(color: Color(0xFFFFFFFF)),
numberStyle: const TextStyle(color: Color(0xFF1565C0)),
commentStyle: const TextStyle(color: Color(0xFF9E9E9E)),
keywordStyle: const TextStyle(color: Color(0xFF80CBC4)),
stringStyle: const TextStyle(color: Color(0xFF009688)),
punctuationStyle: const TextStyle(color: Color(0xFFFFFFFF)),
classStyle: const TextStyle(color: Color(0xFF009688)),
constantStyle: const TextStyle(color: Color(0xFF795548)),
);
}
final TextStyle baseStyle;
final TextStyle numberStyle;
final TextStyle commentStyle;
final TextStyle keywordStyle;
final TextStyle stringStyle;
final TextStyle punctuationStyle;
final TextStyle classStyle;
final TextStyle constantStyle;
}
abstract class SyntaxHighlighter {
TextSpan format(String src);
}
class DartSyntaxHighlighter extends SyntaxHighlighter {
DartSyntaxHighlighter([this._style]) {
_spans = <_HighlightSpan>[];
_style ??= SyntaxHighlighterStyle.darkThemeStyle();
}
SyntaxHighlighterStyle _style;
static const List<String> _keywords = <String>[
'abstract', 'as', 'assert', 'async', 'await', 'break', 'case', 'catch',
'class', 'const', 'continue', 'default', 'deferred', 'do', 'dynamic', 'else',
'enum', 'export', 'external', 'extends', 'factory', 'false', 'final',
'finally', 'for', 'get', 'if', 'implements', 'import', 'in', 'is', 'library',
'new', 'null', 'operator', 'part', 'rethrow', 'return', 'set', 'static',
'super', 'switch', 'sync', 'this', 'throw', 'true', 'try', 'typedef', 'var',
'void', 'while', 'with', 'yield',
];
static const List<String> _builtInTypes = <String>[
'int', 'double', 'num', 'bool',
];
String _src;
StringScanner _scanner;
List<_HighlightSpan> _spans;
@override
TextSpan format(String src) {
_src = src;
_scanner = StringScanner(_src);
if (_generateSpans()) {
// Successfully parsed the code
final List<TextSpan> formattedText = <TextSpan>[];
int currentPosition = 0;
for (_HighlightSpan span in _spans) {
if (currentPosition != span.start)
formattedText.add(TextSpan(text: _src.substring(currentPosition, span.start)));
formattedText.add(TextSpan(style: span.textStyle(_style), text: span.textForSpan(_src)));
currentPosition = span.end;
}
if (currentPosition != _src.length)
formattedText.add(TextSpan(text: _src.substring(currentPosition, _src.length)));
return TextSpan(style: _style.baseStyle, children: formattedText);
} else {
// Parsing failed, return with only basic formatting
return TextSpan(style: _style.baseStyle, text: src);
}
}
bool _generateSpans() {
int lastLoopPosition = _scanner.position;
while (!_scanner.isDone) {
// Skip White space
_scanner.scan(RegExp(r'\s+'));
// Block comments
if (_scanner.scan(RegExp(r'/\*(.|\n)*\*/'))) {
_spans.add(_HighlightSpan(
_HighlightType.comment,
_scanner.lastMatch.start,
_scanner.lastMatch.end,
));
continue;
}
// Line comments
if (_scanner.scan('//')) {
final int startComment = _scanner.lastMatch.start;
bool eof = false;
int endComment;
if (_scanner.scan(RegExp(r'.*\n'))) {
endComment = _scanner.lastMatch.end - 1;
} else {
eof = true;
endComment = _src.length;
}
_spans.add(_HighlightSpan(
_HighlightType.comment,
startComment,
endComment,
));
if (eof)
break;
continue;
}
// Raw r"String"
if (_scanner.scan(RegExp(r'r".*"'))) {
_spans.add(_HighlightSpan(
_HighlightType.string,
_scanner.lastMatch.start,
_scanner.lastMatch.end,
));
continue;
}
// Raw r'String'
if (_scanner.scan(RegExp(r"r'.*'"))) {
_spans.add(_HighlightSpan(
_HighlightType.string,
_scanner.lastMatch.start,
_scanner.lastMatch.end,
));
continue;
}
// Multiline """String"""
if (_scanner.scan(RegExp(r'"""(?:[^"\\]|\\(.|\n))*"""'))) {
_spans.add(_HighlightSpan(
_HighlightType.string,
_scanner.lastMatch.start,
_scanner.lastMatch.end,
));
continue;
}
// Multiline '''String'''
if (_scanner.scan(RegExp(r"'''(?:[^'\\]|\\(.|\n))*'''"))) {
_spans.add(_HighlightSpan(
_HighlightType.string,
_scanner.lastMatch.start,
_scanner.lastMatch.end,
));
continue;
}
// "String"
if (_scanner.scan(RegExp(r'"(?:[^"\\]|\\.)*"'))) {
_spans.add(_HighlightSpan(
_HighlightType.string,
_scanner.lastMatch.start,
_scanner.lastMatch.end,
));
continue;
}
// 'String'
if (_scanner.scan(RegExp(r"'(?:[^'\\]|\\.)*'"))) {
_spans.add(_HighlightSpan(
_HighlightType.string,
_scanner.lastMatch.start,
_scanner.lastMatch.end,
));
continue;
}
// Double
if (_scanner.scan(RegExp(r'\d+\.\d+'))) {
_spans.add(_HighlightSpan(
_HighlightType.number,
_scanner.lastMatch.start,
_scanner.lastMatch.end,
));
continue;
}
// Integer
if (_scanner.scan(RegExp(r'\d+'))) {
_spans.add(_HighlightSpan(
_HighlightType.number,
_scanner.lastMatch.start,
_scanner.lastMatch.end)
);
continue;
}
// Punctuation
if (_scanner.scan(RegExp(r'[\[\]{}().!=<>&\|\?\+\-\*/%\^~;:,]'))) {
_spans.add(_HighlightSpan(
_HighlightType.punctuation,
_scanner.lastMatch.start,
_scanner.lastMatch.end,
));
continue;
}
// Meta data
if (_scanner.scan(RegExp(r'@\w+'))) {
_spans.add(_HighlightSpan(
_HighlightType.keyword,
_scanner.lastMatch.start,
_scanner.lastMatch.end,
));
continue;
}
// Words
if (_scanner.scan(RegExp(r'\w+'))) {
_HighlightType type;
String word = _scanner.lastMatch[0];
if (word.startsWith('_'))
word = word.substring(1);
if (_keywords.contains(word))
type = _HighlightType.keyword;
else if (_builtInTypes.contains(word))
type = _HighlightType.keyword;
else if (_firstLetterIsUpperCase(word))
type = _HighlightType.klass;
else if (word.length >= 2 && word.startsWith('k') && _firstLetterIsUpperCase(word.substring(1)))
type = _HighlightType.constant;
if (type != null) {
_spans.add(_HighlightSpan(
type,
_scanner.lastMatch.start,
_scanner.lastMatch.end,
));
}
}
// Check if this loop did anything
if (lastLoopPosition == _scanner.position) {
// Failed to parse this file, abort gracefully
return false;
}
lastLoopPosition = _scanner.position;
}
_simplify();
return true;
}
void _simplify() {
for (int i = _spans.length - 2; i >= 0; i -= 1) {
if (_spans[i].type == _spans[i + 1].type && _spans[i].end == _spans[i + 1].start) {
_spans[i] = _HighlightSpan(
_spans[i].type,
_spans[i].start,
_spans[i + 1].end,
);
_spans.removeAt(i + 1);
}
}
}
bool _firstLetterIsUpperCase(String str) {
if (str.isNotEmpty) {
final String first = str.substring(0, 1);
return first == first.toUpperCase();
}
return false;
}
}
enum _HighlightType {
number,
comment,
keyword,
string,
punctuation,
klass,
constant
}
class _HighlightSpan {
_HighlightSpan(this.type, this.start, this.end);
final _HighlightType type;
final int start;
final int end;
String textForSpan(String src) {
return src.substring(start, end);
}
TextStyle textStyle(SyntaxHighlighterStyle style) {
if (type == _HighlightType.number)
return style.numberStyle;
else if (type == _HighlightType.comment)
return style.commentStyle;
else if (type == _HighlightType.keyword)
return style.keywordStyle;
else if (type == _HighlightType.string)
return style.stringStyle;
else if (type == _HighlightType.punctuation)
return style.punctuationStyle;
else if (type == _HighlightType.klass)
return style.classStyle;
else if (type == _HighlightType.constant)
return style.constantStyle;
else
return style.baseStyle;
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save