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:
73
web/gallery/.gitignore
vendored
Normal file
73
web/gallery/.gitignore
vendored
Normal 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
10
web/gallery/.metadata
Normal 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
|
||||
@@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
@@ -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']
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
270
web/gallery/lib/demo/calculator/home.dart
Normal file
270
web/gallery/lib/demo/calculator/home.dart
Normal 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);
|
||||
});
|
||||
}
|
||||
342
web/gallery/lib/demo/calculator/logic.dart
Normal file
342
web/gallery/lib/demo/calculator/logic.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
16
web/gallery/lib/demo/calculator_demo.dart
Normal file
16
web/gallery/lib/demo/calculator_demo.dart
Normal 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();
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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',
|
||||
|
||||
14
web/gallery/lib/demo/cupertino/cupertino.dart
Normal file
14
web/gallery/lib/demo/cupertino/cupertino.dart
Normal 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';
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
275
web/gallery/lib/demo/cupertino/cupertino_alert_demo.dart
Normal file
275
web/gallery/lib/demo/cupertino/cupertino_alert_demo.dart
Normal 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');
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
89
web/gallery/lib/demo/cupertino/cupertino_buttons_demo.dart
Normal file
89
web/gallery/lib/demo/cupertino/cupertino_buttons_demo.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
804
web/gallery/lib/demo/cupertino/cupertino_navigation_demo.dart
Normal file
804
web/gallery/lib/demo/cupertino/cupertino_navigation_demo.dart
Normal 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);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
275
web/gallery/lib/demo/cupertino/cupertino_picker_demo.dart
Normal file
275
web/gallery/lib/demo/cupertino/cupertino_picker_demo.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
244
web/gallery/lib/demo/cupertino/cupertino_refresh_demo.dart
Normal file
244
web/gallery/lib/demo/cupertino/cupertino_refresh_demo.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
78
web/gallery/lib/demo/cupertino/cupertino_slider_demo.dart
Normal file
78
web/gallery/lib/demo/cupertino/cupertino_slider_demo.dart
Normal 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'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
91
web/gallery/lib/demo/cupertino/cupertino_switch_demo.dart
Normal file
91
web/gallery/lib/demo/cupertino/cupertino_switch_demo.dart
Normal 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'
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
194
web/gallery/lib/demo/cupertino/cupertino_text_field_demo.dart
Normal file
194
web/gallery/lib/demo/cupertino/cupertino_text_field_demo.dart
Normal 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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
52
web/gallery/lib/demo/fortnightly/README.md
Normal file
52
web/gallery/lib/demo/fortnightly/README.md
Normal 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)
|
||||
236
web/gallery/lib/demo/fortnightly/fortnightly.dart
Normal file
236
web/gallery/lib/demo/fortnightly/fortnightly.dart
Normal 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;
|
||||
}
|
||||
40
web/gallery/lib/demo/images_demo.dart
Normal file
40
web/gallery/lib/demo/images_demo.dart
Normal 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',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
109
web/gallery/lib/demo/material/banner_demo.dart
Normal file
109
web/gallery/lib/demo/material/banner_demo.dart
Normal 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}'),);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
386
web/gallery/lib/demo/material/buttons_demo.dart
Normal file
386
web/gallery/lib/demo/material/buttons_demo.dart
Normal 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',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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())),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
40
web/gallery/lib/demo/material/expansion_tile_list_demo.dart
Normal file
40
web/gallery/lib/demo/material/expansion_tile_list_demo.dart
Normal 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')),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))));
|
||||
});
|
||||
})));
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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'))));
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 it’s 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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}));
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'))
|
||||
]));
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
138
web/gallery/lib/demo/shrine/app.dart
Normal file
138
web/gallery/lib/demo/shrine/app.dart
Normal 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,
|
||||
);
|
||||
330
web/gallery/lib/demo/shrine/backdrop.dart
Normal file
330
web/gallery/lib/demo/shrine/backdrop.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
85
web/gallery/lib/demo/shrine/category_menu_page.dart
Normal file
85
web/gallery/lib/demo/shrine/category_menu_page.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
28
web/gallery/lib/demo/shrine/colors.dart
Normal file
28
web/gallery/lib/demo/shrine/colors.dart
Normal 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;
|
||||
656
web/gallery/lib/demo/shrine/expanding_bottom_sheet.dart
Normal file
656
web/gallery/lib/demo/shrine/expanding_bottom_sheet.dart
Normal 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;
|
||||
}
|
||||
57
web/gallery/lib/demo/shrine/home.dart
Normal file
57
web/gallery/lib/demo/shrine/home.dart
Normal 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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
144
web/gallery/lib/demo/shrine/login.dart
Normal file
144
web/gallery/lib/demo/shrine/login.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
123
web/gallery/lib/demo/shrine/model/app_state_model.dart
Normal file
123
web/gallery/lib/demo/shrine/model/app_state_model.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
48
web/gallery/lib/demo/shrine/model/product.dart
Normal file
48
web/gallery/lib/demo/shrine/model/product.dart
Normal 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)';
|
||||
}
|
||||
293
web/gallery/lib/demo/shrine/model/products_repository.dart
Normal file
293
web/gallery/lib/demo/shrine/model/products_repository.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
275
web/gallery/lib/demo/shrine/shopping_cart.dart
Normal file
275
web/gallery/lib/demo/shrine/shopping_cart.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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: 'Ali’s shop',
|
||||
avatarAsset: 'people/square/ali.png',
|
||||
avatarAssetPackage: _kGalleryAssetsPackage,
|
||||
description:
|
||||
'Ali Connor’s makes custom goods for folks of all shapes and sizes '
|
||||
'made by hand and sometimes by machine, but always with love and care. '
|
||||
'Custom orders are available upon request if you need something extra special.');
|
||||
|
||||
const Vendor _peter = Vendor(
|
||||
name: 'Peter’s shop',
|
||||
avatarAsset: 'people/square/peter.png',
|
||||
avatarAssetPackage: _kGalleryAssetsPackage,
|
||||
description:
|
||||
'Peter makes great stuff for awesome people like you. Super cool and extra '
|
||||
'awesome all of his shop’s goods are handmade with love. Custom orders are '
|
||||
'available upon request if you need something extra special.');
|
||||
|
||||
const Vendor _sandra = Vendor(
|
||||
name: 'Sandra’s shop',
|
||||
avatarAsset: 'people/square/sandra.png',
|
||||
avatarAssetPackage: _kGalleryAssetsPackage,
|
||||
description:
|
||||
'Sandra specializes in furniture, beauty and travel products with a classic vibe. '
|
||||
'Custom orders are available if you’re looking for a certain color or material.');
|
||||
|
||||
const Vendor _stella = Vendor(
|
||||
name: 'Stella’s shop',
|
||||
avatarAsset: 'people/square/stella.png',
|
||||
avatarAssetPackage: _kGalleryAssetsPackage,
|
||||
description:
|
||||
'Stella sells awesome stuff at lovely prices. made by hand and sometimes by '
|
||||
'machine, but always with love and care. Custom orders are available upon request '
|
||||
'if you need something extra special.');
|
||||
|
||||
const Vendor _trevor = Vendor(
|
||||
name: 'Trevor’s shop',
|
||||
avatarAsset: 'people/square/trevor.png',
|
||||
avatarAssetPackage: _kGalleryAssetsPackage,
|
||||
description:
|
||||
'Trevor makes great stuff for awesome people like you. Super cool and extra '
|
||||
'awesome all of his shop’s goods are handmade with love. Custom orders are '
|
||||
'available upon request if you need something extra special.');
|
||||
|
||||
const List<Product> _allProducts = <Product>[
|
||||
Product(
|
||||
name: 'Vintage Brown Belt',
|
||||
imageAsset: 'products/belt.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
categories: <String>['fashion', 'latest'],
|
||||
price: 300.00,
|
||||
vendor: _sandra,
|
||||
description:
|
||||
'Isn’t it cool when things look old, but they\'re not. Looks Old But Not makes '
|
||||
'awesome vintage goods that are super smart. This ol’ belt just got an upgrade. '),
|
||||
Product(
|
||||
name: 'Sunglasses',
|
||||
imageAsset: 'products/sunnies.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
categories: <String>['travel', 'fashion', 'beauty'],
|
||||
price: 20.00,
|
||||
vendor: _trevor,
|
||||
description:
|
||||
'Be an optimist. Carry Sunglasses with you at all times. All Tints and '
|
||||
'Shades products come with polarized lenses and super duper UV protection '
|
||||
'so you can look at the sun for however long you want. Sunglasses make you '
|
||||
'look cool, wear them.'),
|
||||
Product(
|
||||
name: 'Flatwear',
|
||||
imageAsset: 'products/flatwear.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
categories: <String>['furniture'],
|
||||
price: 30.00,
|
||||
vendor: _trevor,
|
||||
description:
|
||||
'Leave the tunnel and the rain is fallin amazing things happen when you wait'),
|
||||
Product(
|
||||
name: 'Salmon Sweater',
|
||||
imageAsset: 'products/sweater.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
categories: <String>['fashion'],
|
||||
price: 300.00,
|
||||
vendor: _stella,
|
||||
description:
|
||||
'Looks can be deceiving. This sweater comes in a wide variety of '
|
||||
'flavors, including salmon, that pop as soon as they hit your eyes. '
|
||||
'Sweaters heat quickly, so savor the warmth.'),
|
||||
Product(
|
||||
name: 'Pine Table',
|
||||
imageAsset: 'products/table.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
categories: <String>['furniture'],
|
||||
price: 63.00,
|
||||
vendor: _stella,
|
||||
description:
|
||||
'Leave the tunnel and the rain is fallin amazing things happen when you wait'),
|
||||
Product(
|
||||
name: 'Green Comfort Jacket',
|
||||
imageAsset: 'products/jacket.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
categories: <String>['fashion'],
|
||||
price: 36.00,
|
||||
vendor: _ali,
|
||||
description:
|
||||
'Leave the tunnel and the rain is fallin amazing things happen when you wait'),
|
||||
Product(
|
||||
name: 'Chambray Top',
|
||||
imageAsset: 'products/top.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
categories: <String>['fashion'],
|
||||
price: 125.00,
|
||||
vendor: _peter,
|
||||
description:
|
||||
'Leave the tunnel and the rain is fallin amazing things happen when you wait'),
|
||||
Product(
|
||||
name: 'Blue Cup',
|
||||
imageAsset: 'products/cup.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
categories: <String>['travel', 'furniture'],
|
||||
price: 75.00,
|
||||
vendor: _sandra,
|
||||
description:
|
||||
'Drinksy has been making extraordinary mugs for decades. With each '
|
||||
'cup purchased Drinksy donates a cup to those in need. Buy yourself a mug, '
|
||||
'buy someone else a mug.'),
|
||||
Product(
|
||||
name: 'Tea Set',
|
||||
imageAsset: 'products/teaset.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
categories: <String>['furniture', 'fashion'],
|
||||
price: 70.00,
|
||||
vendor: _trevor,
|
||||
featureTitle: 'Beautiful glass teapot',
|
||||
featureDescription:
|
||||
'Teapot holds extremely hot liquids and pours them from the spout.',
|
||||
description:
|
||||
'Impress your guests with Tea Set by Kitchen Stuff. Teapot holds extremely '
|
||||
'hot liquids and pours them from the spout. Use the handle, shown on the right, '
|
||||
'so your fingers don’t get burnt while pouring.'),
|
||||
Product(
|
||||
name: 'Blue linen napkins',
|
||||
imageAsset: 'products/napkins.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
categories: <String>['furniture', 'fashion'],
|
||||
price: 89.00,
|
||||
vendor: _trevor,
|
||||
description:
|
||||
'Blue linen napkins were meant to go with friends, so you may want to pick '
|
||||
'up a bunch of these. These things are absorbant.'),
|
||||
Product(
|
||||
name: 'Dipped Earrings',
|
||||
imageAsset: 'products/earrings.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
categories: <String>['fashion', 'beauty'],
|
||||
price: 25.00,
|
||||
vendor: _stella,
|
||||
description:
|
||||
'WeDipIt does it again. These hand-dipped 4 inch earrings are perfect for '
|
||||
'the office or the beach. Just be sure you don’t drop it in a bucket of '
|
||||
'red paint, then they won’t look dipped anymore.'),
|
||||
Product(
|
||||
name: 'Perfect Planters',
|
||||
imageAsset: 'products/planters.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
categories: <String>['latest', 'furniture'],
|
||||
price: 30.00,
|
||||
vendor: _ali,
|
||||
description:
|
||||
'The Perfect Planter Co makes the best vessels for just about anything you '
|
||||
'can pot. This set of Perfect Planters holds succulents and cuttings perfectly. '
|
||||
'Looks great in any room. Keep out of reach from cats.'),
|
||||
Product(
|
||||
name: 'Cloud-White Dress',
|
||||
imageAsset: 'products/dress.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
categories: <String>['fashion'],
|
||||
price: 54.00,
|
||||
vendor: _sandra,
|
||||
description:
|
||||
'Trying to find the perfect outift to match your mood? Try no longer. '
|
||||
'This Cloud-White Dress has you covered for those nights when you need '
|
||||
'to get out, or even if you’re just headed to work.'),
|
||||
Product(
|
||||
name: 'Backpack',
|
||||
imageAsset: 'products/backpack.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
categories: <String>['travel', 'fashion'],
|
||||
price: 25.00,
|
||||
vendor: _peter,
|
||||
description:
|
||||
'This backpack by Bags ‘n’ stuff can hold just about anything: a laptop, '
|
||||
'a pen, a protractor, notebooks, small animals, plugs for your devices, '
|
||||
'sunglasses, gym clothes, shoes, gloves, two kittens, and even lunch!'),
|
||||
Product(
|
||||
name: 'Charcoal Straw Hat',
|
||||
imageAsset: 'products/hat.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
categories: <String>['travel', 'fashion', 'latest'],
|
||||
price: 25.00,
|
||||
vendor: _ali,
|
||||
description:
|
||||
'This is the helmet for those warm summer days on the road. '
|
||||
'Jetset approved, these hats have been rigorously tested. Keep that face '
|
||||
'protected from the sun.'),
|
||||
Product(
|
||||
name: 'Ginger Scarf',
|
||||
imageAsset: 'products/scarf.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
categories: <String>['latest', 'fashion'],
|
||||
price: 17.00,
|
||||
vendor: _peter,
|
||||
description:
|
||||
'Leave the tunnel and the rain is fallin amazing things happen when you wait'),
|
||||
Product(
|
||||
name: 'Blush Sweats',
|
||||
imageAsset: 'products/sweats.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
categories: <String>['travel', 'fashion', 'latest'],
|
||||
price: 25.00,
|
||||
vendor: _stella,
|
||||
description:
|
||||
'Leave the tunnel and the rain is fallin amazing things happen when you wait'),
|
||||
Product(
|
||||
name: 'Mint Jumper',
|
||||
imageAsset: 'products/jumper.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
categories: <String>['travel', 'fashion', 'beauty'],
|
||||
price: 25.00,
|
||||
vendor: _peter,
|
||||
description:
|
||||
'Leave the tunnel and the rain is fallin amazing things happen when you wait'),
|
||||
Product(
|
||||
name: 'Ochre Shirt',
|
||||
imageAsset: 'products/shirt.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
categories: <String>['fashion', 'latest'],
|
||||
price: 120.00,
|
||||
vendor: _stella,
|
||||
description:
|
||||
'Leave the tunnel and the rain is fallin amazing things happen when you wait')
|
||||
];
|
||||
|
||||
List<Product> allProducts() {
|
||||
assert(_allProducts.every((Product product) => product.isValid()));
|
||||
return List<Product>.unmodifiable(_allProducts);
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)';
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
134
web/gallery/lib/demo/shrine/supplemental/cut_corners_border.dart
Normal file
134
web/gallery/lib/demo/shrine/supplemental/cut_corners_border.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
97
web/gallery/lib/demo/shrine/supplemental/product_card.dart
Normal file
97
web/gallery/lib/demo/shrine/supplemental/product_card.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
190
web/gallery/lib/demo/transformations/transformations_demo.dart
Normal file
190
web/gallery/lib/demo/transformations/transformations_demo.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user