1
0
mirror of https://github.com/flutter/samples.git synced 2026-06-24 23:28:33 +00:00

Update web/ samples to work with Flutter SDK (#134)

* add package:http dependency in dad_jokes

* add package:http dependency in filipino_cuisine

* don't build package:http demos until flutter/flutter#34858 is resolved

* update gallery

* update github_dataviz

* update particle_background

* don't build github_dataviz (uses package:http)

* update slide_puzzle

* update spinning_square

* update timeflow

* update vision_challenge

* update charts

* update dad_jokes

* update filipino cuisine

* ignore build output

* update timeflow and vision_challenge

* update slide_puzzle

* don't commit build/ directory

* move preview.png images to assets

* fix path url join

* update readme

* update web/readme.md
This commit is contained in:
John Ryan
2019-09-10 09:49:58 -07:00
committed by GitHub
parent 16fa475ff8
commit 317d459a58
746 changed files with 14607 additions and 61610 deletions

73
web/gallery/.gitignore vendored Normal file
View File

@@ -0,0 +1,73 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
.dart_tool/
.flutter-plugins
.packages
.pub-cache/
.pub/
/build/
# Android related
**/android/**/gradle-wrapper.jar
**/android/.gradle
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
# iOS/XCode related
**/ios/**/*.mode1v3
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside
**/ios/**/*.pbxuser
**/ios/**/*.perspectivev3
**/ios/**/*sync/
**/ios/**/.sconsign.dblite
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
**/ios/**/profile
**/ios/**/xcuserdata
**/ios/.generated/
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/Flutter/flutter_export_environment.sh
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages

10
web/gallery/.metadata Normal file
View File

@@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 393106fbf58124aef777fc12e97a20e237842f99
channel: master
project_type: app

View File

@@ -1,3 +1,5 @@
A gallery of Flutter widgets and UX studies.
Original source on [GitHub](https://github.com/flutter/flutter/tree/master/examples/flutter_gallery).
Original source on [GitHub][gallery].
[gallery]: https://github.com/flutter/flutter/tree/master/examples/flutter_gallery

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1,7 +0,0 @@
# See https://github.com/dart-lang/build/tree/master/build_web_compilers#configuration
targets:
$default:
builders:
build_web_compilers|entrypoint:
# Avoid building the test directory.
generate_for: ['web/**.dart']

View File

@@ -1,15 +1,17 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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 'calculator_demo.dart';
export 'colors_demo.dart';
export 'contacts_demo.dart';
//export 'cupertino/cupertino.dart';
//export 'images_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';
export 'video_demo.dart';

View File

@@ -1,11 +1,14 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter_web/rendering.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'sections.dart';
import 'widgets.dart';
@@ -31,17 +34,18 @@ 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;
}) : 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;
if (_maxHeight == value)
return;
_maxHeight = value;
markNeedsLayout();
}
@@ -52,15 +56,15 @@ class _RenderStatusBarPaddingSliver extends RenderSliver {
double _scrollFactor;
set scrollFactor(double value) {
assert(scrollFactor != null && scrollFactor >= 1.0);
if (_scrollFactor == value) return;
if (_scrollFactor == value)
return;
_scrollFactor = value;
markNeedsLayout();
}
@override
void performLayout() {
final double height = (maxHeight - constraints.scrollOffset / scrollFactor)
.clamp(0.0, maxHeight);
final double height = (maxHeight - constraints.scrollOffset / scrollFactor).clamp(0.0, maxHeight);
geometry = SliverGeometry(
paintExtent: math.min(height, constraints.remainingPaintExtent),
scrollExtent: maxHeight,
@@ -74,9 +78,9 @@ class _StatusBarPaddingSliver extends SingleChildRenderObjectWidget {
Key key,
@required this.maxHeight,
this.scrollFactor = 5.0,
}) : assert(maxHeight != null && maxHeight >= 0.0),
assert(scrollFactor != null && scrollFactor >= 1.0),
super(key: key);
}) : assert(maxHeight != null && maxHeight >= 0.0),
assert(scrollFactor != null && scrollFactor >= 1.0),
super(key: key);
final double maxHeight;
final double scrollFactor;
@@ -90,8 +94,7 @@ class _StatusBarPaddingSliver extends SingleChildRenderObjectWidget {
}
@override
void updateRenderObject(
BuildContext context, _RenderStatusBarPaddingSliver renderObject) {
void updateRenderObject(BuildContext context, _RenderStatusBarPaddingSliver renderObject) {
renderObject
..maxHeight = maxHeight
..scrollFactor = scrollFactor;
@@ -122,16 +125,15 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
double get maxExtent => math.max(maxHeight, minHeight);
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
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;
return maxHeight != oldDelegate.maxHeight
|| minHeight != oldDelegate.minHeight
|| child != oldDelegate.child;
}
@override
@@ -195,28 +197,24 @@ class _AllSectionsLayout extends MultiChildLayoutDelegate {
// 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;
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;
(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 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));
@@ -224,38 +222,25 @@ class _AllSectionsLayout extends MultiChildLayoutDelegate {
}
// 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 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);
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 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);
final Offset rowIndicatorOrigin = Offset(centeredRowIndicatorX, rowIndicatorY);
final Offset indicatorOrigin = _interpolatePoint(columnIndicatorOrigin, rowIndicatorOrigin);
positionChild('indicator$index', indicatorOrigin + offset);
columnCardY += columnCardHeight;
@@ -267,9 +252,9 @@ class _AllSectionsLayout extends MultiChildLayoutDelegate {
@override
bool shouldRelayout(_AllSectionsLayout oldDelegate) {
return tColumnToRow != oldDelegate.tColumnToRow ||
cardCount != oldDelegate.cardCount ||
selectedIndex != oldDelegate.selectedIndex;
return tColumnToRow != oldDelegate.tColumnToRow
|| cardCount != oldDelegate.cardCount
|| selectedIndex != oldDelegate.selectedIndex;
}
}
@@ -283,14 +268,13 @@ class _AllSectionsView extends AnimatedWidget {
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);
}) : 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;
@@ -310,14 +294,17 @@ class _AllSectionsView extends AnimatedWidget {
// 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);
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);
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;
@@ -356,8 +343,7 @@ class _AllSectionsView extends AnimatedWidget {
return CustomMultiChildLayout(
delegate: _AllSectionsLayout(
translation:
Alignment((selectedIndex.value - sectionIndex) * 2.0 - 1.0, -1.0),
translation: Alignment((selectedIndex.value - sectionIndex) * 2.0 - 1.0, -1.0),
tColumnToRow: tColumnToRow,
tCollapsed: tCollapsed,
cardCount: sections.length,
@@ -380,34 +366,29 @@ class _SnappingScrollPhysics extends ClampingScrollPhysics {
const _SnappingScrollPhysics({
ScrollPhysics parent,
@required this.midScrollOffset,
}) : assert(midScrollOffset != null),
super(parent: parent);
}) : assert(midScrollOffset != null),
super(parent: parent);
final double midScrollOffset;
@override
_SnappingScrollPhysics applyTo(ScrollPhysics ancestor) {
return _SnappingScrollPhysics(
parent: buildParent(ancestor), midScrollOffset: midScrollOffset);
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);
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);
return ScrollSpringSimulation(spring, offset, 0.0, velocity, tolerance: tolerance);
}
@override
Simulation createBallisticSimulation(
ScrollMetrics position, double dragVelocity) {
final Simulation simulation =
super.createBallisticSimulation(position, dragVelocity);
Simulation createBallisticSimulation(ScrollMetrics position, double dragVelocity) {
final Simulation simulation = super.createBallisticSimulation(position, dragVelocity);
final double offset = position.pixels;
if (simulation != null) {
@@ -416,7 +397,8 @@ class _SnappingScrollPhysics extends ClampingScrollPhysics {
// 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 (simulationEnd >= midScrollOffset)
return simulation;
if (dragVelocity > 0.0)
return _toMidScrollOffsetSimulation(offset, dragVelocity);
if (dragVelocity < 0.0)
@@ -437,7 +419,7 @@ class _SnappingScrollPhysics extends ClampingScrollPhysics {
}
class AnimationDemoHome extends StatefulWidget {
const AnimationDemoHome({Key key}) : super(key: key);
const AnimationDemoHome({ Key key }) : super(key: key);
static const String routeName = '/animation';
@@ -465,21 +447,18 @@ class _AnimationDemoHomeState extends State<AnimationDemoHome> {
void _handleBackButton(double midScrollOffset) {
if (_scrollController.offset >= midScrollOffset)
_scrollController.animateTo(0.0,
curve: _kScrollCurve, duration: _kScrollDuration);
_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) {
bool _handleScrollNotification(ScrollNotification notification, double midScrollOffset) {
if (notification.depth == 0 && notification is ScrollUpdateNotification) {
final ScrollPhysics physics =
_scrollController.position.pixels >= midScrollOffset
? const PageScrollPhysics()
: const NeverScrollableScrollPhysics();
final ScrollPhysics physics = _scrollController.position.pixels >= midScrollOffset
? const PageScrollPhysics()
: const NeverScrollableScrollPhysics();
if (physics != _headingScrollPhysics) {
setState(() {
_headingScrollPhysics = physics;
@@ -493,35 +472,27 @@ class _AnimationDemoHomeState extends State<AnimationDemoHome> {
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);
_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);
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) {
bool _handlePageNotification(ScrollNotification notification, PageController leader, PageController follower) {
if (notification.depth == 0 && notification is ScrollUpdateNotification) {
selectedIndex.value = leader.page;
if (follower.page != leader.page)
// ignore: deprecated_member_use
follower.position.jumpToWithoutSettling(leader.position.pixels);
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) {
final Iterable<Widget> detailItems = section.details.map<Widget>((SectionDetail detail) {
return SectionDetailView(detail: detail);
});
return ListTile.divideTiles(context: context, tiles: detailItems);
@@ -533,33 +504,35 @@ class _AnimationDemoHomeState extends State<AnimationDemoHome> {
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);
});
}),
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,
color: _kAppBackgroundColor,
child: ClipRect(
child: _AllSectionsView(
sectionIndex: index,
sections: allSections,
selectedIndex: selectedIndex,
minHeight: _kAppBarMinHeight,
midHeight: _kAppBarMidHeight,
maxHeight: maxHeight,
sectionCards: sectionCards,
),
),
),
));
)
);
}
return headings;
}
@@ -571,21 +544,18 @@ class _AnimationDemoHomeState extends State<AnimationDemoHome> {
final double appBarMaxHeight = screenHeight - statusBarHeight;
// The scroll offset that reveals the appBarMidHeight appbar.
final double appBarMidScrollOffset =
statusBarHeight + appBarMaxHeight - _kAppBarMidHeight;
final double appBarMidScrollOffset = statusBarHeight + appBarMaxHeight - _kAppBarMidHeight;
return SizedBox.expand(
child: Stack(
children: <Widget>[
NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
return _handleScrollNotification(
notification, appBarMidScrollOffset);
return _handleScrollNotification(notification, appBarMidScrollOffset);
},
child: CustomScrollView(
controller: _scrollController,
physics: _SnappingScrollPhysics(
midScrollOffset: appBarMidScrollOffset),
physics: _SnappingScrollPhysics(midScrollOffset: appBarMidScrollOffset),
slivers: <Widget>[
// Start out below the status bar, gradually move to the top of the screen.
_StatusBarPaddingSliver(
@@ -600,14 +570,12 @@ class _AnimationDemoHomeState extends State<AnimationDemoHome> {
maxHeight: appBarMaxHeight,
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
return _handlePageNotification(notification,
_headingPageController, _detailsPageController);
return _handlePageNotification(notification, _headingPageController, _detailsPageController);
},
child: PageView(
physics: _headingScrollPhysics,
controller: _headingPageController,
children: _allHeadingItems(
appBarMaxHeight, appBarMidScrollOffset),
children: _allHeadingItems(appBarMaxHeight, appBarMidScrollOffset),
),
),
),
@@ -618,8 +586,7 @@ class _AnimationDemoHomeState extends State<AnimationDemoHome> {
height: 610.0,
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
return _handlePageNotification(notification,
_detailsPageController, _headingPageController);
return _handlePageNotification(notification, _detailsPageController, _headingPageController);
},
child: PageView(
controller: _detailsPageController,
@@ -645,11 +612,12 @@ class _AnimationDemoHomeState extends State<AnimationDemoHome> {
top: false,
bottom: false,
child: IconButton(
icon: const BackButtonIcon(),
tooltip: 'Back',
onPressed: () {
_handleBackButton(appBarMidScrollOffset);
}),
icon: const BackButtonIcon(),
tooltip: 'Back',
onPressed: () {
_handleBackButton(appBarMidScrollOffset);
},
),
),
),
),

View File

@@ -1,15 +1,17 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
// 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 = null;
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
class SectionDetail {
const SectionDetail({
@@ -41,8 +43,9 @@ class Section {
final List<SectionDetail> details;
@override
bool operator ==(Object other) {
if (other is! Section) return false;
bool operator==(Object other) {
if (other is! Section)
return false;
final Section otherSection = other;
return title == otherSection.title;
}

View File

@@ -1,8 +1,8 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter/material.dart';
import 'sections.dart';
@@ -10,9 +10,9 @@ 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);
const SectionCard({ Key key, @required this.section })
: assert(section != null),
super(key: key);
final Section section;
@@ -32,17 +32,12 @@ class SectionCard extends StatelessWidget {
],
),
),
// TODO(b:119312219): Remove Opacity layer when Image Color Filter
// is implemented in paintImage.
child: Opacity(
opacity: 0.075,
child: Image.asset(
section.backgroundAsset,
package: section.backgroundAssetPackage,
color: const Color.fromRGBO(255, 255, 255, 0.075),
colorBlendMode: BlendMode.modulate,
fit: BoxFit.cover,
),
child: Image.asset(
section.backgroundAsset,
package: section.backgroundAssetPackage,
color: const Color.fromRGBO(255, 255, 255, 0.075),
colorBlendMode: BlendMode.modulate,
fit: BoxFit.cover,
),
),
);
@@ -57,10 +52,10 @@ class SectionTitle extends StatelessWidget {
@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);
}) : assert(section != null),
assert(scale != null),
assert(opacity != null && opacity >= 0.0 && opacity <= 1.0),
super(key: key);
final Section section;
final double scale;
@@ -104,7 +99,7 @@ class SectionTitle extends StatelessWidget {
// Small horizontal bar that indicates the selected section.
class SectionIndicator extends StatelessWidget {
const SectionIndicator({Key key, this.opacity = 1.0}) : super(key: key);
const SectionIndicator({ Key key, this.opacity = 1.0 }) : super(key: key);
final double opacity;
@@ -122,10 +117,10 @@ class SectionIndicator extends StatelessWidget {
// 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);
SectionDetailView({ Key key, @required this.detail })
: assert(detail != null && detail.imageAsset != null),
assert((detail.imageAsset ?? detail.title) != null),
super(key: key);
final SectionDetail detail;

View File

@@ -1,8 +1,8 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter/material.dart';
import 'animation/home.dart';

View File

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

View File

@@ -0,0 +1,342 @@
// 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>[];
outList.add(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;
}
}

View File

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

View File

@@ -1,118 +1,61 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter/material.dart';
const double kColorItemHeight = 48.0;
class Palette {
Palette({this.name, this.primary, this.accent, this.threshold = 900});
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
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: '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: '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);
}) : 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()}";
String colorString() => "#${color.value.toRadixString(16).padLeft(8, '0').toUpperCase()}";
@override
Widget build(BuildContext context) {
@@ -143,52 +86,38 @@ class PaletteTabView extends StatelessWidget {
PaletteTabView({
Key key,
@required this.colors,
}) : assert(colors != null && colors.isValid),
super(key: key);
}) : 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> primaryKeys = <int>[50, 100, 200, 300, 400, 500, 600, 700, 800, 900];
static const List<int> accentKeys = <int>[100, 200, 400, 700];
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
final TextStyle whiteTextStyle =
textTheme.body1.copyWith(color: Colors.white);
final TextStyle blackTextStyle =
textTheme.body1.copyWith(color: Colors.black);
final List<Widget> colorItems = primaryKeys.map<Widget>((int index) {
return DefaultTextStyle(
style: index > colors.threshold ? whiteTextStyle : blackTextStyle,
child: ColorItem(index: index, color: colors.primary[index]),
);
}).toList();
if (colors.accent != null) {
colorItems.addAll(accentKeys.map<Widget>((int index) {
return DefaultTextStyle(
style: index > colors.threshold ? whiteTextStyle : blackTextStyle,
child:
ColorItem(index: index, color: colors.accent[index], prefix: 'A'),
);
}).toList());
}
return ListView(
itemExtent: kColorItemHeight,
children: colorItems,
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'),
);
}),
],
)
);
}
}
@@ -206,9 +135,7 @@ class ColorsDemo extends StatelessWidget {
title: const Text('Colors'),
bottom: TabBar(
isScrollable: true,
tabs: allPalettes
.map<Widget>((Palette swatch) => Tab(text: swatch.name))
.toList(),
tabs: allPalettes.map<Widget>((Palette swatch) => Tab(text: swatch.name)).toList(),
),
),
body: TabBarView(

View File

@@ -1,12 +1,12 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter_web/services.dart';
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);
const _ContactCategory({ Key key, this.icon, this.children }) : super(key: key);
final IconData icon;
final List<Widget> children;
@@ -17,7 +17,8 @@ class _ContactCategory extends StatelessWidget {
return Container(
padding: const EdgeInsets.symmetric(vertical: 16.0),
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: themeData.dividerColor))),
border: Border(bottom: BorderSide(color: themeData.dividerColor))
),
child: DefaultTextStyle(
style: Theme.of(context).textTheme.subhead,
child: SafeArea(
@@ -27,10 +28,11 @@ class _ContactCategory extends StatelessWidget {
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))
padding: const EdgeInsets.symmetric(vertical: 24.0),
width: 72.0,
child: Icon(icon, color: themeData.primaryColor),
),
Expanded(child: Column(children: children)),
],
),
),
@@ -40,9 +42,9 @@ class _ContactCategory extends StatelessWidget {
}
class _ContactItem extends StatelessWidget {
_ContactItem({Key key, this.icon, this.lines, this.tooltip, this.onPressed})
: assert(lines.length > 1),
super(key: key);
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;
@@ -52,32 +54,35 @@ class _ContactItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final ThemeData themeData = Theme.of(context);
final List<Widget> columnChildren = lines
.sublist(0, lines.length - 1)
.map<Widget>((String line) => Text(line))
.toList();
final List<Widget> columnChildren = lines.sublist(0, lines.length - 1).map<Widget>((String line) => Text(line)).toList();
columnChildren.add(Text(lines.last, style: themeData.textTheme.caption));
final List<Widget> rowChildren = <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: columnChildren))
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: columnChildren,
),
),
];
if (icon != null) {
rowChildren.add(SizedBox(
width: 72.0,
child: IconButton(
icon: Icon(icon),
color: themeData.primaryColor,
onPressed: onPressed)));
width: 72.0,
child: IconButton(
icon: Icon(icon),
color: themeData.primaryColor,
onPressed: onPressed,
),
));
}
return MergeSemantics(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: rowChildren)),
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: rowChildren,
),
),
);
}
}
@@ -92,8 +97,7 @@ class ContactsDemo extends StatefulWidget {
enum AppBarBehavior { normal, pinned, floating, snapping }
class ContactsDemoState extends State<ContactsDemo> {
static final GlobalKey<ScaffoldState> _scaffoldKey =
GlobalKey<ScaffoldState>();
static final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final double _appBarHeight = 256.0;
AppBarBehavior _appBarBehavior = AppBarBehavior.pinned;
@@ -113,8 +117,7 @@ class ContactsDemoState extends State<ContactsDemo> {
SliverAppBar(
expandedHeight: _appBarHeight,
pinned: _appBarBehavior == AppBarBehavior.pinned,
floating: _appBarBehavior == AppBarBehavior.floating ||
_appBarBehavior == AppBarBehavior.snapping,
floating: _appBarBehavior == AppBarBehavior.floating || _appBarBehavior == AppBarBehavior.snapping,
snap: _appBarBehavior == AppBarBehavior.snapping,
actions: <Widget>[
IconButton(
@@ -122,8 +125,8 @@ class ContactsDemoState extends State<ContactsDemo> {
tooltip: 'Edit',
onPressed: () {
_scaffoldKey.currentState.showSnackBar(const SnackBar(
content:
Text("Editing isn't supported in this screen.")));
content: Text("Editing isn't supported in this screen."),
));
},
),
PopupMenuButton<AppBarBehavior>(
@@ -132,20 +135,23 @@ class ContactsDemoState extends State<ContactsDemo> {
_appBarBehavior = value;
});
},
itemBuilder: (BuildContext context) =>
<PopupMenuItem<AppBarBehavior>>[
itemBuilder: (BuildContext context) => <PopupMenuItem<AppBarBehavior>>[
const PopupMenuItem<AppBarBehavior>(
value: AppBarBehavior.normal,
child: Text('App bar scrolls away')),
value: AppBarBehavior.normal,
child: Text('App bar scrolls away'),
),
const PopupMenuItem<AppBarBehavior>(
value: AppBarBehavior.pinned,
child: Text('App bar stays put')),
value: AppBarBehavior.pinned,
child: Text('App bar stays put'),
),
const PopupMenuItem<AppBarBehavior>(
value: AppBarBehavior.floating,
child: Text('App bar floats')),
value: AppBarBehavior.floating,
child: Text('App bar floats'),
),
const PopupMenuItem<AppBarBehavior>(
value: AppBarBehavior.snapping,
child: Text('App bar snaps')),
value: AppBarBehavior.snapping,
child: Text('App bar snaps'),
),
],
),
],
@@ -156,7 +162,7 @@ class ContactsDemoState extends State<ContactsDemo> {
children: <Widget>[
Image.asset(
'people/ali_landscape.png',
// package: 'flutter_gallery_assets',
package: 'flutter_gallery_assets',
fit: BoxFit.cover,
height: _appBarHeight,
),
@@ -187,8 +193,8 @@ class ContactsDemoState extends State<ContactsDemo> {
tooltip: 'Send message',
onPressed: () {
_scaffoldKey.currentState.showSnackBar(const SnackBar(
content: Text(
'Pretend that this opened your SMS application.')));
content: Text('Pretend that this opened your SMS application.'),
));
},
lines: const <String>[
'(650) 555-1234',
@@ -200,7 +206,8 @@ class ContactsDemoState extends State<ContactsDemo> {
tooltip: 'Send message',
onPressed: () {
_scaffoldKey.currentState.showSnackBar(const SnackBar(
content: Text('A messaging app appears.')));
content: Text('A messaging app appears.'),
));
},
lines: const <String>[
'(323) 555-6789',
@@ -212,8 +219,8 @@ class ContactsDemoState extends State<ContactsDemo> {
tooltip: 'Send message',
onPressed: () {
_scaffoldKey.currentState.showSnackBar(const SnackBar(
content: Text(
'Imagine if you will, a messaging application.')));
content: Text('Imagine if you will, a messaging application.'),
));
},
lines: const <String>[
'(650) 555-6789',
@@ -231,8 +238,8 @@ class ContactsDemoState extends State<ContactsDemo> {
tooltip: 'Send personal e-mail',
onPressed: () {
_scaffoldKey.currentState.showSnackBar(const SnackBar(
content: Text(
'Here, your e-mail application would open.')));
content: Text('Here, your e-mail application would open.'),
));
},
lines: const <String>[
'ali_connors@example.com',
@@ -244,8 +251,8 @@ class ContactsDemoState extends State<ContactsDemo> {
tooltip: 'Send work e-mail',
onPressed: () {
_scaffoldKey.currentState.showSnackBar(const SnackBar(
content: Text(
'Summon your favorite e-mail application here.')));
content: Text('Summon your favorite e-mail application here.'),
));
},
lines: const <String>[
'aliconnors@example.com',
@@ -262,8 +269,8 @@ class ContactsDemoState extends State<ContactsDemo> {
tooltip: 'Open map',
onPressed: () {
_scaffoldKey.currentState.showSnackBar(const SnackBar(
content: Text(
'This would show a map of San Francisco.')));
content: Text('This would show a map of San Francisco.'),
));
},
lines: const <String>[
'2000 Main Street',
@@ -276,8 +283,8 @@ class ContactsDemoState extends State<ContactsDemo> {
tooltip: 'Open map',
onPressed: () {
_scaffoldKey.currentState.showSnackBar(const SnackBar(
content: Text(
'This would show a map of Mountain View.')));
content: Text('This would show a map of Mountain View.'),
));
},
lines: const <String>[
'1600 Amphitheater Parkway',
@@ -290,8 +297,8 @@ class ContactsDemoState extends State<ContactsDemo> {
tooltip: 'Open map',
onPressed: () {
_scaffoldKey.currentState.showSnackBar(const SnackBar(
content: Text(
'This would also show a map, if this was not a demo.')));
content: Text('This would also show a map, if this was not a demo.'),
));
},
lines: const <String>[
'126 Severyns Ave',

View File

@@ -0,0 +1,14 @@
// 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';

View File

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

View File

@@ -0,0 +1,275 @@
// 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) {
final List<Widget> stackChildren = <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: () {
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');
},
),
],
),
);
},
),
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: () {
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');
},
),
],
),
);
},
),
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: () {
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.'),
),
);
},
),
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: () {
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');
},
),
),
);
},
),
],
),
),
];
if (lastSelectedValue != null) {
stackChildren.add(
Positioned(
bottom: 32.0,
child: Text('You selected: $lastSelectedValue'),
),
);
}
return Stack(
alignment: Alignment.center,
children: stackChildren,
);
},
),
),
);
}
}
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');
},
),
],
);
}
}

View File

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

View File

@@ -0,0 +1,804 @@
// 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(
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: 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)),
const Text(
'Buy this cool color',
style: TextStyle(
color: Color(0xFF8E8E93),
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: const Color(0xFFD9D9D9),
),
],
);
}
}
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: const TextStyle(
color: Color(0xFF8E8E93),
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>[
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: const BoxDecoration(
color: Color(0xFFE5E5E5),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const <Widget>[
Text(
'SUPPORT TICKET',
style: TextStyle(
color: Color(0xFF646464),
letterSpacing: -0.9,
fontSize: 14.0,
fontWeight: FontWeight.w500,
),
),
Text(
'Show More',
style: TextStyle(
color: Color(0xFF646464),
letterSpacing: -0.6,
fontSize: 12.0,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
Container(
decoration: const BoxDecoration(
color: Color(0xFFF3F3F3),
),
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) {
return Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(18.0)),
color: color == Tab2ConversationBubbleColor.blue
? CupertinoColors.activeBlue
: CupertinoColors.lightBackgroundGray,
),
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: color == Tab2ConversationBubbleColor.blue
? CupertinoColors.white
: CupertinoColors.black,
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 List<Widget> children = <Widget>[];
if (avatar != null)
children.add(avatar);
final bool isSelf = avatar == null;
children.add(
Tab2ConversationBubble(
text: text,
color: isSelf
? Tab2ConversationBubbleColor.blue
: Tab2ConversationBubbleColor.gray,
),
);
return SafeArea(
child: Row(
mainAxisAlignment: isSelf ? MainAxisAlignment.end : MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: isSelf ? CrossAxisAlignment.center : CrossAxisAlignment.end,
children: children,
),
);
}
}
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,
),
child: DecoratedBox(
decoration: BoxDecoration(
color: CupertinoTheme.of(context).brightness == Brightness.light
? CupertinoColors.extraLightBackgroundGray
: CupertinoColors.darkBackgroundGray,
),
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);
},
),
],
),
),
);
}
}

View File

@@ -0,0 +1,275 @@
// 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 _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 _buildMenu(List<Widget> children) {
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,
),
),
),
);
}
Widget _buildBottomPicker(Widget picker) {
return Container(
height: _kPickerSheetHeight,
padding: const EdgeInsets.only(top: 6.0),
color: CupertinoColors.white,
child: DefaultTextStyle(
style: const TextStyle(
color: CupertinoColors.black,
fontSize: 22.0,
),
child: GestureDetector(
// Blocks taps from propagating to the modal sheet and popping.
onTap: () { },
child: SafeArea(
top: false,
child: picker,
),
),
),
);
}
Widget _buildColorPicker(BuildContext context) {
final FixedExtentScrollController scrollController =
FixedExtentScrollController(initialItem: _selectedColorIndex);
return GestureDetector(
onTap: () async {
await showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) {
return _buildBottomPicker(
CupertinoPicker(
scrollController: scrollController,
itemExtent: _kPickerItemHeight,
backgroundColor: CupertinoColors.white,
onSelectedItemChanged: (int index) {
setState(() => _selectedColorIndex = index);
},
children: List<Widget>.generate(coolColorNames.length, (int index) {
return Center(
child: Text(coolColorNames[index]),
);
}),
),
);
},
);
},
child: _buildMenu(
<Widget>[
const Text('Favorite Color'),
Text(
coolColorNames[_selectedColorIndex],
style: const TextStyle(
color: CupertinoColors.inactiveGray
),
),
],
),
);
}
Widget _buildCountdownTimerPicker(BuildContext context) {
return GestureDetector(
onTap: () {
showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) {
return _buildBottomPicker(
CupertinoTimerPicker(
initialTimerDuration: timer,
onTimerDurationChanged: (Duration newTimer) {
setState(() => timer = newTimer);
},
),
);
},
);
},
child: _buildMenu(
<Widget>[
const Text('Countdown Timer'),
Text(
'${timer.inHours}:'
'${(timer.inMinutes % 60).toString().padLeft(2,'0')}:'
'${(timer.inSeconds % 60).toString().padLeft(2,'0')}',
style: const TextStyle(color: CupertinoColors.inactiveGray),
),
],
),
);
}
Widget _buildDatePicker(BuildContext context) {
return GestureDetector(
onTap: () {
showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) {
return _buildBottomPicker(
CupertinoDatePicker(
mode: CupertinoDatePickerMode.date,
initialDateTime: date,
onDateTimeChanged: (DateTime newDateTime) {
setState(() => date = newDateTime);
},
),
);
},
);
},
child: _buildMenu(
<Widget>[
const Text('Date'),
Text(
DateFormat.yMMMMd().format(date),
style: const TextStyle(color: CupertinoColors.inactiveGray),
),
]
),
);
}
Widget _buildTimePicker(BuildContext context) {
return GestureDetector(
onTap: () {
showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) {
return _buildBottomPicker(
CupertinoDatePicker(
mode: CupertinoDatePickerMode.time,
initialDateTime: time,
onDateTimeChanged: (DateTime newDateTime) {
setState(() => time = newDateTime);
},
),
);
},
);
},
child: _buildMenu(
<Widget>[
const Text('Time'),
Text(
DateFormat.jm().format(time),
style: const TextStyle(color: CupertinoColors.inactiveGray),
),
],
),
);
}
Widget _buildDateAndTimePicker(BuildContext context) {
return GestureDetector(
onTap: () {
showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) {
return _buildBottomPicker(
CupertinoDatePicker(
mode: CupertinoDatePickerMode.dateAndTime,
initialDateTime: dateTime,
onDateTimeChanged: (DateTime newDateTime) {
setState(() => dateTime = newDateTime);
},
),
);
},
);
},
child: _buildMenu(
<Widget>[
const Text('Date and Time'),
Text(
DateFormat.yMMMd().add_jm().format(dateTime),
style: const TextStyle(color: CupertinoColors.inactiveGray),
),
],
),
);
}
@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: DecoratedBox(
decoration: BoxDecoration(
color: CupertinoTheme.of(context).brightness == Brightness.light
? CupertinoColors.extraLightBackgroundGray
: CupertinoColors.darkBackgroundGray,
),
child: ListView(
children: <Widget>[
const Padding(padding: EdgeInsets.only(top: 32.0)),
_buildColorPicker(context),
_buildCountdownTimerPicker(context),
_buildDatePicker(context),
_buildTimePicker(context),
_buildDateAndTimePicker(context),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,244 @@
// 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(
child: DecoratedBox(
decoration: BoxDecoration(
color: CupertinoTheme.of(context).brightness == Brightness.light
? CupertinoColors.extraLightBackgroundGray
: CupertinoColors.darkBackgroundGray,
),
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: CupertinoTheme.of(context).scaffoldBackgroundColor,
height: 60.0,
padding: const EdgeInsets.only(top: 9.0),
child: Row(
children: <Widget>[
Container(
width: 38.0,
child: called
? const Align(
alignment: Alignment.topCenter,
child: Icon(
CupertinoIcons.phone_solid,
color: CupertinoColors.inactiveGray,
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: const TextStyle(
fontSize: 15.0,
letterSpacing: -0.24,
color: CupertinoColors.inactiveGray,
),
),
],
),
),
Text(
date,
style: const TextStyle(
color: CupertinoColors.inactiveGray,
fontSize: 15.0,
letterSpacing: -0.41,
),
),
Padding(
padding: const EdgeInsets.only(left: 9.0),
child: Icon(
CupertinoIcons.info,
color: CupertinoTheme.of(context).primaryColor,
),
),
],
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,127 @@
// 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 sharedValue = 0;
@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,
child: SafeArea(
child: Column(
children: <Widget>[
const Padding(
padding: EdgeInsets.all(16.0),
),
SizedBox(
width: 500.0,
child: CupertinoSegmentedControl<int>(
children: children,
onValueChanged: (int newValue) {
setState(() {
sharedValue = newValue;
});
},
groupValue: sharedValue,
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 32.0,
horizontal: 16.0,
),
child: 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[sharedValue],
),
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,78 @@
// 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'),
],
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,91 @@
// 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'
),
],
),
),
],
),
),
),
),
);
}
}

View File

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

View File

@@ -0,0 +1,52 @@
# 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)

View File

@@ -0,0 +1,236 @@
// 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>[
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;
}

View File

@@ -0,0 +1,40 @@
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',
),
),
),
],
);
}
}

View File

@@ -4,14 +4,14 @@
import 'dart:math' as math;
import 'package:flutter_web/material.dart';
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});
const Category({ this.title, this.assets });
final String title;
final List<String> assets;
@override
@@ -95,49 +95,52 @@ const List<Category> allCategories = <Category>[
];
class CategoryView extends StatelessWidget {
const CategoryView({Key key, this.category}) : super(key: key);
const CategoryView({ Key key, this.category }) : super(key: key);
final Category category;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return ListView(
key: PageStorageKey<Category>(category),
padding: const EdgeInsets.symmetric(
vertical: 16.0,
horizontal: 64.0,
),
children: category.assets.map<Widget>((String asset) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Card(
child: Container(
width: 144.0,
alignment: Alignment.center,
child: Column(
children: <Widget>[
Image.asset(
'$asset',
fit: BoxFit.contain,
),
Container(
padding: const EdgeInsets.only(bottom: 16.0),
alignment: AlignmentDirectional.center,
child: Text(
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,
style: theme.textTheme.caption,
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(),
const SizedBox(height: 24.0),
],
);
}).toList(),
)
);
}
}
@@ -242,8 +245,7 @@ class BackdropDemo extends StatefulWidget {
_BackdropDemoState createState() => _BackdropDemoState();
}
class _BackdropDemoState extends State<BackdropDemo>
with SingleTickerProviderStateMixin {
class _BackdropDemoState extends State<BackdropDemo> with SingleTickerProviderStateMixin {
final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop');
AnimationController _controller;
Category _category = allCategories[0];
@@ -273,8 +275,7 @@ class _BackdropDemoState extends State<BackdropDemo>
bool get _backdropPanelVisible {
final AnimationStatus status = _controller.status;
return status == AnimationStatus.completed ||
status == AnimationStatus.forward;
return status == AnimationStatus.completed || status == AnimationStatus.forward;
}
void _toggleBackdropPanelVisibility() {
@@ -290,19 +291,17 @@ class _BackdropDemoState extends State<BackdropDemo>
// 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;
if (_controller.isAnimating || _controller.status == AnimationStatus.completed)
return;
_controller.value -=
details.primaryDelta / (_backdropHeight ?? details.primaryDelta);
_controller.value -= details.primaryDelta / (_backdropHeight ?? details.primaryDelta);
}
void _handleDragEnd(DragEndDetails details) {
if (_controller.isAnimating ||
_controller.status == AnimationStatus.completed) return;
if (_controller.isAnimating || _controller.status == AnimationStatus.completed)
return;
final double flingVelocity =
details.velocity.pixelsPerSecond.dy / _backdropHeight;
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)
@@ -334,14 +333,15 @@ class _BackdropDemoState extends State<BackdropDemo>
);
final ThemeData theme = Theme.of(context);
final List<Widget> backdropItems =
allCategories.map<Widget>((Category category) {
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,
color: selected
? Colors.white.withOpacity(0.25)
: Colors.transparent,
child: ListTile(
title: Text(category.title),
selected: selected,

View File

@@ -0,0 +1,109 @@
// 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}'),);
}),
);
}
}

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
@@ -18,8 +18,7 @@ class BottomAppBarDemo extends StatefulWidget {
// for bottom application bar.
class _BottomAppBarDemoState extends State<BottomAppBarDemo> {
static final GlobalKey<ScaffoldState> _scaffoldKey =
GlobalKey<ScaffoldState>();
static final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
// FAB shape
@@ -64,41 +63,35 @@ class _BottomAppBarDemoState extends State<BottomAppBarDemo> {
// FAB Position
static const _ChoiceValue<FloatingActionButtonLocation> kFabEndDocked =
_ChoiceValue<FloatingActionButtonLocation>(
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>(
static const _ChoiceValue<FloatingActionButtonLocation> kFabCenterDocked = _ChoiceValue<FloatingActionButtonLocation>(
title: 'Attached - Center',
label:
'floating action button is docked at the center of the bottom app bar',
label: 'floating action button is docked at the center of the bottom app bar',
value: FloatingActionButtonLocation.centerDocked,
);
static const _ChoiceValue<FloatingActionButtonLocation> kFabEndFloat =
_ChoiceValue<FloatingActionButtonLocation>(
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>(
static const _ChoiceValue<FloatingActionButtonLocation> kFabCenterFloat = _ChoiceValue<FloatingActionButtonLocation>(
title: 'Free - Center',
label:
'floating action button is floats above the center of the bottom app bar',
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.';
"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)),
);
@@ -154,42 +147,45 @@ class _BottomAppBarDemoState extends State<BottomAppBarDemo> {
actions: <Widget>[
MaterialDemoDocumentationButton(BottomAppBarDemo.routeName),
IconButton(
icon: const Icon(Icons.sentiment_very_satisfied,
semanticLabel: 'Update shape'),
icon: const Icon(Icons.sentiment_very_satisfied, semanticLabel: 'Update shape'),
onPressed: () {
setState(() {
_fabShape =
_fabShape == kCircularFab ? kDiamondFab : kCircularFab;
_fabShape = _fabShape == kCircularFab ? kDiamondFab : kCircularFab;
});
},
),
],
),
body: ListView(
padding: const EdgeInsets.only(bottom: 88.0),
children: <Widget>[
const _Heading('FAB Shape'),
_RadioItem<Widget>(kCircularFab, _fabShape, _onFabShapeChanged),
_RadioItem<Widget>(kDiamondFab, _fabShape, _onFabShapeChanged),
_RadioItem<Widget>(kNoFab, _fabShape, _onFabShapeChanged),
const Divider(),
const _Heading('Notch'),
_RadioItem<bool>(kShowNotchTrue, _showNotch, _onShowNotchChanged),
_RadioItem<bool>(kShowNotchFalse, _showNotch, _onShowNotchChanged),
const Divider(),
const _Heading('FAB Position'),
_RadioItem<FloatingActionButtonLocation>(
kFabEndDocked, _fabLocation, _onFabLocationChanged),
_RadioItem<FloatingActionButtonLocation>(
kFabCenterDocked, _fabLocation, _onFabLocationChanged),
_RadioItem<FloatingActionButtonLocation>(
kFabEndFloat, _fabLocation, _onFabLocationChanged),
_RadioItem<FloatingActionButtonLocation>(
kFabCenterFloat, _fabLocation, _onFabLocationChanged),
const Divider(),
const _Heading('App bar color'),
_ColorsItem(kBabColors, _babColor, _onBabColorChanged),
],
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,
@@ -202,15 +198,18 @@ class _BottomAppBarDemoState extends State<BottomAppBarDemo> {
}
NotchedShape _selectNotch() {
if (!_showNotch.value) return null;
if (_fabShape == kCircularFab) return const CircularNotchedRectangle();
if (_fabShape == kDiamondFab) return const _DiamondNotchedRectangle();
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});
const _ChoiceValue({ this.value, this.title, this.label });
final T value;
final String title;
@@ -235,30 +234,32 @@ class _RadioItem<T> extends StatelessWidget {
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,
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,
),
),
),
),
),
]),
],
),
),
);
}
@@ -294,9 +295,7 @@ class _ColorsItem extends StatelessWidget {
fillColor: namedColor.color,
shape: CircleBorder(
side: BorderSide(
color: namedColor.color == selectedColor
? Colors.black
: const Color(0xFFD5D7DA),
color: namedColor.color == selectedColor ? Colors.black : const Color(0xFFD5D7DA),
width: 2.0,
),
),
@@ -333,69 +332,59 @@ class _Heading extends StatelessWidget {
}
class _DemoBottomAppBar extends StatelessWidget {
const _DemoBottomAppBar({this.color, this.fabLocation, this.shape});
const _DemoBottomAppBar({
this.color,
this.fabLocation,
this.shape,
});
final Color color;
final FloatingActionButtonLocation fabLocation;
final NotchedShape shape;
static final List<FloatingActionButtonLocation> kCenterLocations =
<FloatingActionButtonLocation>[
static final List<FloatingActionButtonLocation> kCenterLocations = <FloatingActionButtonLocation>[
FloatingActionButtonLocation.centerDocked,
FloatingActionButtonLocation.centerFloat,
];
@override
Widget build(BuildContext context) {
final List<Widget> rowContents = <Widget>[
IconButton(
icon: const Icon(Icons.menu, semanticLabel: 'Show bottom sheet'),
onPressed: () {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) => const _DemoDrawer(),
);
},
),
];
if (kCenterLocations.contains(fabLocation)) {
rowContents.add(
const Expanded(child: SizedBox()),
);
}
rowContents.addAll(<Widget>[
IconButton(
icon: const Icon(
Icons.search,
semanticLabel: 'show search action',
),
onPressed: () {
Scaffold.of(context).showSnackBar(
const SnackBar(content: Text('This is a dummy search action.')),
);
},
),
IconButton(
icon: Icon(
Theme.of(context).platform == TargetPlatform.iOS
? Icons.more_horiz
: Icons.more_vert,
semanticLabel: 'Show menu actions',
),
onPressed: () {
Scaffold.of(context).showSnackBar(
const SnackBar(content: Text('This is a dummy menu action.')),
);
},
),
]);
return BottomAppBar(
color: color,
child: Row(children: rowContents),
shape: shape,
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.')),
);
},
),
]),
);
}
}
@@ -459,7 +448,8 @@ class _DiamondNotchedRectangle implements NotchedShape {
@override
Path getOuterPath(Rect host, Rect guest) {
if (!host.overlaps(guest)) return Path()..addRect(host);
if (!host.overlaps(guest))
return Path()..addRect(host);
assert(guest.width > 0.0);
final Rect intersection = guest.intersect(host);
@@ -476,7 +466,8 @@ class _DiamondNotchedRectangle implements NotchedShape {
// 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);
intersection.height * (guest.height / 2.0)
/ (guest.width / 2.0);
return Path()
..moveTo(host.left, host.top)
@@ -499,22 +490,22 @@ class _DiamondBorder extends ShapeBorder {
}
@override
Path getInnerPath(Rect rect, {TextDirection textDirection}) {
Path getInnerPath(Rect rect, { TextDirection textDirection }) {
return getOuterPath(rect, textDirection: textDirection);
}
@override
Path getOuterPath(Rect rect, {TextDirection textDirection}) {
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.width / 2.0, rect.bottom)
..lineTo(rect.left, rect.top + rect.height / 2.0)
..close();
}
@override
void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) {}
void paint(Canvas canvas, Rect rect, { TextDirection textDirection }) { }
// This border doesn't support scaling.
@override

View File

@@ -1,8 +1,8 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
@@ -13,19 +13,19 @@ class NavigationIconView {
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,
) {
}) : _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),
));
@@ -38,8 +38,7 @@ class NavigationIconView {
final AnimationController controller;
Animation<double> _animation;
FadeTransition transition(
BottomNavigationBarType type, BuildContext context) {
FadeTransition transition(BottomNavigationBarType type, BuildContext context) {
Color iconColor;
if (type == BottomNavigationBarType.shifting) {
iconColor = _color;
@@ -92,12 +91,13 @@ class CustomInactiveIcon extends StatelessWidget {
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),
));
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),
),
);
}
}
@@ -150,27 +150,19 @@ class _BottomNavigationDemoState extends State<BottomNavigationDemo>
title: 'Event',
color: Colors.pink,
vsync: this,
)
),
];
for (NavigationIconView view in _navigationViews)
view.controller.addListener(_rebuild);
_navigationViews[_currentIndex].controller.value = 1.0;
}
@override
void dispose() {
for (NavigationIconView view in _navigationViews) view.controller.dispose();
for (NavigationIconView view in _navigationViews)
view.controller.dispose();
super.dispose();
}
void _rebuild() {
setState(() {
// Rebuild in order to animate views.
});
}
Widget _buildTransitionsStack() {
final List<FadeTransition> transitions = <FadeTransition>[];
@@ -193,8 +185,7 @@ class _BottomNavigationDemoState extends State<BottomNavigationDemo>
Widget build(BuildContext context) {
final BottomNavigationBar botNavBar = BottomNavigationBar(
items: _navigationViews
.map<BottomNavigationBarItem>(
(NavigationIconView navigationView) => navigationView.item)
.map<BottomNavigationBarItem>((NavigationIconView navigationView) => navigationView.item)
.toList(),
currentIndex: _currentIndex,
type: _type,
@@ -218,8 +209,7 @@ class _BottomNavigationDemoState extends State<BottomNavigationDemo>
_type = value;
});
},
itemBuilder: (BuildContext context) =>
<PopupMenuItem<BottomNavigationBarType>>[
itemBuilder: (BuildContext context) => <PopupMenuItem<BottomNavigationBarType>>[
const PopupMenuItem<BottomNavigationBarType>(
value: BottomNavigationBarType.fixed,
child: Text('Fixed'),
@@ -227,12 +217,14 @@ class _BottomNavigationDemoState extends State<BottomNavigationDemo>
const PopupMenuItem<BottomNavigationBarType>(
value: BottomNavigationBarType.shifting,
child: Text('Shifting'),
)
),
],
)
),
],
),
body: Center(child: _buildTransitionsStack()),
body: Center(
child: _buildTransitionsStack(),
),
bottomNavigationBar: botNavBar,
);
}

View File

@@ -0,0 +1,386 @@
// 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',
),
);
}
}

View File

@@ -1,142 +1,101 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/foundation.dart';
import 'package:flutter_web/material.dart';
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({
this.assetName,
this.assetPackage,
this.title,
this.description,
});
@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 List<String> description;
bool get isValid =>
assetName != null && title != null && description?.length == 3;
final String description;
final String city;
final String location;
final CardDemoType type;
}
final List<TravelDestination> destinations = <TravelDestination>[
const TravelDestination(
const List<TravelDestination> destinations = <TravelDestination>[
TravelDestination(
assetName: 'places/india_thanjavur_market.png',
assetPackage: _kGalleryAssetsPackage,
title: 'Top 10 Cities to Visit in Tamil Nadu',
description: <String>[
'Number 10',
'Thanjavur',
'Thanjavur, Tamil Nadu',
],
description: 'Number 10',
city: 'Thanjavur',
location: 'Thanjavur, Tamil Nadu',
),
const TravelDestination(
TravelDestination(
assetName: 'places/india_chettinad_silk_maker.png',
assetPackage: _kGalleryAssetsPackage,
title: 'Artisans of Southern India',
description: <String>[
'Silk Spinners',
'Chettinad',
'Sivaganga, Tamil Nadu',
],
)
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 {
TravelDestinationItem({Key key, @required this.destination, this.shape})
: assert(destination != null && destination.isValid),
super(key: key);
const TravelDestinationItem({ Key key, @required this.destination, this.shape })
: assert(destination != null),
super(key: key);
static const double height = 366.0;
// 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) {
final ThemeData theme = Theme.of(context);
final TextStyle titleStyle =
theme.textTheme.headline.copyWith(color: Colors.white);
final TextStyle descriptionStyle = theme.textTheme.subhead;
return Container(
padding: const EdgeInsets.all(8.0),
height: height,
child: Card(
shape: shape,
return SafeArea(
top: false,
bottom: false,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// photo and title
const SectionTitle(title: 'Normal'),
SizedBox(
height: 184.0,
child: Stack(
children: <Widget>[
Positioned(
bottom: 16.0,
left: 16.0,
right: 16.0,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
destination.title,
style: titleStyle,
),
),
),
],
),
),
// description and share/explore buttons
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0.0),
child: DefaultTextStyle(
softWrap: false,
overflow: TextOverflow.ellipsis,
style: descriptionStyle,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// three line description
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
destination.description[0],
style:
descriptionStyle.copyWith(color: Colors.black54),
),
),
Text(destination.description[1]),
Text(destination.description[2]),
],
),
),
),
),
// share, explore buttons
ButtonTheme.bar(
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
FlatButton(
child: const Text('SHARE'),
textColor: Colors.amber.shade500,
onPressed: () {/* do nothing */},
),
FlatButton(
child: const Text('EXPLORE'),
textColor: Colors.amber.shade500,
onPressed: () {/* do nothing */},
),
],
height: height,
child: Card(
// This ensures that the Card's children are clipped correctly.
clipBehavior: Clip.antiAlias,
shape: shape,
child: TravelDestinationContent(destination: destination),
),
),
],
@@ -146,6 +105,250 @@ class TravelDestinationItem extends StatelessWidget {
}
}
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;
final List<Widget> 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) {
children.add(
// 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'); },
),
],
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
);
}
}
class CardsDemo extends StatefulWidget {
static const String routeName = '/material/cards';
@@ -156,26 +359,57 @@ class CardsDemo extends StatefulWidget {
class _CardsDemoState extends State<CardsDemo> {
ShapeBorder _shape;
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
@override
Widget build(BuildContext context) {
return wrapScaffold('Cards Demo', context, _scaffoldKey,
_buildContents(context), CardsDemo.routeName);
}
Widget _buildContents(BuildContext context) {
return ListView(
itemExtent: TravelDestinationItem.height,
padding: const EdgeInsets.only(top: 8.0, left: 8.0, right: 8.0),
children: destinations.map((TravelDestination destination) {
return Container(
margin: const EdgeInsets.only(bottom: 8.0),
child: TravelDestinationItem(
destination: destination,
shape: _shape,
return Scaffold(
appBar: AppBar(
title: const Text('Cards'),
actions: <Widget>[
MaterialDemoDocumentationButton(CardsDemo.routeName),
IconButton(
icon: const Icon(
Icons.sentiment_very_satisfied,
semanticLabel: 'update shape',
),
);
}).toList());
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(),
),
),
);
}
}

View File

@@ -1,79 +1,335 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
class ChipDemo extends StatefulWidget {
static const routeName = '/material/chip';
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
State<StatefulWidget> createState() => _ChipDemoState();
Widget build(BuildContext context) {
final List<Widget> cardChildren = <Widget>[
Container(
padding: const EdgeInsets.only(top: 16.0, bottom: 4.0),
alignment: Alignment.center,
child: Text(label, textAlign: TextAlign.start),
),
];
if (children.isNotEmpty) {
cardChildren.add(Wrap(
children: children.map<Widget>((Widget chip) {
return Padding(
padding: const EdgeInsets.all(2.0),
child: chip,
);
}).toList()));
} else {
final TextStyle textStyle = Theme.of(context).textTheme.caption.copyWith(fontStyle: FontStyle.italic);
cardChildren.add(
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: textStyle),
),
));
}
return Card(
semanticContainer: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: cardChildren,
),
);
}
}
class ChipDemo extends StatefulWidget {
static const String routeName = '/material/chip';
@override
_ChipDemoState createState() => _ChipDemoState();
}
class _ChipDemoState extends State<ChipDemo> {
bool _filterChipSelected = false;
bool _hasAvatar = true;
_ChipDemoState() {
_reset();
}
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
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) {
return wrapScaffold('Chip Demo', context, _scaffoldKey, _buildContents(),
ChipDemo.routeName);
}
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();
Widget _buildContents() {
return Material(
child: Column(
children: [
addPadding(Chip(
label: Text('Chip'),
)),
addPadding(InputChip(
label: Text('InputChip'),
)),
addPadding(ChoiceChip(
label: Text('Selected ChoiceChip'),
selected: true,
)),
addPadding(ChoiceChip(
label: Text('Deselected ChoiceChip'),
selected: false,
)),
addPadding(FilterChip(
label: Text('FilterChip'),
selected: _filterChipSelected,
onSelected: (bool newValue) {
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(() {
_filterChipSelected = newValue;
_removeTool(name);
});
},
)),
addPadding(ActionChip(
label: Text('ActionChip'),
onPressed: () {},
)),
addPadding(ActionChip(
label: Text('Chip with avatar'),
avatar: _hasAvatar
? CircleAvatar(
backgroundColor: Colors.amber,
child: Text('Z'),
)
: null,
onPressed: () {
setState(() {
_hasAvatar = !_hasAvatar;
});
},
)),
],
));
});
}).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'),
),
);
}
}
Padding addPadding(Widget widget) => Padding(
padding: EdgeInsets.all(10.0),
child: widget,
);

View File

@@ -1,15 +1,14 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter_web/rendering.dart';
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);
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;
@@ -24,57 +23,60 @@ class Dessert {
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),
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) {
@@ -96,29 +98,31 @@ class DessertDataSource extends DataTableSource {
@override
DataRow getRow(int index) {
assert(index >= 0);
if (index >= _desserts.length) return null;
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}%')),
]);
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
@@ -131,7 +135,8 @@ class DessertDataSource extends DataTableSource {
int get selectedRowCount => _selectedCount;
void _selectAll(bool checked) {
for (Dessert dessert in _desserts) dessert.selected = checked;
for (Dessert dessert in _desserts)
dessert.selected = checked;
_selectedCount = checked ? _desserts.length : 0;
notifyListeners();
}
@@ -150,8 +155,7 @@ class _DataTableDemoState extends State<DataTableDemo> {
bool _sortAscending = true;
final DessertDataSource _dessertsDataSource = DessertDataSource();
void _sort<T>(
Comparable<T> getField(Dessert d), int columnIndex, bool ascending) {
void _sort<T>(Comparable<T> getField(Dessert d), int columnIndex, bool ascending) {
_dessertsDataSource._sort<T>(getField, ascending);
setState(() {
_sortColumnIndex = columnIndex;
@@ -162,70 +166,71 @@ class _DataTableDemoState extends State<DataTableDemo> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Data tables'),
actions: <Widget>[
MaterialDemoDocumentationButton(DataTableDemo.routeName),
],
),
body: ListView(padding: const EdgeInsets.all(20.0), children: <Widget>[
PaginatedDataTable(
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;
});
},
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)),
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)),
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)),
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)),
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)),
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)),
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)),
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)),
label: const Text('Iron (%)'),
numeric: true,
onSort: (int columnIndex, bool ascending) => _sort<num>((Dessert d) => d.iron, columnIndex, ascending),
),
],
source: _dessertsDataSource)
]));
source: _dessertsDataSource,
),
],
),
),
);
}
}

View File

@@ -1,23 +1,23 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
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);
const _InputDropdown({
Key key,
this.child,
this.labelText,
this.valueText,
this.valueStyle,
this.onPressed,
}) : super(key: key);
final String labelText;
final String valueText;
@@ -40,9 +40,8 @@ class _InputDropdown extends StatelessWidget {
children: <Widget>[
Text(valueText, style: valueStyle),
Icon(Icons.arrow_drop_down,
color: Theme.of(context).brightness == Brightness.light
? Colors.grey.shade700
: Colors.white70),
color: Theme.of(context).brightness == Brightness.light ? Colors.grey.shade700 : Colors.white70,
),
],
),
),
@@ -51,14 +50,14 @@ class _InputDropdown extends StatelessWidget {
}
class _DateTimePicker extends StatelessWidget {
const _DateTimePicker(
{Key key,
this.labelText,
this.selectedDate,
this.selectedTime,
this.selectDate,
this.selectTime})
: super(key: key);
const _DateTimePicker({
Key key,
this.labelText,
this.selectedDate,
this.selectedTime,
this.selectDate,
this.selectTime,
}) : super(key: key);
final String labelText;
final DateTime selectedDate;
@@ -68,17 +67,22 @@ class _DateTimePicker extends StatelessWidget {
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);
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);
final TimeOfDay picked = await showTimePicker(
context: context,
initialTime: selectedTime,
);
if (picked != null && picked != selectedTime)
selectTime(picked);
}
@override
@@ -93,9 +97,7 @@ class _DateTimePicker extends StatelessWidget {
labelText: labelText,
valueText: DateFormat.yMMMd().format(selectedDate),
valueStyle: valueStyle,
onPressed: () {
_selectDate(context);
},
onPressed: () { _selectDate(context); },
),
),
const SizedBox(width: 12.0),
@@ -104,9 +106,7 @@ class _DateTimePicker extends StatelessWidget {
child: _InputDropdown(
valueText: selectedTime.format(context),
valueStyle: valueStyle,
onPressed: () {
_selectTime(context);
},
onPressed: () { _selectTime(context); },
),
),
],
@@ -126,12 +126,7 @@ class _DateAndTimePickerDemoState extends State<DateAndTimePickerDemo> {
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'
];
final List<String> _allActivities = <String>['hiking', 'swimming', 'boating', 'fishing'];
String _activity = 'fishing';
@override
@@ -139,9 +134,7 @@ class _DateAndTimePickerDemoState extends State<DateAndTimePickerDemo> {
return Scaffold(
appBar: AppBar(
title: const Text('Date and time pickers'),
actions: <Widget>[
MaterialDemoDocumentationButton(DateAndTimePickerDemo.routeName)
],
actions: <Widget>[MaterialDemoDocumentationButton(DateAndTimePickerDemo.routeName)],
),
body: DropdownButtonHideUnderline(
child: SafeArea(
@@ -162,10 +155,7 @@ class _DateAndTimePickerDemoState extends State<DateAndTimePickerDemo> {
decoration: const InputDecoration(
labelText: 'Location',
),
style: Theme.of(context)
.textTheme
.display1
.copyWith(fontSize: 20.0),
style: Theme.of(context).textTheme.display1.copyWith(fontSize: 20.0),
),
_DateTimePicker(
labelText: 'From',
@@ -212,8 +202,7 @@ class _DateAndTimePickerDemoState extends State<DateAndTimePickerDemo> {
_activity = newValue;
});
},
items: _allActivities
.map<DropdownMenuItem<String>>((String value) {
items: _allActivities.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),

View File

@@ -1,8 +1,8 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
import 'full_screen_dialog_demo.dart';
@@ -17,13 +17,11 @@ enum DialogDemoAction {
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.';
'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);
const DialogDemoItem({ Key key, this.icon, this.color, this.text, this.onPressed }) : super(key: key);
final IconData icon;
final Color color;
@@ -68,15 +66,16 @@ class DialogDemoState extends State<DialogDemo> {
_selectedTime = TimeOfDay(hour: now.hour, minute: now.minute);
}
void showDemoDialog<T>({BuildContext context, Widget child}) {
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.
)
.then<void>((T value) { // The value passed to Navigator.pop() or null.
if (value != null) {
_scaffoldKey.currentState
.showSnackBar(SnackBar(content: Text('You selected: $value')));
_scaffoldKey.currentState.showSnackBar(SnackBar(
content: Text('You selected: $value'),
));
}
});
}
@@ -84,128 +83,132 @@ class DialogDemoState extends State<DialogDemo> {
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final TextStyle dialogTextStyle =
theme.textTheme.subhead.copyWith(color: theme.textTheme.caption.color);
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()));
key: _scaffoldKey,
appBar: AppBar(
title: const Text('Dialogs'),
actions: <Widget>[MaterialDemoDocumentationButton(DialogDemo.routeName)],
),
body: ListView(
padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 72.0),
children: <Widget>[
RaisedButton(
child: const Text('ALERT'),
onPressed: () {
showDemoDialog<DialogDemoAction>(
context: context,
child: AlertDialog(
content: Text(
_alertWithoutTitleText,
style: dialogTextStyle,
),
actions: <Widget>[
FlatButton(
child: const Text('CANCEL'),
onPressed: () { Navigator.pop(context, DialogDemoAction.cancel); },
),
FlatButton(
child: const Text('DISCARD'),
onPressed: () { Navigator.pop(context, DialogDemoAction.discard); },
),
],
),
);
},
),
RaisedButton(
child: const Text('ALERT WITH TITLE'),
onPressed: () {
showDemoDialog<DialogDemoAction>(
context: context,
child: AlertDialog(
title: const Text('Use Google\'s location service?'),
content: Text(
_alertWithTitleText,
style: dialogTextStyle,
),
actions: <Widget>[
FlatButton(
child: const Text('DISAGREE'),
onPressed: () { Navigator.pop(context, DialogDemoAction.disagree); },
),
FlatButton(
child: const Text('AGREE'),
onPressed: () { Navigator.pop(context, DialogDemoAction.agree); },
),
],
),
);
},
),
RaisedButton(
child: const Text('SIMPLE'),
onPressed: () {
showDemoDialog<String>(
context: context,
child: SimpleDialog(
title: const Text('Set backup account'),
children: <Widget>[
DialogDemoItem(
icon: Icons.account_circle,
color: theme.primaryColor,
text: 'username@gmail.com',
onPressed: () { Navigator.pop(context, 'username@gmail.com'); },
),
DialogDemoItem(
icon: Icons.account_circle,
color: theme.primaryColor,
text: 'user02@gmail.com',
onPressed: () { Navigator.pop(context, 'user02@gmail.com'); },
),
DialogDemoItem(
icon: Icons.add_circle,
text: 'add account',
color: theme.disabledColor,
),
],
),
);
},
),
RaisedButton(
child: const Text('CONFIRMATION'),
onPressed: () {
showTimePicker(
context: context,
initialTime: _selectedTime,
)
.then<void>((TimeOfDay value) {
if (value != null && value != _selectedTime) {
_selectedTime = value;
_scaffoldKey.currentState.showSnackBar(SnackBar(
content: Text('You selected: ${value.format(context)}'),
));
}
});
},
),
RaisedButton(
child: const Text('FULLSCREEN'),
onPressed: () {
Navigator.push(context, MaterialPageRoute<DismissDialogAction>(
builder: (BuildContext context) => FullScreenDialogDemo(),
fullscreenDialog: true,
));
},
),
]
// Add a little space between the buttons
.map<Widget>((Widget button) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: button,
);
})
.toList(),
),
);
}
}

View File

@@ -1,11 +1,17 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
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';
@@ -17,11 +23,7 @@ class _DrawerDemoState extends State<DrawerDemo> with TickerProviderStateMixin {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
static const List<String> _drawerContents = <String>[
'A',
'B',
'C',
'D',
'E',
'A', 'B', 'C', 'D', 'E',
];
static final Animatable<Offset> _drawerDetailsTween = Tween<Offset>(
@@ -70,13 +72,15 @@ class _DrawerDemoState extends State<DrawerDemo> with TickerProviderStateMixin {
void _showNotImplementedMessage() {
Navigator.pop(context); // Dismiss the drawer.
_scaffoldKey.currentState.showSnackBar(
const SnackBar(content: Text("The drawer's items don't do anything")));
_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(
@@ -88,9 +92,7 @@ class _DrawerDemoState extends State<DrawerDemo> with TickerProviderStateMixin {
},
),
title: const Text('Navigation drawer'),
actions: <Widget>[
MaterialDemoDocumentationButton(DrawerDemo.routeName)
],
actions: <Widget>[MaterialDemoDocumentationButton(DrawerDemo.routeName)],
),
drawer: Drawer(
child: Column(
@@ -98,6 +100,44 @@ class _DrawerDemoState extends State<DrawerDemo> with TickerProviderStateMixin {
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;
@@ -113,6 +153,7 @@ class _DrawerDemoState extends State<DrawerDemo> with TickerProviderStateMixin {
removeTop: true,
child: Expanded(
child: ListView(
dragStartBehavior: DragStartBehavior.down,
padding: const EdgeInsets.only(top: 8.0),
children: <Widget>[
Stack(
@@ -179,11 +220,19 @@ class _DrawerDemoState extends State<DrawerDemo> with TickerProviderStateMixin {
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',
child: Text('Tap here to open the drawer',
style: Theme.of(context).textTheme.subhead,
),
),
@@ -194,4 +243,23 @@ class _DrawerDemoState extends State<DrawerDemo> with TickerProviderStateMixin {
),
);
}
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);
},
),
],
);
},
);
}
}

View File

@@ -1,92 +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_web/material.dart';
class EditableTextDemo extends StatefulWidget {
static String routeName = '/material/editable_text';
@override
State<StatefulWidget> createState() => EditableTextDemoState();
}
class EditableTextDemoState extends State<EditableTextDemo> {
final cyanController = TextEditingController(text: 'Cyan');
final orangeController = TextEditingController(text: 'Orange');
final thickController = TextEditingController(text: 'Thick Rounded Cursor');
final multiController =
TextEditingController(text: 'First line\nSecond line');
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Text Editing'),
centerTitle: true,
),
body: ListView(
children: [
field(
cyanController,
color: Colors.cyan.shade50,
selection: Colors.cyan.shade200,
cursor: Colors.cyan.shade900,
),
field(
orangeController,
color: Colors.orange.shade50,
selection: Colors.orange.shade200,
cursor: Colors.orange.shade900,
center: true,
),
field(
thickController,
color: Colors.white,
selection: Colors.grey.shade200,
cursor: Colors.red.shade900,
radius: const Radius.circular(2),
cursorWidth: 8,
),
Banner(
child: TextField(
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
controller: multiController,
maxLines: 3,
),
message: 'W.I.P',
textDirection: TextDirection.ltr,
location: BannerLocation.bottomEnd,
),
],
),
);
}
}
Widget field(
TextEditingController controller, {
Color color,
Color selection,
Color cursor,
Radius radius = null,
double cursorWidth = 2,
bool center = false,
}) {
return Theme(
data: ThemeData(textSelectionColor: selection),
child: Container(
color: color,
child: TextField(
textAlign: center ? TextAlign.center : TextAlign.start,
decoration: InputDecoration(
contentPadding: EdgeInsets.fromLTRB(8, 16, 8, 16),
),
controller: controller,
cursorColor: cursor,
cursorRadius: radius,
cursorWidth: cursorWidth,
),
),
);
}

View File

@@ -1,8 +1,4 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
@@ -58,12 +54,10 @@ class _ElevationDemoState extends State<ElevationDemo> {
onPressed: () {
setState(() => _showElevation = !_showElevation);
},
)
),
],
),
body: ListView(
children: buildCards(),
),
body: Scrollbar(child: ListView(children: buildCards())),
);
}
}

View File

@@ -1,19 +1,28 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
@visibleForTesting
enum Location { Barbados, Bahamas, Bermuda }
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});
const DualHeaderWithHint({
this.name,
this.value,
this.hint,
this.showHint,
});
final String name;
final String value;
@@ -27,8 +36,7 @@ class DualHeaderWithHint extends StatelessWidget {
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,
crossFadeState: isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst,
duration: const Duration(milliseconds: 200),
);
}
@@ -38,37 +46,45 @@ class DualHeaderWithHint extends StatelessWidget {
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),
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(
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)))
]);
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});
const CollapsibleBody({
this.margin = EdgeInsets.zero,
this.child,
this.onSave,
this.onCancel,
});
final EdgeInsets margin;
final Widget child;
@@ -80,42 +96,62 @@ class CollapsibleBody extends StatelessWidget {
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,
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(
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(
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(
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')))
]))
]);
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));
DemoItem({
this.name,
this.value,
this.hint,
this.builder,
this.valueToString,
}) : textController = TextEditingController(text: valueToString(value));
final String name;
final String hint;
@@ -128,10 +164,11 @@ class DemoItem<T> {
ExpansionPanelHeaderBuilder get headerBuilder {
return (BuildContext context, bool isExpanded) {
return DualHeaderWithHint(
name: name,
value: valueToString(value),
hint: hint,
showHint: isExpanded);
name: name,
value: valueToString(value),
hint: hint,
showHint: isExpanded,
);
};
}
@@ -170,14 +207,8 @@ class _ExpansionPanelsDemoState extends State<ExpansionPanelsDemo> {
builder: (BuildContext context) {
return CollapsibleBody(
margin: const EdgeInsets.symmetric(horizontal: 16.0),
onSave: () {
Form.of(context).save();
close();
},
onCancel: () {
Form.of(context).reset();
close();
},
onSave: () { Form.of(context).save(); close(); },
onCancel: () { Form.of(context).reset(); close(); },
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: TextFormField(
@@ -186,9 +217,7 @@ class _ExpansionPanelsDemoState extends State<ExpansionPanelsDemo> {
hintText: item.hint,
labelText: item.name,
),
onSaved: (String value) {
item.value = value;
},
onSaved: (String value) { item.value = value; },
),
),
);
@@ -198,104 +227,101 @@ class _ExpansionPanelsDemoState extends State<ExpansionPanelsDemo> {
},
),
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>(
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;
},
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,
),
]);
}),
);
}));
}),
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;
});
}
name: 'Sun',
value: 80.0,
hint: 'Select sun level',
valueToString: (double amount) => '${amount.round()}',
builder: (DemoItem<double> item) {
void close() {
setState(() {
item.isExpanded = false;
});
}
return Form(child: Builder(builder: (BuildContext context) {
return CollapsibleBody(
onSave: () {
Form.of(context).save();
close();
},
onCancel: () {
Form.of(context).reset();
close();
},
child: FormField<double>(
initialValue: item.value,
onSaved: (double value) {
item.value = value;
},
builder: (FormFieldState<double> field) {
return Slider(
min: 0.0,
max: 100.0,
divisions: 5,
activeColor:
Colors.orange[100 + (field.value * 5.0).round()],
label: '${field.value.round()}',
value: field.value,
onChanged: field.didChange,
);
},
),
);
}));
})
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,
),
);
},
),
);
}
),
);
},
),
];
}
@@ -315,18 +341,19 @@ class _ExpansionPanelsDemoState extends State<ExpansionPanelsDemo> {
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()),
expansionCallback: (int index, bool isExpanded) {
setState(() {
_demoItems[index].isExpanded = !isExpanded;
});
},
children: _demoItems.map<ExpansionPanel>((DemoItem<dynamic> item) {
return ExpansionPanel(
isExpanded: item.isExpanded,
headerBuilder: item.headerBuilder,
body: item.build(),
);
}).toList(),
),
),
),
),

View File

@@ -0,0 +1,40 @@
// 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')),
],
),
),
);
}
}

View File

@@ -1,14 +1,14 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
// This demo is based on
// https://material.google.com/components/dialogs.html#dialogs-full-screen-dialogs
// https://material.io/design/components/dialogs.html#full-screen-dialog
enum DismissDialogAction {
cancel,
@@ -17,11 +17,11 @@ enum DismissDialogAction {
}
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);
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;
@@ -32,55 +32,66 @@ class DateTimeItem extends StatelessWidget {
final ThemeData theme = Theme.of(context);
return DefaultTextStyle(
style: theme.textTheme.subhead,
child: Row(children: <Widget>[
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),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
decoration: BoxDecoration(
border:
Border(bottom: BorderSide(color: theme.dividerColor))),
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)}'),
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),
],
),
),
),
],
),
);
}
}
@@ -100,35 +111,37 @@ class FullScreenDialogDemoState extends State<FullScreenDialogDemo> {
Future<bool> _onWillPop() async {
_saveNeeded = _hasLocation || _hasName || _saveNeeded;
if (!_saveNeeded) return true;
if (!_saveNeeded)
return true;
final ThemeData theme = Theme.of(context);
final TextStyle dialogTextStyle =
theme.textTheme.subhead.copyWith(color: theme.textTheme.caption.color);
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;
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
@@ -137,96 +150,119 @@ class FullScreenDialogDemoState extends State<FullScreenDialogDemo> {
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);
})
]),
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,
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())),
padding: const EdgeInsets.all(16.0),
children: <Widget>[
Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
alignment: Alignment.bottomLeft,
child: TextField(
decoration: const InputDecoration(
labelText: 'Event name',
filled: true,
),
style: theme.textTheme.headline,
onChanged: (String value) {
setState(() {
_hasName = value.isNotEmpty;
if (_hasName) {
_eventName = value;
}
});
},
),
),
Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
alignment: Alignment.bottomLeft,
child: TextField(
decoration: const InputDecoration(
labelText: 'Location',
hintText: 'Where is the event?',
filled: true,
),
onChanged: (String value) {
setState(() {
_hasLocation = value.isNotEmpty;
});
},
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('From', style: theme.textTheme.caption),
DateTimeItem(
dateTime: _fromDateTime,
onChanged: (DateTime value) {
setState(() {
_fromDateTime = value;
_saveNeeded = true;
});
},
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('To', style: theme.textTheme.caption),
DateTimeItem(
dateTime: _toDateTime,
onChanged: (DateTime value) {
setState(() {
_toDateTime = value;
_saveNeeded = true;
});
},
),
const Text('All-day'),
],
),
Container(
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: theme.dividerColor))
),
child: Row(
children: <Widget> [
Checkbox(
value: _allDayValue,
onChanged: (bool value) {
setState(() {
_allDayValue = value;
_saveNeeded = true;
});
},
),
const Text('All-day'),
],
),
),
]
.map<Widget>((Widget child) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
height: 96.0,
child: child,
);
})
.toList(),
),
),
),
);
}
}

View File

@@ -1,12 +1,16 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
enum GridDemoTileStyle { imageOnly, oneLine, twoLine }
enum GridDemoTileStyle {
imageOnly,
oneLine,
twoLine
}
typedef BannerTapCallback = void Function(Photo photo);
@@ -30,15 +34,11 @@ class Photo {
bool isFavorite;
String get tag => assetName; // Assuming that all asset names are unique.
bool get isValid =>
assetName != null &&
title != null &&
caption != null &&
isFavorite != null;
bool get isValid => assetName != null && title != null && caption != null && isFavorite != null;
}
class GridPhotoViewer extends StatefulWidget {
const GridPhotoViewer({Key key, this.photo}) : super(key: key);
const GridPhotoViewer({ Key key, this.photo }) : super(key: key);
final Photo photo;
@@ -61,8 +61,7 @@ class _GridTitleText extends StatelessWidget {
}
}
class _GridPhotoViewerState extends State<GridPhotoViewer>
with SingleTickerProviderStateMixin {
class _GridPhotoViewerState extends State<GridPhotoViewer> with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<Offset> _flingAnimation;
Offset _offset = Offset.zero;
@@ -88,8 +87,7 @@ class _GridPhotoViewerState extends State<GridPhotoViewer>
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));
return Offset(offset.dx.clamp(minOffset.dx, 0.0), offset.dy.clamp(minOffset.dy, 0.0));
}
void _handleFlingAnimation() {
@@ -117,11 +115,14 @@ class _GridPhotoViewerState extends State<GridPhotoViewer>
void _handleOnScaleEnd(ScaleEndDetails details) {
final double magnitude = details.velocity.pixelsPerSecond.distance;
if (magnitude < _kMinFlingVelocity) return;
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)));
begin: _offset,
end: _clampOffset(_offset + direction * distance),
));
_controller
..value = 0.0
..fling(velocity: magnitude / 1000.0);
@@ -139,8 +140,8 @@ class _GridPhotoViewerState extends State<GridPhotoViewer>
..translate(_offset.dx, _offset.dy)
..scale(_scale),
child: Image.asset(
'${widget.photo.assetName}',
// TODO(flutter_web): package: widget.photo.assetPackage,
widget.photo.assetName,
package: widget.photo.assetPackage,
fit: BoxFit.cover,
),
),
@@ -150,50 +151,52 @@ class _GridPhotoViewerState extends State<GridPhotoViewer>
}
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);
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.
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),
Navigator.push(context, MaterialPageRoute<void>(
builder: (BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(photo.title),
),
),
);
}));
body: SizedBox.expand(
child: Hero(
tag: photo.tag,
child: GridPhotoViewer(photo: photo),
),
),
);
}
));
}
@override
Widget build(BuildContext context) {
final Widget image = GestureDetector(
onTap: () {
showPhoto(context);
},
child: Hero(
key: Key(photo.assetName),
tag: photo.tag,
child: Image.asset(
'${photo.assetName}',
// TODO(flutter_web): package: photo.assetPackage,
fit: BoxFit.cover,
)));
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;
@@ -204,9 +207,7 @@ class GridDemoPhotoItem extends StatelessWidget {
case GridDemoTileStyle.oneLine:
return GridTile(
header: GestureDetector(
onTap: () {
onBannerTap(photo);
},
onTap: () { onBannerTap(photo); },
child: GridTileBar(
title: _GridTitleText(photo.title),
backgroundColor: Colors.black45,
@@ -222,9 +223,7 @@ class GridDemoPhotoItem extends StatelessWidget {
case GridDemoTileStyle.twoLine:
return GridTile(
footer: GestureDetector(
onTap: () {
onBannerTap(photo);
},
onTap: () { onBannerTap(photo); },
child: GridTileBar(
backgroundColor: Colors.black45,
title: _GridTitleText(photo.title),
@@ -244,7 +243,7 @@ class GridDemoPhotoItem extends StatelessWidget {
}
class GridListDemo extends StatefulWidget {
const GridListDemo({Key key}) : super(key: key);
const GridListDemo({ Key key }) : super(key: key);
static const String routeName = '/material/grid-list';
@@ -346,8 +345,7 @@ class GridListDemoState extends State<GridListDemo> {
MaterialDemoDocumentationButton(GridListDemo.routeName),
PopupMenuButton<GridDemoTileStyle>(
onSelected: changeTileStyle,
itemBuilder: (BuildContext context) =>
<PopupMenuItem<GridDemoTileStyle>>[
itemBuilder: (BuildContext context) => <PopupMenuItem<GridDemoTileStyle>>[
const PopupMenuItem<GridDemoTileStyle>(
value: GridDemoTileStyle.imageOnly,
child: Text('Image only'),
@@ -375,17 +373,17 @@ class GridListDemoState extends State<GridListDemo> {
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
padding: const EdgeInsets.all(4.0),
childAspectRatio:
(orientation == Orientation.portrait) ? 1.0 : 1.3,
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;
});
photo: photo,
tileStyle: _tileStyle,
onBannerTap: (Photo photo) {
setState(() {
photo.isFavorite = !photo.isFavorite;
});
},
);
}).toList(),
),
),

View File

@@ -1,8 +1,8 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
@@ -58,15 +58,15 @@ class IconsDemoState extends State<IconsDemo> {
child: SafeArea(
top: false,
bottom: false,
child: ListView(
padding: const EdgeInsets.all(24.0),
children: <Widget>[
_IconsDemoCard(
handleIconButtonPress, Icons.face), // direction-agnostic icon
const SizedBox(height: 24.0),
_IconsDemoCard(handleIconButtonPress,
Icons.battery_unknown), // direction-aware icon
],
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
],
),
),
),
),
@@ -82,21 +82,23 @@ class _IconsDemoCard extends StatelessWidget {
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);
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),
);
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>[
children: <Widget> [
_centeredText(size.floor().toString()),
_buildIconButton(size, icon, true),
_buildIconButton(size, icon, false),
@@ -107,8 +109,7 @@ class _IconsDemoCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final TextStyle textStyle =
theme.textTheme.subhead.copyWith(color: theme.textTheme.caption.color);
final TextStyle textStyle = theme.textTheme.subhead.copyWith(color: theme.textTheme.caption.color);
return Card(
child: DefaultTextStyle(
style: textStyle,
@@ -116,12 +117,14 @@ class _IconsDemoCard extends StatelessWidget {
explicitChildNodes: true,
child: Table(
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
children: <TableRow>[
TableRow(children: <Widget>[
_centeredText('Size'),
_centeredText('Enabled'),
_centeredText('Disabled'),
]),
children: <TableRow> [
TableRow(
children: <Widget> [
_centeredText('Size'),
_centeredText('Enabled'),
_centeredText('Disabled'),
]
),
_buildIconRow(18.0),
_buildIconRow(24.0),
_buildIconRow(36.0),

View File

@@ -1,24 +1,27 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter_web/semantics.dart';
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import '../../gallery/demo.dart';
enum LeaveBehindDemoAction { reset, horizontalSwipe, leftSwipe, rightSwipe }
enum LeaveBehindDemoAction {
reset,
horizontalSwipe,
leftSwipe,
rightSwipe,
confirmDismiss,
}
class LeaveBehindItem implements Comparable<LeaveBehindItem> {
LeaveBehindItem({this.index, this.name, this.subject, this.body});
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;
: index = item.index, name = item.name, subject = item.subject, body = item.body;
final int index;
final String name;
@@ -30,7 +33,7 @@ class LeaveBehindItem implements Comparable<LeaveBehindItem> {
}
class LeaveBehindDemo extends StatefulWidget {
const LeaveBehindDemo({Key key}) : super(key: key);
const LeaveBehindDemo({ Key key }) : super(key: key);
static const String routeName = '/material/leave-behind';
@@ -39,18 +42,19 @@ class LeaveBehindDemo extends StatefulWidget {
}
class LeaveBehindDemoState extends State<LeaveBehindDemo> {
static final GlobalKey<ScaffoldState> _scaffoldKey =
GlobalKey<ScaffoldState>();
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...");
index: index,
name: 'Item $index Sender',
subject: 'Subject: $index',
body: "[$index] first line of the message's body...",
);
});
}
@@ -75,6 +79,9 @@ class LeaveBehindDemoState extends State<LeaveBehindDemo> {
case LeaveBehindDemoAction.rightSwipe:
_dismissDirection = DismissDirection.startToEnd;
break;
case LeaveBehindDemoAction.confirmDismiss:
_confirmDismiss = !_confirmDismiss;
break;
}
});
}
@@ -91,12 +98,12 @@ class LeaveBehindDemoState extends State<LeaveBehindDemo> {
leaveBehindItems.remove(item);
});
_scaffoldKey.currentState.showSnackBar(SnackBar(
content: Text('You archived item ${item.index}'),
action: SnackBarAction(
label: 'UNDO',
onPressed: () {
handleUndo(item);
})));
content: Text('You archived item ${item.index}'),
action: SnackBarAction(
label: 'UNDO',
onPressed: () { handleUndo(item); },
),
));
}
void _handleDelete(LeaveBehindItem item) {
@@ -104,12 +111,12 @@ class LeaveBehindDemoState extends State<LeaveBehindDemo> {
leaveBehindItems.remove(item);
});
_scaffoldKey.currentState.showSnackBar(SnackBar(
content: Text('You deleted item ${item.index}'),
action: SnackBarAction(
label: 'UNDO',
onPressed: () {
handleUndo(item);
})));
content: Text('You deleted item ${item.index}'),
action: SnackBarAction(
label: 'UNDO',
onPressed: () { handleUndo(item); },
),
));
}
@override
@@ -123,43 +130,59 @@ class LeaveBehindDemoState extends State<LeaveBehindDemo> {
),
);
} else {
body = ListView(
body = Scrollbar(
child: ListView(
children: leaveBehindItems.map<Widget>((LeaveBehindItem item) {
return _LeaveBehindListItem(
item: item,
onArchive: _handleArchive,
onDelete: _handleDelete,
dismissDirection: _dismissDirection,
);
}).toList());
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>(
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'))
])
]),
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,
);
}
@@ -172,12 +195,14 @@ class _LeaveBehindListItem extends StatelessWidget {
@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);
@@ -204,25 +229,70 @@ class _LeaveBehindListItem extends StatelessWidget {
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 ListTile(
leading: Icon(Icons.delete, color: Colors.white, size: 36.0))),
color: theme.primaryColor,
child: const ListTile(
leading: Icon(Icons.delete, color: Colors.white, size: 36.0),
),
),
secondaryBackground: Container(
color: theme.primaryColor,
child: const ListTile(
trailing:
Icon(Icons.archive, color: Colors.white, size: 36.0))),
color: theme.primaryColor,
child: const ListTile(
trailing: Icon(Icons.archive, color: Colors.white, size: 36.0),
),
),
child: Container(
decoration: BoxDecoration(
color: theme.canvasColor,
border: Border(bottom: BorderSide(color: theme.dividerColor))),
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),
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
},
),
],
);
},
);
}
}

View File

@@ -1,8 +1,8 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
@@ -21,7 +21,7 @@ enum _MaterialListType {
}
class ListDemo extends StatefulWidget {
const ListDemo({Key key}) : super(key: key);
const ListDemo({ Key key }) : super(key: key);
static const String routeName = '/material/list';
@@ -30,8 +30,7 @@ class ListDemo extends StatefulWidget {
}
class _ListDemoState extends State<ListDemo> {
static final GlobalKey<ScaffoldState> scaffoldKey =
GlobalKey<ScaffoldState>();
static final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
PersistentBottomSheetController<void> _bottomSheet;
_MaterialListType _itemType = _MaterialListType.threeLine;
@@ -41,33 +40,18 @@ class _ListDemoState extends State<ListDemo> {
bool _showDividers = false;
bool _reverseSort = false;
List<String> items = <String>[
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
];
void changeItemType(_MaterialListType type) {
setState(() {
_itemType = type;
});
_bottomSheet?.setState(() {});
_bottomSheet?.setState(() { });
}
void _showConfigurationSheet() {
final PersistentBottomSheetController<void> bottomSheet = scaffoldKey
.currentState
.showBottomSheet<void>((BuildContext bottomSheetContext) {
final PersistentBottomSheetController<void> bottomSheet = scaffoldKey.currentState.showBottomSheet<void>((BuildContext bottomSheetContext) {
return Container(
decoration: const BoxDecoration(
border: Border(top: BorderSide(color: Colors.black26)),
@@ -78,25 +62,25 @@ class _ListDemoState extends State<ListDemo> {
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,
)),
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,
)),
dense: true,
title: const Text('Two-line'),
trailing: Radio<_MaterialListType>(
value: _MaterialListType.twoLine,
groupValue: _itemType,
onChanged: changeItemType,
),
),
),
MergeSemantics(
child: ListTile(
@@ -119,7 +103,7 @@ class _ListDemoState extends State<ListDemo> {
setState(() {
_showAvatars = value;
});
_bottomSheet?.setState(() {});
_bottomSheet?.setState(() { });
},
),
),
@@ -134,7 +118,7 @@ class _ListDemoState extends State<ListDemo> {
setState(() {
_showIcons = value;
});
_bottomSheet?.setState(() {});
_bottomSheet?.setState(() { });
},
),
),
@@ -149,7 +133,7 @@ class _ListDemoState extends State<ListDemo> {
setState(() {
_showDividers = value;
});
_bottomSheet?.setState(() {});
_bottomSheet?.setState(() { });
},
),
),
@@ -164,7 +148,7 @@ class _ListDemoState extends State<ListDemo> {
setState(() {
_dense = value;
});
_bottomSheet?.setState(() {});
_bottomSheet?.setState(() { });
},
),
),
@@ -200,14 +184,10 @@ class _ListDemoState extends State<ListDemo> {
child: ListTile(
isThreeLine: _itemType == _MaterialListType.threeLine,
dense: _dense,
leading: _showAvatars
? ExcludeSemantics(child: CircleAvatar(child: Text(item)))
: null,
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,
trailing: _showIcons ? Icon(Icons.info, color: Theme.of(context).disabledColor) : null,
),
);
}
@@ -229,8 +209,7 @@ class _ListDemoState extends State<ListDemo> {
break;
}
Iterable<Widget> listTiles =
items.map<Widget>((String item) => buildListTile(context, item));
Iterable<Widget> listTiles = items.map<Widget>((String item) => buildListTile(context, item));
if (_showDividers)
listTiles = ListTile.divideTiles(context: context, tiles: listTiles);
@@ -246,8 +225,7 @@ class _ListDemoState extends State<ListDemo> {
onPressed: () {
setState(() {
_reverseSort = !_reverseSort;
items.sort((String a, String b) =>
_reverseSort ? b.compareTo(a) : a.compareTo(b));
items.sort((String a, String b) => _reverseSort ? b.compareTo(a) : a.compareTo(b));
});
},
),

View File

@@ -1,20 +1,21 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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 'material_button_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 'editable_text_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';
@@ -33,7 +34,5 @@ export 'slider_demo.dart';
export 'snack_bar_demo.dart';
export 'tabs_demo.dart';
export 'tabs_fab_demo.dart';
export 'text_demo.dart';
export 'text_form_field_demo.dart';
export 'tooltip_demo.dart';
export 'two_level_list_demo.dart';

View File

@@ -1,103 +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_web/material.dart';
import '../../gallery/demo.dart';
class ButtonsDemo extends StatelessWidget {
static const String routeName = '/material/buttons';
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
@override
Widget build(BuildContext context) {
IconData _backIcon() {
switch (Theme.of(context).platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
return Icons.arrow_back;
case TargetPlatform.iOS:
return Icons.arrow_back_ios;
}
assert(false);
return null;
}
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
leading: IconButton(
icon: Icon(_backIcon()),
alignment: Alignment.centerLeft,
tooltip: 'Back',
onPressed: () {
Navigator.pop(context);
},
),
title: const Text('Material buttons'),
actions: <Widget>[
MaterialDemoDocumentationButton(ButtonsDemo.routeName)
],
),
body: Center(
child: _buildButtons(),
),
);
}
Widget _buildButtons() {
return Column(
children: [
pad(MaterialButton(
onPressed: () {
print('MaterialButton pressed');
},
elevation: 3.0,
child: Text('MaterialButton'),
)),
pad(FlatButton(
onPressed: () {
print('FlatButton pressed');
},
child: Text('FlatButton'),
)),
pad(RaisedButton(
onPressed: () {},
elevation: 0.0,
child: Text('RaisedButton 0.0'),
)),
pad(RaisedButton(
onPressed: () {},
elevation: 1.0,
child: Text('RaisedButton 1.0'),
)),
pad(RaisedButton(
onPressed: () {},
elevation: 2.0,
child: Text('RaisedButton 2.0'),
)),
pad(RaisedButton(
onPressed: () {},
elevation: 3.0,
child: Text('RaisedButton 3.0'),
)),
pad(RaisedButton(
onPressed: () {},
elevation: 4.0,
child: Text('RaisedButton 4.0'),
)),
pad(RaisedButton(
onPressed: () {},
elevation: 8.0,
child: Text('RaisedButton 8.0'),
)),
],
);
}
}
Padding pad(Widget widget) => Padding(
padding: EdgeInsets.all(10.0),
child: widget,
);

View File

@@ -1,13 +1,13 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
class MenuDemo extends StatefulWidget {
const MenuDemo({Key key}) : super(key: key);
const MenuDemo({ Key key }) : super(key: key);
static const String routeName = '/material/menu';
@@ -37,7 +37,9 @@ class MenuDemoState extends State<MenuDemo> {
}
void showInSnackBar(String value) {
_scaffoldKey.currentState.showSnackBar(SnackBar(content: Text(value)));
_scaffoldKey.currentState.showSnackBar(SnackBar(
content: Text(value),
));
}
void showMenuSelection(String value) {
@@ -60,122 +62,158 @@ class MenuDemoState extends State<MenuDemo> {
@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>[
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')),
])),
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')))
])),
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))
]),
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))
]))
]));
title: const Text('An item with a checklist menu'),
trailing: PopupMenuButton<String>(
padding: EdgeInsets.zero,
onSelected: showCheckedMenuSelections,
itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
CheckedPopupMenuItem<String>(
value: _checkedValue1,
checked: isChecked(_checkedValue1),
child: Text(_checkedValue1),
),
CheckedPopupMenuItem<String>(
value: _checkedValue2,
enabled: false,
checked: isChecked(_checkedValue2),
child: Text(_checkedValue2),
),
CheckedPopupMenuItem<String>(
value: _checkedValue3,
checked: isChecked(_checkedValue3),
child: Text(_checkedValue3),
),
CheckedPopupMenuItem<String>(
value: _checkedValue4,
checked: isChecked(_checkedValue4),
child: Text(_checkedValue4),
),
],
),
),
],
),
);
}
}

View File

@@ -1,8 +1,8 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
@@ -12,27 +12,31 @@ class ModalBottomSheetDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Modal bottom sheet'),
actions: <Widget>[MaterialDemoDocumentationButton(routeName)],
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,
),
),
),
);
});
},
),
body: Center(
child: RaisedButton(
child: const Text('SHOW BOTTOM SHEET'),
onPressed: () {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return Container(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Text(
'This is the modal bottom sheet. Tap anywhere to dismiss.',
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).accentColor,
fontSize: 24.0))));
});
})));
),
);
}
}

View File

@@ -1,17 +1,17 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
enum IndicatorType { overscroll, refresh }
class OverscrollDemo extends StatefulWidget {
const OverscrollDemo({Key key}) : super(key: key);
const OverscrollDemo({ Key key }) : super(key: key);
static const String routeName = '/material/overscroll';
@@ -21,38 +21,24 @@ class OverscrollDemo extends StatefulWidget {
class OverscrollDemoState extends State<OverscrollDemo> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
GlobalKey<RefreshIndicatorState>();
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'
'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();
});
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();
})));
content: const Text('Refresh complete'),
action: SnackBarAction(
label: 'RETRY',
onPressed: () {
_refreshIndicatorKey.currentState.show();
},
),
));
});
}
@@ -60,31 +46,36 @@ class OverscrollDemoState extends State<OverscrollDemo> {
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(title: const Text('Pull to refresh'), actions: <Widget>[
MaterialDemoDocumentationButton(OverscrollDemo.routeName),
IconButton(
appBar: AppBar(
title: const Text('Pull to refresh'),
actions: <Widget>[
MaterialDemoDocumentationButton(OverscrollDemo.routeName),
IconButton(
icon: const Icon(Icons.refresh),
tooltip: 'Refresh',
onPressed: () {
_refreshIndicatorKey.currentState.show();
}),
]),
},
),
],
),
body: RefreshIndicator(
key: _refreshIndicatorKey,
onRefresh: _handleRefresh,
child: ListView.builder(
padding: kMaterialListPadding,
itemCount: _items.length,
itemBuilder: (BuildContext context, int index) {
final String item = _items[index];
return ListTile(
isThreeLine: true,
leading: CircleAvatar(child: Text(item)),
title: Text('This item represents $item.'),
subtitle: const Text(
'Even more additional list item information appears on line three.'),
);
},
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.'),
);
},
),
),
),
);

View File

@@ -1,21 +1,20 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
class _PageSelector extends StatelessWidget {
const _PageSelector({this.icons});
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));
controller.animateTo((controller.index + delta).clamp(0, icons.length - 1));
}
@override
@@ -28,24 +27,26 @@ class _PageSelector extends StatelessWidget {
child: Column(
children: <Widget>[
Container(
margin: const EdgeInsets.only(top: 16.0),
child: Row(children: <Widget>[
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'),
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)),
icon: const Icon(Icons.chevron_right),
color: color,
onPressed: () { _handleArrowButtonPress(context, 1); },
tooltip: 'Page forward',
),
],
mainAxisAlignment: MainAxisAlignment.spaceBetween,
),
),
Expanded(
child: IconTheme(
data: IconThemeData(
@@ -53,16 +54,17 @@ class _PageSelector extends StatelessWidget {
color: color,
),
child: TabBarView(
children: icons.map<Widget>((Icon icon) {
return Container(
padding: const EdgeInsets.all(12.0),
child: Card(
child: Center(
child: icon,
children: icons.map<Widget>((Icon icon) {
return Container(
padding: const EdgeInsets.all(12.0),
child: Card(
child: Center(
child: icon,
),
),
),
);
}).toList()),
);
}).toList(),
),
),
),
],

View File

@@ -1,8 +1,8 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
@@ -10,8 +10,7 @@ class PersistentBottomSheetDemo extends StatefulWidget {
static const String routeName = '/material/persistent-bottom-sheet';
@override
_PersistentBottomSheetDemoState createState() =>
_PersistentBottomSheetDemoState();
_PersistentBottomSheetDemoState createState() => _PersistentBottomSheetDemoState();
}
class _PersistentBottomSheetDemoState extends State<PersistentBottomSheetDemo> {
@@ -26,36 +25,34 @@ class _PersistentBottomSheetDemoState extends State<PersistentBottomSheetDemo> {
}
void _showBottomSheet() {
setState(() {
// disable the button
setState(() { // disable the button
_showBottomSheetCallback = null;
});
_scaffoldKey.currentState
.showBottomSheet<Null>((BuildContext context) {
final ThemeData themeData = Theme.of(context);
return Container(
decoration: BoxDecoration(
border:
Border(top: BorderSide(color: themeData.disabledColor))),
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Text(
'This is a Material persistent bottom sheet. Drag downwards to dismiss it.',
textAlign: TextAlign.center,
style: TextStyle(color: themeData.accentColor, fontSize: 24.0),
),
_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;
});
}
),
),
);
})
.closed.whenComplete(() {
if (mounted) {
setState(() { // re-enable the button
_showBottomSheetCallback = _showBottomSheet;
});
}
});
}
void _showMessage() {
@@ -66,10 +63,11 @@ class _PersistentBottomSheetDemoState extends State<PersistentBottomSheetDemo> {
content: const Text('You tapped the floating action button.'),
actions: <Widget>[
FlatButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('OK'))
onPressed: () {
Navigator.pop(context);
},
child: const Text('OK'),
),
],
);
},
@@ -79,25 +77,27 @@ class _PersistentBottomSheetDemoState extends State<PersistentBottomSheetDemo> {
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: const Text('Persistent bottom sheet'),
actions: <Widget>[
MaterialDemoDocumentationButton(
PersistentBottomSheetDemo.routeName),
],
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',
),
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'),
),
body: Center(
child: RaisedButton(
onPressed: _showBottomSheetCallback,
child: const Text('SHOW BOTTOM SHEET'))));
),
);
}
}

View File

@@ -1,8 +1,8 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
@@ -13,8 +13,7 @@ class ProgressIndicatorDemo extends StatefulWidget {
_ProgressIndicatorDemoState createState() => _ProgressIndicatorDemoState();
}
class _ProgressIndicatorDemoState extends State<ProgressIndicatorDemo>
with SingleTickerProviderStateMixin {
class _ProgressIndicatorDemoState extends State<ProgressIndicatorDemo> with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> _animation;
@@ -28,14 +27,15 @@ class _ProgressIndicatorDemoState extends State<ProgressIndicatorDemo>
)..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();
});
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
@@ -66,7 +66,10 @@ class _ProgressIndicatorDemoState extends State<ProgressIndicatorDemo>
Widget _buildIndicators(BuildContext context, Widget child) {
final List<Widget> indicators = <Widget>[
const SizedBox(width: 200.0, child: LinearProgressIndicator()),
const SizedBox(
width: 200.0,
child: LinearProgressIndicator(),
),
const LinearProgressIndicator(),
const LinearProgressIndicator(),
LinearProgressIndicator(value: _animation.value),
@@ -77,23 +80,22 @@ class _ProgressIndicatorDemoState extends State<ProgressIndicatorDemo>
SizedBox(
width: 20.0,
height: 20.0,
child: CircularProgressIndicator(value: _animation.value)),
child: CircularProgressIndicator(value: _animation.value),
),
SizedBox(
width: 100.0,
height: 20.0,
child: Text('${(_animation.value * 100.0).toStringAsFixed(1)}%',
textAlign: TextAlign.right),
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(),
.map<Widget>((Widget c) => Container(child: c, margin: const EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0)))
.toList(),
);
}
@@ -102,9 +104,7 @@ class _ProgressIndicatorDemoState extends State<ProgressIndicatorDemo>
return Scaffold(
appBar: AppBar(
title: const Text('Progress indicators'),
actions: <Widget>[
MaterialDemoDocumentationButton(ProgressIndicatorDemo.routeName)
],
actions: <Widget>[MaterialDemoDocumentationButton(ProgressIndicatorDemo.routeName)],
),
body: Center(
child: SingleChildScrollView(
@@ -117,10 +117,11 @@ class _ProgressIndicatorDemoState extends State<ProgressIndicatorDemo>
top: false,
bottom: false,
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 12.0, horizontal: 8.0),
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 8.0),
child: AnimatedBuilder(
animation: _animation, builder: _buildIndicators),
animation: _animation,
builder: _buildIndicators,
),
),
),
),

View File

@@ -2,9 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/foundation.dart';
import 'package:flutter_web/material.dart';
import 'package:flutter_web/rendering.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import '../../gallery/demo.dart';
@@ -20,7 +20,7 @@ enum _ReorderableListType {
}
class ReorderableListDemo extends StatefulWidget {
const ReorderableListDemo({Key key}) : super(key: key);
const ReorderableListDemo({ Key key }) : super(key: key);
static const String routeName = '/material/reorderable-list';
@@ -37,27 +37,14 @@ class _ListItem {
}
class _ListDemoState extends State<ReorderableListDemo> {
static final GlobalKey<ScaffoldState> scaffoldKey =
GlobalKey<ScaffoldState>();
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',
'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) {
@@ -65,15 +52,28 @@ class _ListDemoState extends State<ReorderableListDemo> {
_itemType = type;
});
// Rebuild the bottom sheet to reflect the selected list view.
_bottomSheet?.setState(() {});
_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) {
_bottomSheet = scaffoldKey.currentState.showBottomSheet<void>((BuildContext bottomSheetContext) {
return DecoratedBox(
decoration: const BoxDecoration(
border: Border(top: BorderSide(color: Colors.black26)),
@@ -82,6 +82,12 @@ class _ListDemoState extends State<ReorderableListDemo> {
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'),
@@ -146,8 +152,7 @@ class _ListDemoState extends State<ReorderableListDemo> {
key: Key(item.value),
height: 100.0,
width: 100.0,
child: CircleAvatar(
child: Text(item.value),
child: CircleAvatar(child: Text(item.value),
backgroundColor: Colors.green,
),
);
@@ -167,6 +172,7 @@ class _ListDemoState extends State<ReorderableListDemo> {
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -181,9 +187,7 @@ class _ListDemoState extends State<ReorderableListDemo> {
onPressed: () {
setState(() {
_reverseSort = !_reverseSort;
_items.sort((_ListItem a, _ListItem b) => _reverseSort
? b.value.compareTo(a.value)
: a.value.compareTo(b.value));
_items.sort((_ListItem a, _ListItem b) => _reverseSort ? b.value.compareTo(a.value) : a.value.compareTo(b.value));
});
},
),
@@ -203,13 +207,11 @@ class _ListDemoState extends State<ReorderableListDemo> {
header: _itemType != _ReorderableListType.threeLine
? Padding(
padding: const EdgeInsets.all(8.0),
child: Text('Header of the list',
style: Theme.of(context).textTheme.headline))
child: Text('Header of the list', style: Theme.of(context).textTheme.headline))
: null,
onReorder: _onReorder,
scrollDirection: _itemType == _ReorderableListType.horizontalAvatar
? Axis.horizontal
: Axis.vertical,
reverse: _reverse,
scrollDirection: _itemType == _ReorderableListType.horizontalAvatar ? Axis.horizontal : Axis.vertical,
padding: const EdgeInsets.symmetric(vertical: 8.0),
children: _items.map<Widget>(buildListTile).toList(),
),

View File

@@ -1,15 +1,19 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
enum TabsDemoStyle { iconsAndText, iconsOnly, textOnly }
enum TabsDemoStyle {
iconsAndText,
iconsOnly,
textOnly
}
class _Page {
const _Page({this.icon, this.text});
const _Page({ this.icon, this.text });
final IconData icon;
final String text;
}
@@ -38,8 +42,7 @@ class ScrollableTabsDemo extends StatefulWidget {
ScrollableTabsDemoState createState() => ScrollableTabsDemoState();
}
class ScrollableTabsDemoState extends State<ScrollableTabsDemo>
with SingleTickerProviderStateMixin {
class ScrollableTabsDemoState extends State<ScrollableTabsDemo> with SingleTickerProviderStateMixin {
TabController _controller;
TabsDemoStyle _demoStyle = TabsDemoStyle.iconsAndText;
bool _customIndicator = false;
@@ -63,57 +66,55 @@ class ScrollableTabsDemoState extends State<ScrollableTabsDemo>
}
Decoration getIndicator() {
if (!_customIndicator) return const UnderlineTabIndicator();
if (!_customIndicator)
return const UnderlineTabIndicator();
switch (_demoStyle) {
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,
),
),
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,
),
),
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,
),
),
side: BorderSide(
color: Colors.white24,
width: 2.0,
),
) + const StadiumBorder(
side: BorderSide(
color: Colors.transparent,
width: 4.0,
),
),
);
}
return null;
@@ -137,15 +138,19 @@ class ScrollableTabsDemoState extends State<ScrollableTabsDemo>
),
PopupMenuButton<TabsDemoStyle>(
onSelected: changeDemoStyle,
itemBuilder: (BuildContext context) =>
<PopupMenuItem<TabsDemoStyle>>[
itemBuilder: (BuildContext context) => <PopupMenuItem<TabsDemoStyle>>[
const PopupMenuItem<TabsDemoStyle>(
value: TabsDemoStyle.iconsAndText,
child: Text('Icons and text')),
value: TabsDemoStyle.iconsAndText,
child: Text('Icons and text'),
),
const PopupMenuItem<TabsDemoStyle>(
value: TabsDemoStyle.iconsOnly, child: Text('Icons only')),
value: TabsDemoStyle.iconsOnly,
child: Text('Icons only'),
),
const PopupMenuItem<TabsDemoStyle>(
value: TabsDemoStyle.textOnly, child: Text('Text only')),
value: TabsDemoStyle.textOnly,
child: Text('Text only'),
),
],
),
],
@@ -168,27 +173,28 @@ class ScrollableTabsDemoState extends State<ScrollableTabsDemo>
),
),
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',
),
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()),
),
);
}).toList(),
),
);
}
}

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_web/material.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
@@ -60,7 +60,7 @@ class _SearchDemoState extends State<SearchDemo> {
? Icons.more_horiz
: Icons.more_vert,
),
onPressed: () {},
onPressed: () { },
),
],
),
@@ -86,13 +86,12 @@ class _SearchDemoState extends State<SearchDemo> {
Text(' icon in the AppBar'),
],
),
const Text(
'and search for an integer between 0 and 100,000.'),
const Text('and search for an integer between 0 and 100,000.'),
],
),
),
const SizedBox(height: 64.0),
Text('Last selected integer: ${_lastIntegerSelected ?? 'NONE'}.')
Text('Last selected integer: ${_lastIntegerSelected ?? 'NONE' }.'),
],
),
),
@@ -113,6 +112,7 @@ class _SearchDemoState extends State<SearchDemo> {
currentAccountPicture: CircleAvatar(
backgroundImage: AssetImage(
'people/square/peter.png',
package: 'flutter_gallery_assets',
),
),
margin: EdgeInsets.zero,
@@ -134,8 +134,7 @@ class _SearchDemoState extends State<SearchDemo> {
}
class _SearchDemoSearchDelegate extends SearchDelegate<int> {
final List<int> _data =
List<int>.generate(100001, (int i) => i).reversed.toList();
final List<int> _data = List<int>.generate(100001, (int i) => i).reversed.toList();
final List<int> _history = <int>[42607, 85604, 66374, 44, 174];
@override
@@ -154,6 +153,7 @@ class _SearchDemoSearchDelegate extends SearchDelegate<int> {
@override
Widget buildSuggestions(BuildContext context) {
final Iterable<int> suggestions = query.isEmpty
? _history
: _data.where((int i) => '$i'.startsWith(query));
@@ -219,7 +219,7 @@ class _SearchDemoSearchDelegate extends SearchDelegate<int> {
query = '';
showSuggestions(context);
},
)
),
];
}
}
@@ -275,8 +275,7 @@ class _SuggestionList extends StatelessWidget {
title: RichText(
text: TextSpan(
text: suggestion.substring(0, query.length),
style:
theme.textTheme.subhead.copyWith(fontWeight: FontWeight.bold),
style: theme.textTheme.subhead.copyWith(fontWeight: FontWeight.bold),
children: <TextSpan>[
TextSpan(
text: suggestion.substring(query.length),

View File

@@ -1,24 +1,77 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
class SelectionControlsDemo extends StatefulWidget {
static const String routeName = '/material/selection';
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> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
@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(() {
@@ -26,23 +79,12 @@ class _SelectionControlsDemoState extends State<SelectionControlsDemo> {
});
}
@override
Widget build(BuildContext context) {
return wrapScaffold('Selection Controls', context, _scaffoldKey,
_buildContents(), SelectionControlsDemo.routeName);
}
Widget _buildContents() {
return Material(
color: Colors.white,
child: new Column(
children: <Widget>[buildCheckbox(), Divider(), buildRadio()]));
}
Widget buildCheckbox() {
return Align(
alignment: const Alignment(0.0, -0.2),
child: Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
alignment: const Alignment(0.0, -0.2),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
@@ -73,39 +115,91 @@ class _SelectionControlsDemoState extends State<SelectionControlsDemo> {
),
],
),
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),
])
]));
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>(
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>(
onChanged: handleRadioValueChanged,
),
Radio<int>(
value: 1,
groupValue: radioValue,
onChanged: handleRadioValueChanged),
Radio<int>(
onChanged: handleRadioValueChanged,
),
Radio<int>(
value: 2,
groupValue: radioValue,
onChanged: handleRadioValueChanged)
]),
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)
])
]));
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),
],
),
);
}
}

View File

@@ -1,10 +1,10 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
@@ -15,30 +15,102 @@ class SliderDemo extends StatefulWidget {
_SliderDemoState createState() => _SliderDemoState();
}
Path _triangle(double size, Offset thumbCenter, {bool invert = false}) {
Path _downTriangle(double size, Offset thumbCenter, { bool invert = false }) {
final Path thumbPath = Path();
final double height = math.sqrt(3.0) / 2.0;
final double halfSide = size / 2.0;
final double centerHeight = size * height / 3.0;
final double halfSize = size / 2.0;
final double sign = invert ? -1.0 : 1.0;
thumbPath.moveTo(
thumbCenter.dx - halfSide, thumbCenter.dy + sign * centerHeight);
thumbPath.moveTo(thumbCenter.dx - halfSize, thumbCenter.dy + sign * centerHeight);
thumbPath.lineTo(thumbCenter.dx, thumbCenter.dy - 2.0 * sign * centerHeight);
thumbPath.lineTo(
thumbCenter.dx + halfSide, thumbCenter.dy + 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);
return isEnabled ? const Size.fromRadius(_thumbSize) : const Size.fromRadius(_disabledThumbSize);
}
static final Animatable<double> sizeTween = Tween<double>(
@@ -65,9 +137,8 @@ class _CustomThumbShape extends SliderComponentShape {
end: sliderTheme.thumbColor,
);
final double size = _thumbSize * sizeTween.evaluate(enableAnimation);
final Path thumbPath = _triangle(size, thumbCenter);
canvas.drawPath(
thumbPath, Paint()..color = colorTween.evaluate(enableAnimation));
final Path thumbPath = _downTriangle(size, thumbCenter);
canvas.drawPath(thumbPath, Paint()..color = colorTween.evaluate(enableAnimation));
}
}
@@ -109,16 +180,9 @@ class _CustomValueIndicatorShape extends SliderComponentShape {
end: _slideUpHeight,
);
final double size = _indicatorSize * sizeTween.evaluate(enableAnimation);
final Offset slideUpOffset =
Offset(0.0, -slideUpTween.evaluate(activationAnimation));
final Path thumbPath = _triangle(
size,
thumbCenter + slideUpOffset,
invert: true,
);
final Color paintColor = enableColor
.evaluate(enableAnimation)
.withAlpha((255.0 * activationAnimation.value).round());
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,
@@ -130,27 +194,49 @@ class _CustomValueIndicatorShape extends SliderComponentShape {
..color = paintColor
..style = PaintingStyle.stroke
..strokeWidth = 2.0);
labelPainter.paint(
canvas,
thumbCenter +
slideUpOffset +
Offset(-labelPainter.width / 2.0, -labelPainter.height - 4.0));
labelPainter.paint(canvas, thumbCenter + slideUpOffset + Offset(-labelPainter.width / 2.0, -labelPainter.height - 4.0));
}
}
class _SliderDemoState extends State<SliderDemo> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
@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',
),
];
double _value = 25.0;
double _discreteValue = 40.0;
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) {
return wrapScaffold('Slider Demo', context, _scaffoldKey,
_buildContents(context), SliderDemo.routeName);
}
Widget _buildContents(BuildContext context) {
final ThemeData theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 40.0),
@@ -160,30 +246,52 @@ class _SliderDemoState extends State<SliderDemo> {
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Slider(
value: _value,
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(() {
_value = value;
_continuousValue = value;
});
},
),
const Text('Continuous'),
const Text('Continuous with Editable Numerical Value'),
],
),
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Slider(value: 0.25, onChanged: (double val) {}),
children: const <Widget>[
Slider.adaptive(value: 0.25, onChanged: null),
Text('Disabled'),
],
),
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Slider(
Slider.adaptive(
value: _discreteValue,
min: 0.0,
max: 200.0,
@@ -204,28 +312,26 @@ class _SliderDemoState extends State<SliderDemo> {
SliderTheme(
data: theme.sliderTheme.copyWith(
activeTrackColor: Colors.deepPurple,
inactiveTrackColor: Colors.black26,
activeTickMarkColor: Colors.white70,
inactiveTickMarkColor: Colors.black,
overlayColor: Colors.black12,
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: Colors.black87),
valueIndicatorTextStyle: theme.accentTextTheme.body2.copyWith(color: theme.colorScheme.onSurface),
),
child: Slider(
value: _discreteValue,
value: _discreteCustomValue,
min: 0.0,
max: 200.0,
divisions: 5,
semanticFormatterCallback: (double value) =>
value.round().toString(),
label: '${_discreteValue.round()}',
semanticFormatterCallback: (double value) => value.round().toString(),
label: '${_discreteCustomValue.round()}',
onChanged: (double value) {
setState(() {
_discreteValue = value;
_discreteCustomValue = value;
});
},
),
@@ -238,3 +344,98 @@ class _SliderDemoState extends State<SliderDemo> {
);
}
}
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'),
],
),
],
),
);
}
}

View File

@@ -1,24 +1,25 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
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.';
'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.';
'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 ';
'By default snackbars automatically disappear after a few seconds ';
class SnackBarDemo extends StatefulWidget {
const SnackBarDemo({Key key}) : super(key: key);
const SnackBarDemo({ Key key }) : super(key: key);
static const String routeName = '/material/snack-bar';
@@ -34,50 +35,61 @@ class _SnackBarDemoState extends State<SnackBarDemo> {
top: false,
bottom: false,
child: ListView(
padding: const EdgeInsets.all(24.0),
children: <Widget>[
const Text(_text1),
const Text(_text2),
Center(
child: Row(children: <Widget>[
RaisedButton(
child: const Text('SHOW A SNACKBAR'),
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: () {
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.')));
}),
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()),
),
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));
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');
}
),
);
}
}

View File

@@ -1,23 +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_web/material.dart';
class StackDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
border: Border.all(
color: Colors.greenAccent,
width: 1.0,
),
),
child: Stack(children: [
Text('A'),
Text('B'),
]),
);
}
}

View File

@@ -1,42 +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_web/material.dart';
import '../../gallery/demo.dart';
class SwitchDemo extends StatefulWidget {
static const routeName = '/material/switch';
@override
SwitchDemoState createState() => SwitchDemoState();
}
class SwitchDemoState extends State<SwitchDemo> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
@override
Widget build(BuildContext context) {
return wrapScaffold('Switch Demo', context, _scaffoldKey, _buildContents(),
SwitchDemo.routeName);
}
bool _value = true;
Widget _buildContents() {
return Material(
child: Column(
children: [
Switch(
value: _value,
onChanged: (bool newValue) {
setState(() {
_value = newValue;
});
}),
],
),
);
}
}

View File

@@ -1,18 +1,18 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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.
import 'package:flutter_web/material.dart';
import '../../gallery/demo.dart';
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
class _Page {
_Page({this.label});
_Page({ this.label });
final String label;
String get id => label[0];
@override
@@ -20,7 +20,7 @@ class _Page {
}
class _CardData {
const _CardData({this.title, this.imageAsset, this.imageAssetPackage});
const _CardData({ this.title, this.imageAsset, this.imageAssetPackage });
final String title;
final String imageAsset;
final String imageAssetPackage;
@@ -94,7 +94,7 @@ final Map<_Page, List<_CardData>> _allPages = <_Page, List<_CardData>>{
};
class _CardDataItem extends StatelessWidget {
const _CardDataItem({this.page, this.data});
const _CardDataItem({ this.page, this.data });
static const double height = 272.0;
final _Page page;
@@ -110,17 +110,20 @@ class _CardDataItem extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Align(
alignment:
page.id == 'H' ? Alignment.centerLeft : Alignment.centerRight,
alignment: page.id == 'H'
? Alignment.centerLeft
: Alignment.centerRight,
child: CircleAvatar(child: Text('${page.id}')),
),
SizedBox(width: 144.0, height: 144.0, child: new Text('image')
// Image.asset(
// data.imageAsset,
// package: data.imageAssetPackage,
// fit: BoxFit.contain,
// ),
),
SizedBox(
width: 144.0,
height: 144.0,
child: Image.asset(
data.imageAsset,
package: data.imageAssetPackage,
fit: BoxFit.contain,
),
),
Center(
child: Text(
data.title,
@@ -145,18 +148,19 @@ class TabsDemo extends StatelessWidget {
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
title: const Text('Tabs and scrolling'),
actions: <Widget>[MaterialDemoDocumentationButton(routeName)],
pinned: true,
expandedHeight: 150.0,
forceElevated: innerBoxIsScrolled,
bottom: TabBar(
tabs: _allPages.keys
.map<Widget>(
(_Page page) => Tab(text: page.label),
)
.toList(),
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(),
),
),
),
];
@@ -171,6 +175,9 @@ class TabsDemo extends StatelessWidget {
return CustomScrollView(
key: PageStorageKey<_Page>(page),
slivers: <Widget>[
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverPadding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,

View File

@@ -1,26 +1,25 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
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.';
"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});
_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;
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);
@@ -42,8 +41,7 @@ class TabsFabDemo extends StatefulWidget {
_TabsFabDemoState createState() => _TabsFabDemoState();
}
class _TabsFabDemoState extends State<TabsFabDemo>
with SingleTickerProviderStateMixin {
class _TabsFabDemoState extends State<TabsFabDemo> with SingleTickerProviderStateMixin {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
TabController _controller;
@@ -71,50 +69,63 @@ class _TabsFabDemoState extends State<TabsFabDemo>
}
void _showExplanatoryText() {
_scaffoldKey.currentState.showBottomSheet<Null>((BuildContext context) {
_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)));
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(
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))));
});
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 (!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);
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);
key: page.fabKey,
tooltip: 'Show explanation',
backgroundColor: page.fabColor,
child: page.fabIcon,
onPressed: _showExplanatoryText,
);
}
@override
@@ -125,14 +136,12 @@ class _TabsFabDemoState extends State<TabsFabDemo>
title: const Text('FAB per tab'),
bottom: TabBar(
controller: _controller,
tabs: _allPages
.map<Widget>((_Page page) => Tab(text: page.label.toUpperCase()))
.toList(),
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),
icon: const Icon(Icons.sentiment_very_satisfied, semanticLabel: 'Toggle extended buttons'),
onPressed: () {
setState(() {
_extendedButtons = !_extendedButtons;
@@ -143,8 +152,9 @@ class _TabsFabDemoState extends State<TabsFabDemo>
),
floatingActionButton: buildFloatingActionButton(_selectedPage),
body: TabBarView(
controller: _controller,
children: _allPages.map<Widget>(buildTabView).toList()),
controller: _controller,
children: _allPages.map<Widget>(buildTabView).toList(),
),
);
}
}

View File

@@ -1,52 +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_web/material.dart';
class TextDemo extends StatelessWidget {
static const routeName = '/material/text';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Text'),
centerTitle: true,
),
body: ListView(
children: [
pad(Text('Single line of text')),
Divider(),
// Single line with many whitespaces in between.
pad(Text(' Text with a lot of whitespace ')),
Divider(),
// Forced multi-line because of the \n.
pad(Text('Text with a newline\ncharacter should render in 2 lines')),
Divider(),
// Multi-line with regular whitespace.
pad(Text(
'''Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas auctor
vel ligula eget fermentum. Integer mattis nulla vitae ullamcorper
dignissim. Donec vel velit vel eros lobortis laoreet at sit amet turpis.
Ut in orci blandit, rhoncus metus quis, finibus augue. Nullam a elit
venenatis metus accumsan dapibus. Vestibulum imperdiet tristique viverra.''',
)),
Divider(),
// Multi-line with a lot of whitespace in between.
pad(Text(
'''
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Maecenas auctor vel ligula eget fermentum.
Integer mattis nulla vitae ullamcorper dignissim.
Donec vel velit vel eros lobortis laoreet at sit amet turpis.''',
)),
Divider(),
],
),
);
}
Padding pad(Widget child) =>
Padding(padding: EdgeInsets.all(12), child: child);
}

View File

@@ -1,16 +1,17 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter_web/services.dart';
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);
const TextFormFieldDemo({ Key key }) : super(key: key);
static const String routeName = '/material/text-form-field';
@@ -67,6 +68,7 @@ class _PasswordFieldState extends State<PasswordField> {
labelText: widget.labelText,
helperText: widget.helperText,
suffixIcon: GestureDetector(
dragStartBehavior: DragStartBehavior.down,
onTap: () {
setState(() {
_obscureText = !_obscureText;
@@ -88,17 +90,17 @@ class TextFormFieldDemoState extends State<TextFormFieldDemo> {
PersonData person = PersonData();
void showInSnackBar(String value) {
_scaffoldKey.currentState.showSnackBar(SnackBar(content: Text(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();
final GlobalKey<FormFieldState<String>> _passwordFieldKey = GlobalKey<FormFieldState<String>>();
final _UsNumberTextInputFormatter _phoneNumberFormatter = _UsNumberTextInputFormatter();
void _handleSubmitted() {
final FormState form = _formKey.currentState;
if (!form.validate()) {
@@ -112,7 +114,8 @@ class TextFormFieldDemoState extends State<TextFormFieldDemo> {
String _validateName(String value) {
_formWasEdited = true;
if (value.isEmpty) return 'Name is required.';
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.';
@@ -132,49 +135,45 @@ class TextFormFieldDemoState extends State<TextFormFieldDemo> {
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';
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;
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;
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)
],
actions: <Widget>[MaterialDemoDocumentationButton(TextFormFieldDemo.routeName)],
),
body: SafeArea(
top: false,
@@ -183,120 +182,118 @@ class TextFormFieldDemoState extends State<TextFormFieldDemo> {
key: _formKey,
autovalidate: _autovalidate,
onWillPop: _warnUserAboutInvalidData,
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
const SizedBox(height: 24.0),
TextFormField(
textCapitalization: TextCapitalization.words,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
filled: true,
icon: Icon(Icons.person),
hintText: 'What do people call you?',
labelText: 'Name * ',
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,
),
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',
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,
],
),
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',
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; },
),
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',
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,
),
maxLines: 3,
),
const SizedBox(height: 24.0),
TextFormField(
keyboardType: TextInputType.number,
decoration: const InputDecoration(
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',
suffixStyle: TextStyle(color: Colors.green),
),
maxLines: 1,
),
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),
PasswordField(
fieldKey: _passwordFieldKey,
helperText: 'No more than 8 characters.',
labelText: 'Password *',
onFieldSubmitted: (String value) {
setState(() {
person.password = value;
});
},
),
),
const SizedBox(height: 24.0),
Text('* indicates required field',
style: Theme.of(context).textTheme.caption),
const SizedBox(height: 24.0),
],
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),
],
),
),
),
),
@@ -309,26 +306,32 @@ class TextFormFieldDemoState extends State<TextFormFieldDemo> {
class _UsNumberTextInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue, TextEditingValue newValue) {
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 (newValue.selection.end >= 1)
selectionIndex++;
}
if (newTextLength >= 4) {
newText.write(newValue.text.substring(0, usedSubstringIndex = 3) + ') ');
if (newValue.selection.end >= 3) selectionIndex += 2;
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 (newValue.selection.end >= 6)
selectionIndex++;
}
if (newTextLength >= 11) {
newText.write(newValue.text.substring(6, usedSubstringIndex = 10) + ' ');
if (newValue.selection.end >= 10) selectionIndex++;
if (newValue.selection.end >= 10)
selectionIndex++;
}
// Dump the rest.
if (newTextLength >= usedSubstringIndex)

View File

@@ -1,59 +1,75 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
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.';
'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) {
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(
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()),
iconSize: 48.0,
icon: const Icon(Icons.call),
color: theme.iconTheme.color,
tooltip: 'Place a phone call',
onPressed: () {
Scaffold.of(context).showSnackBar(const SnackBar(
content: Text('That was an ordinary tap.'),
));
},
),
),
]
.map<Widget>((Widget widget) {
return Padding(
padding: const EdgeInsets.only(top: 16.0, left: 16.0, right: 16.0),
child: widget,
);
})
.toList(),
),
);
}));
}
),
);
}
}

View File

@@ -1,34 +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_web/material.dart';
import '../../gallery/demo.dart';
class TwoLevelListDemo extends StatelessWidget {
static const String routeName = '/material/two-level-list';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Expand/collapse list control'),
actions: <Widget>[MaterialDemoDocumentationButton(routeName)],
),
body: ListView(children: <Widget>[
const ListTile(title: Text('Top')),
ExpansionTile(
title: const Text('Sublist'),
backgroundColor: Theme.of(context).accentColor.withOpacity(0.025),
children: const <Widget>[
ListTile(title: Text('One')),
ListTile(title: Text('Two')),
// https://en.wikipedia.org/wiki/Free_Four
ListTile(title: Text('Free')),
ListTile(title: Text('Four'))
]),
const ListTile(title: Text('Bottom'))
]));
}
}

View File

@@ -1,12 +1,12 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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_web/material.dart';
import 'package:flutter_web/rendering.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class PestoDemo extends StatelessWidget {
const PestoDemo({Key key}) : super(key: key);
const PestoDemo({ Key key }) : super(key: key);
static const String routeName = '/pesto';
@@ -14,13 +14,14 @@ class PestoDemo extends StatelessWidget {
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 _kFabHalfSize = 28.0; // TODO(mpcomplete): needs to adapt to screen size
const double _kRecipePageMaxWidth = 500.0;
final Set<Recipe> _favoriteRecipes = Set<Recipe>();
final Set<Recipe> _favoriteRecipes = <Recipe>{};
final ThemeData _kTheme = ThemeData(
brightness: Brightness.light,
@@ -50,20 +51,20 @@ class PestoStyle extends TextStyle {
double letterSpacing,
double height,
}) : super(
inherit: false,
color: color,
fontFamily: 'Raleway',
fontSize: fontSize,
fontWeight: fontWeight,
textBaseline: TextBaseline.alphabetic,
letterSpacing: letterSpacing,
height: height,
);
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);
const RecipeGridPage({ Key key, this.recipes }) : super(key: key);
final List<Recipe> recipes;
@@ -119,10 +120,8 @@ class _RecipeGridPageState extends State<RecipeGridPage> {
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 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(
@@ -130,7 +129,8 @@ class _RecipeGridPageState extends State<RecipeGridPage> {
bottom: extraPadding,
),
child: Center(
child: PestoLogo(height: logoHeight, t: t.clamp(0.0, 1.0))),
child: PestoLogo(height: logoHeight, t: t.clamp(0.0, 1.0)),
),
);
},
),
@@ -140,10 +140,11 @@ class _RecipeGridPageState extends State<RecipeGridPage> {
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);
top: 8.0,
left: 8.0 + mediaPadding.left,
right: 8.0 + mediaPadding.right,
bottom: 8.0,
);
return SliverPadding(
padding: padding,
sliver: SliverGrid(
@@ -157,9 +158,7 @@ class _RecipeGridPageState extends State<RecipeGridPage> {
final Recipe recipe = widget.recipes[index];
return RecipeCard(
recipe: recipe,
onTap: () {
showRecipePage(context, recipe);
},
onTap: () { showRecipePage(context, recipe); },
);
},
childCount: widget.recipes.length,
@@ -169,26 +168,22 @@ class _RecipeGridPageState extends State<RecipeGridPage> {
}
void showFavoritesPage(BuildContext context) {
Navigator.push(
context,
MaterialPageRoute<void>(
settings: const RouteSettings(name: '/pesto/favorites'),
builder: (BuildContext context) => PestoFavorites(),
));
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),
);
},
));
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),
);
},
));
}
}
@@ -208,18 +203,15 @@ class _PestoLogoState extends State<PestoLogo> {
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 TextStyle titleStyle = const PestoStyle(fontSize: kTextHeight, fontWeight: FontWeight.w900, color: Colors.white, letterSpacing: 3.0);
final RectTween _textRectTween = RectTween(
begin: Rect.fromLTWH(0.0, kLogoHeight, kLogoWidth, kTextHeight),
end: Rect.fromLTWH(0.0, kImageHeight, kLogoWidth, kTextHeight));
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: Rect.fromLTWH(0.0, 0.0, kLogoWidth, kLogoHeight),
end: Rect.fromLTWH(0.0, 0.0, kLogoWidth, kImageHeight),
begin: const Rect.fromLTWH(0.0, 0.0, kLogoWidth, kLogoHeight),
end: const Rect.fromLTWH(0.0, 0.0, kLogoWidth, kImageHeight),
);
@override
@@ -237,7 +229,8 @@ class _PestoLogoState extends State<PestoLogo> {
Positioned.fromRect(
rect: _imageRectTween.lerp(widget.t),
child: Image.asset(
'$_kSmallLogoImage',
_kSmallLogoImage,
package: _kGalleryAssetsPackage,
fit: BoxFit.contain,
),
),
@@ -245,8 +238,7 @@ class _PestoLogoState extends State<PestoLogo> {
rect: _textRectTween.lerp(widget.t),
child: Opacity(
opacity: _textOpacity.transform(widget.t),
child: Text('PESTO',
style: titleStyle, textAlign: TextAlign.center),
child: Text('PESTO', style: titleStyle, textAlign: TextAlign.center),
),
),
],
@@ -259,15 +251,13 @@ class _PestoLogoState extends State<PestoLogo> {
// 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);
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);
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) {
@@ -278,11 +268,11 @@ class RecipeCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Hero(
tag: '${recipe.imagePath}',
tag: 'packages/$_kGalleryAssetsPackage/${recipe.imagePath}',
child: AspectRatio(
aspectRatio: 4.0 / 3.0,
child: Image.asset(
'${recipe.imagePath}',
recipe.imagePath,
package: recipe.imagePackage,
fit: BoxFit.cover,
semanticLabel: recipe.name,
@@ -295,7 +285,7 @@ class RecipeCard extends StatelessWidget {
Padding(
padding: const EdgeInsets.all(16.0),
child: Image.asset(
'${recipe.ingredientsImagePath}',
recipe.ingredientsImagePath,
package: recipe.ingredientsImagePackage,
width: 48.0,
height: 48.0,
@@ -306,10 +296,7 @@ class RecipeCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(recipe.name,
style: titleStyle,
softWrap: false,
overflow: TextOverflow.ellipsis),
Text(recipe.name, style: titleStyle, softWrap: false, overflow: TextOverflow.ellipsis),
Text(recipe.author, style: authorStyle),
],
),
@@ -326,7 +313,7 @@ class RecipeCard extends StatelessWidget {
// Displays one recipe. Includes the recipe sheet with a background image.
class RecipePage extends StatefulWidget {
const RecipePage({Key key, this.recipe}) : super(key: key);
const RecipePage({ Key key, this.recipe }) : super(key: key);
final Recipe recipe;
@@ -336,11 +323,9 @@ class RecipePage extends StatefulWidget {
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);
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;
double _getAppBarHeight(BuildContext context) => MediaQuery.of(context).size.height * 0.3;
@override
Widget build(BuildContext context) {
@@ -361,9 +346,9 @@ class _RecipePageState extends State<RecipePage> {
right: 0.0,
height: appBarHeight + _kFabHalfSize,
child: Hero(
tag: '${widget.recipe.imagePath}',
tag: 'packages/$_kGalleryAssetsPackage/${widget.recipe.imagePath}',
child: Image.asset(
'${widget.recipe.imagePath}',
widget.recipe.imagePath,
package: widget.recipe.imagePackage,
fit: fullWidth ? BoxFit.fitWidth : BoxFit.cover,
),
@@ -376,9 +361,8 @@ class _RecipePageState extends State<RecipePage> {
backgroundColor: Colors.transparent,
actions: <Widget>[
PopupMenuButton<String>(
onSelected: (String item) {},
itemBuilder: (BuildContext context) =>
<PopupMenuItem<String>>[
onSelected: (String item) { },
itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
_buildMenuItem(Icons.share, 'Tweet recipe'),
_buildMenuItem(Icons.email, 'Email recipe'),
_buildMenuItem(Icons.message, 'Message recipe'),
@@ -399,23 +383,23 @@ class _RecipePageState extends State<RecipePage> {
),
),
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,
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,
),
),
],
),
),
],
),
],
@@ -428,8 +412,9 @@ class _RecipePageState extends State<RecipePage> {
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(right: 24.0),
child: Icon(icon, color: Colors.black54)),
padding: const EdgeInsets.only(right: 24.0),
child: Icon(icon, color: Colors.black54),
),
Text(label, style: menuItemStyle),
],
),
@@ -448,17 +433,13 @@ class _RecipePageState extends State<RecipePage> {
/// Displays the recipe's name and instructions.
class RecipeSheet extends StatelessWidget {
RecipeSheet({Key key, this.recipe}) : super(key: key);
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 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;
@@ -472,48 +453,62 @@ class RecipeSheet extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 40.0),
child: Table(
columnWidths: const <int, TableColumnWidth>{
0: FixedColumnWidth(64.0)
0: FixedColumnWidth(64.0),
},
children: <TableRow>[
TableRow(children: <Widget>[
TableCell(
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(
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(
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(
child: Text(recipe.description, style: descriptionStyle),
),
]
),
TableRow(
children: <Widget>[
const SizedBox(),
Padding(
padding: const EdgeInsets.only(top: 24.0, bottom: 4.0),
child: Text('Ingredients', style: headingStyle)),
]),
]
..addAll(recipe.ingredients
.map<TableRow>((RecipeIngredient ingredient) {
child: Text('Ingredients', style: headingStyle),
),
]
),
...recipe.ingredients.map<TableRow>((RecipeIngredient ingredient) {
return _buildItemRow(ingredient.amount, ingredient.description);
}))
..add(TableRow(children: <Widget>[
const SizedBox(),
Padding(
}),
TableRow(
children: <Widget>[
const SizedBox(),
Padding(
padding: const EdgeInsets.only(top: 24.0, bottom: 4.0),
child: Text('Steps', style: headingStyle)),
]))
..addAll(recipe.steps.map<TableRow>((RecipeStep step) {
child: Text('Steps', style: headingStyle),
),
]
),
...recipe.steps.map<TableRow>((RecipeStep step) {
return _buildItemRow(step.duration ?? '', step.description);
})),
}),
],
),
),
),
@@ -537,16 +532,17 @@ class RecipeSheet extends StatelessWidget {
}
class Recipe {
const Recipe(
{this.name,
this.author,
this.description,
this.imagePath,
this.imagePackage,
this.ingredientsImagePath,
this.ingredientsImagePackage,
this.ingredients,
this.steps});
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;
@@ -578,9 +574,10 @@ const List<Recipe> kPestoRecipes = <Recipe>[
name: 'Roasted Chicken',
author: 'Peter Carlsson',
ingredientsImagePath: 'food/icons/main.png',
description:
'The perfect dish to welcome your family and friends with on a crisp autumn night. Pair with roasted veggies to truly impress them.',
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'),
@@ -597,11 +594,12 @@ const List<Recipe> kPestoRecipes = <Recipe>[
name: 'Chopped Beet Leaves',
author: 'Trevor Hansen',
ingredientsImagePath: 'food/icons/veggie.png',
description:
'This vegetable has more to offer than just its root. Beet greens can be tossed into a salad to add some variety or sauteed on its own with some oil and garlic.',
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'),
RecipeIngredient(amount: '3 cups', description: 'Beet greens'),
],
steps: <RecipeStep>[
RecipeStep(duration: '5 min', description: 'Chop'),
@@ -611,15 +609,15 @@ const List<Recipe> kPestoRecipes = <Recipe>[
name: 'Pesto Pasta',
author: 'Ali Connors',
ingredientsImagePath: 'food/icons/main.png',
description:
'With this pesto recipe, you can quickly whip up a meal to satisfy your savory needs. And if you\'re feeling festive, you can add bacon to taste.',
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/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'),
@@ -635,13 +633,13 @@ const List<Recipe> kPestoRecipes = <Recipe>[
name: 'Cherry Pie',
author: 'Sandra Adams',
ingredientsImagePath: 'food/icons/main.png',
description:
'Sometimes when you\'re craving some cheer in your life you can jumpstart your day with some cherry pie. Dessert for breakfast is perfectly acceptable.',
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: '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'),
@@ -655,9 +653,10 @@ const List<Recipe> kPestoRecipes = <Recipe>[
name: 'Spinach Salad',
author: 'Peter Carlsson',
ingredientsImagePath: 'food/icons/spicy.png',
description:
'Everyone\'s favorite leafy green is back. Paired with fresh sliced onion, it\'s ready to tackle any dish, whether it be a salad or an egg scramble.',
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'),
@@ -670,9 +669,10 @@ const List<Recipe> kPestoRecipes = <Recipe>[
name: 'Butternut Squash Soup',
author: 'Ali Connors',
ingredientsImagePath: 'food/icons/healthy.png',
description:
'This creamy butternut squash soup will warm you on the chilliest of winter nights and bring a delightful pop of orange to the dinner table.',
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'),
@@ -686,16 +686,17 @@ const List<Recipe> kPestoRecipes = <Recipe>[
steps: <RecipeStep>[
RecipeStep(duration: '10 min', description: 'Prep vegetables'),
RecipeStep(duration: '5 min', description: 'Stir'),
RecipeStep(duration: '1 hr 10 min', description: 'Cook')
RecipeStep(duration: '1 hr 10 min', description: 'Cook'),
],
),
Recipe(
name: 'Spanakopita',
author: 'Trevor Hansen',
ingredientsImagePath: 'food/icons/quick.png',
description:
'You \'feta\' believe this is a crowd-pleaser! Flaky phyllo pastry surrounds a delicious mixture of spinach and cheeses to create the perfect appetizer.',
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'),
@@ -706,13 +707,9 @@ const List<Recipe> kPestoRecipes = <Recipe>[
],
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')
RecipeStep(duration: '3 min', description: 'Stir vegetables and other filling ingredients'),
RecipeStep(duration: '10 min', description: 'Fill phyllo squares half-full with filling and fold.'),
RecipeStep(duration: '40 min', description: 'Bake'),
],
),
];

View File

@@ -0,0 +1,138 @@
// 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,
);

View File

@@ -0,0 +1,330 @@
// 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 _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>[
widget.backLayer,
PositionedTransition(
rect: _layerAnimation,
child: _FrontLayer(
onTap: _toggleBackdropLayerVisibility,
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,
),
);
}
}

View File

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

View File

@@ -0,0 +1,28 @@
// 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;

View File

@@ -0,0 +1,656 @@
// 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;
}

View File

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

View File

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

View File

@@ -0,0 +1,123 @@
// 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)';
}
}

View File

@@ -0,0 +1,48 @@
// 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)';
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,134 @@
// 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;
}
}
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,190 @@
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;
}
}

View File

@@ -0,0 +1,286 @@
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(),
);
}
}

View File

@@ -0,0 +1,70 @@
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,
),
),
);
}
}

View File

@@ -0,0 +1,36 @@
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,
),
],
);
}
}

View File

@@ -0,0 +1,571 @@
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();
}
}

View File

@@ -0,0 +1,68 @@
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);
}
}

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