mirror of https://github.com/flutter/samples.git
Deleting web/gallery because the newer gallery/gallery includes web support. (#240)
parent
9d6f6d3641
commit
234a7f3d0d
@ -1,32 +0,0 @@
|
||||
# Flutter gallery
|
||||
|
||||
A demo app for Flutter's material design and cupertino widgets, as
|
||||
well as many other features of the Flutter SDK.
|
||||
|
||||
## Building
|
||||
|
||||
You can follow these instructions to build the gallery app
|
||||
and install it onto your device.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
If you are new to Flutter, please first follow
|
||||
the [Flutter Setup](https://flutter.dev/setup/) guide.
|
||||
|
||||
### Building and installing the Flutter app
|
||||
|
||||
* `cd $FLUTTER_ROOT/examples/flutter_gallery`
|
||||
* `flutter pub get`
|
||||
* `flutter run --release`
|
||||
|
||||
The `flutter run --release` command both builds and installs the Flutter app.
|
||||
|
||||
## Prerelease checklist
|
||||
|
||||
* Verify that the About box's license page scrolls and reveals its long
|
||||
long stream of license texts.
|
||||
|
||||
## Icon
|
||||
|
||||
Android launcher icons were generated using Android Asset Studio:
|
||||
https://romannurik.github.io/AndroidAssetStudio/icons-launcher.html#foreground.type=image&foreground.space.trim=1&foreground.space.pad=0.1&foreColor=607d8b%2C0&crop=0&backgroundShape=square&backColor=fafafa%2C100&effects=none
|
||||
@ -1,8 +0,0 @@
|
||||
# Take our settings from the repo's main analysis_options.yaml file, but add
|
||||
# an exclude for the build directory.
|
||||
|
||||
include: ../../analysis_options.yaml
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
- build/**
|
||||
|
Before Width: | Height: | Size: 22 KiB |
@ -1,17 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
export 'animation_demo.dart';
|
||||
export 'calculator_demo.dart';
|
||||
export 'colors_demo.dart';
|
||||
export 'contacts_demo.dart';
|
||||
export 'cupertino/cupertino.dart';
|
||||
export 'fortnightly/fortnightly.dart';
|
||||
export 'images_demo.dart';
|
||||
export 'material/material.dart';
|
||||
export 'pesto_demo.dart';
|
||||
export 'shrine_demo.dart';
|
||||
export 'transformations/transformations_demo.dart';
|
||||
export 'typography_demo.dart';
|
||||
export 'video_demo.dart';
|
||||
@ -1,628 +0,0 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// Based on https://material.uplabs.com/posts/google-newsstand-navigation-pattern
|
||||
// See also: https://material-motion.github.io/material-motion/documentation/
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import 'sections.dart';
|
||||
import 'widgets.dart';
|
||||
|
||||
const Color _kAppBackgroundColor = Color(0xFF353662);
|
||||
const Duration _kScrollDuration = Duration(milliseconds: 400);
|
||||
const Curve _kScrollCurve = Curves.fastOutSlowIn;
|
||||
|
||||
// This app's contents start out at _kHeadingMaxHeight and they function like
|
||||
// an appbar. Initially the appbar occupies most of the screen and its section
|
||||
// headings are laid out in a column. By the time its height has been
|
||||
// reduced to _kAppBarMidHeight, its layout is horizontal, only one section
|
||||
// heading is visible, and the section's list of details is visible below the
|
||||
// heading. The appbar's height can be reduced to no more than _kAppBarMinHeight.
|
||||
const double _kAppBarMinHeight = 90.0;
|
||||
const double _kAppBarMidHeight = 256.0;
|
||||
// The AppBar's max height depends on the screen, see _AnimationDemoHomeState._buildBody()
|
||||
|
||||
// Initially occupies the same space as the status bar and gets smaller as
|
||||
// the primary scrollable scrolls upwards.
|
||||
// TODO(hansmuller): it would be worth adding something like this to the framework.
|
||||
class _RenderStatusBarPaddingSliver extends RenderSliver {
|
||||
_RenderStatusBarPaddingSliver({
|
||||
@required double maxHeight,
|
||||
@required double scrollFactor,
|
||||
}) : assert(maxHeight != null && maxHeight >= 0.0),
|
||||
assert(scrollFactor != null && scrollFactor >= 1.0),
|
||||
_maxHeight = maxHeight,
|
||||
_scrollFactor = scrollFactor;
|
||||
|
||||
// The height of the status bar
|
||||
double get maxHeight => _maxHeight;
|
||||
double _maxHeight;
|
||||
set maxHeight(double value) {
|
||||
assert(maxHeight != null && maxHeight >= 0.0);
|
||||
if (_maxHeight == value)
|
||||
return;
|
||||
_maxHeight = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
// That rate at which this renderer's height shrinks when the scroll
|
||||
// offset changes.
|
||||
double get scrollFactor => _scrollFactor;
|
||||
double _scrollFactor;
|
||||
set scrollFactor(double value) {
|
||||
assert(scrollFactor != null && scrollFactor >= 1.0);
|
||||
if (_scrollFactor == value)
|
||||
return;
|
||||
_scrollFactor = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
final double height = (maxHeight - constraints.scrollOffset / scrollFactor).clamp(0.0, maxHeight);
|
||||
geometry = SliverGeometry(
|
||||
paintExtent: math.min(height, constraints.remainingPaintExtent),
|
||||
scrollExtent: maxHeight,
|
||||
maxPaintExtent: maxHeight,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatusBarPaddingSliver extends SingleChildRenderObjectWidget {
|
||||
const _StatusBarPaddingSliver({
|
||||
Key key,
|
||||
@required this.maxHeight,
|
||||
this.scrollFactor = 5.0,
|
||||
}) : assert(maxHeight != null && maxHeight >= 0.0),
|
||||
assert(scrollFactor != null && scrollFactor >= 1.0),
|
||||
super(key: key);
|
||||
|
||||
final double maxHeight;
|
||||
final double scrollFactor;
|
||||
|
||||
@override
|
||||
_RenderStatusBarPaddingSliver createRenderObject(BuildContext context) {
|
||||
return _RenderStatusBarPaddingSliver(
|
||||
maxHeight: maxHeight,
|
||||
scrollFactor: scrollFactor,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, _RenderStatusBarPaddingSliver renderObject) {
|
||||
renderObject
|
||||
..maxHeight = maxHeight
|
||||
..scrollFactor = scrollFactor;
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder description) {
|
||||
super.debugFillProperties(description);
|
||||
description.add(DoubleProperty('maxHeight', maxHeight));
|
||||
description.add(DoubleProperty('scrollFactor', scrollFactor));
|
||||
}
|
||||
}
|
||||
|
||||
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
||||
_SliverAppBarDelegate({
|
||||
@required this.minHeight,
|
||||
@required this.maxHeight,
|
||||
@required this.child,
|
||||
});
|
||||
|
||||
final double minHeight;
|
||||
final double maxHeight;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
double get minExtent => minHeight;
|
||||
@override
|
||||
double get maxExtent => math.max(maxHeight, minHeight);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||
return SizedBox.expand(child: child);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
|
||||
return maxHeight != oldDelegate.maxHeight
|
||||
|| minHeight != oldDelegate.minHeight
|
||||
|| child != oldDelegate.child;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => '_SliverAppBarDelegate';
|
||||
}
|
||||
|
||||
// Arrange the section titles, indicators, and cards. The cards are only included when
|
||||
// the layout is transitioning between vertical and horizontal. Once the layout is
|
||||
// horizontal the cards are laid out by a PageView.
|
||||
//
|
||||
// The layout of the section cards, titles, and indicators is defined by the
|
||||
// two 0.0-1.0 "t" parameters, both of which are based on the layout's height:
|
||||
// - tColumnToRow
|
||||
// 0.0 when height is maxHeight and the layout is a column
|
||||
// 1.0 when the height is midHeight and the layout is a row
|
||||
// - tCollapsed
|
||||
// 0.0 when height is midHeight and the layout is a row
|
||||
// 1.0 when height is minHeight and the layout is a (still) row
|
||||
//
|
||||
// minHeight < midHeight < maxHeight
|
||||
//
|
||||
// The general approach here is to compute the column layout and row size
|
||||
// and position of each element and then interpolate between them using
|
||||
// tColumnToRow. Once tColumnToRow reaches 1.0, the layout changes are
|
||||
// defined by tCollapsed. As tCollapsed increases the titles spread out
|
||||
// until only one title is visible and the indicators cluster together
|
||||
// until they're all visible.
|
||||
class _AllSectionsLayout extends MultiChildLayoutDelegate {
|
||||
_AllSectionsLayout({
|
||||
this.translation,
|
||||
this.tColumnToRow,
|
||||
this.tCollapsed,
|
||||
this.cardCount,
|
||||
this.selectedIndex,
|
||||
});
|
||||
|
||||
final Alignment translation;
|
||||
final double tColumnToRow;
|
||||
final double tCollapsed;
|
||||
final int cardCount;
|
||||
final double selectedIndex;
|
||||
|
||||
Rect _interpolateRect(Rect begin, Rect end) {
|
||||
return Rect.lerp(begin, end, tColumnToRow);
|
||||
}
|
||||
|
||||
Offset _interpolatePoint(Offset begin, Offset end) {
|
||||
return Offset.lerp(begin, end, tColumnToRow);
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout(Size size) {
|
||||
final double columnCardX = size.width / 5.0;
|
||||
final double columnCardWidth = size.width - columnCardX;
|
||||
final double columnCardHeight = size.height / cardCount;
|
||||
final double rowCardWidth = size.width;
|
||||
final Offset offset = translation.alongSize(size);
|
||||
double columnCardY = 0.0;
|
||||
double rowCardX = -(selectedIndex * rowCardWidth);
|
||||
|
||||
// When tCollapsed > 0 the titles spread apart
|
||||
final double columnTitleX = size.width / 10.0;
|
||||
final double rowTitleWidth = size.width * ((1 + tCollapsed) / 2.25);
|
||||
double rowTitleX = (size.width - rowTitleWidth) / 2.0 - selectedIndex * rowTitleWidth;
|
||||
|
||||
// When tCollapsed > 0, the indicators move closer together
|
||||
//final double rowIndicatorWidth = 48.0 + (1.0 - tCollapsed) * (rowTitleWidth - 48.0);
|
||||
const double paddedSectionIndicatorWidth = kSectionIndicatorWidth + 8.0;
|
||||
final double rowIndicatorWidth = paddedSectionIndicatorWidth +
|
||||
(1.0 - tCollapsed) * (rowTitleWidth - paddedSectionIndicatorWidth);
|
||||
double rowIndicatorX = (size.width - rowIndicatorWidth) / 2.0 - selectedIndex * rowIndicatorWidth;
|
||||
|
||||
// Compute the size and origin of each card, title, and indicator for the maxHeight
|
||||
// "column" layout, and the midHeight "row" layout. The actual layout is just the
|
||||
// interpolated value between the column and row layouts for t.
|
||||
for (int index = 0; index < cardCount; index++) {
|
||||
|
||||
// Layout the card for index.
|
||||
final Rect columnCardRect = Rect.fromLTWH(columnCardX, columnCardY, columnCardWidth, columnCardHeight);
|
||||
final Rect rowCardRect = Rect.fromLTWH(rowCardX, 0.0, rowCardWidth, size.height);
|
||||
final Rect cardRect = _interpolateRect(columnCardRect, rowCardRect).shift(offset);
|
||||
final String cardId = 'card$index';
|
||||
if (hasChild(cardId)) {
|
||||
layoutChild(cardId, BoxConstraints.tight(cardRect.size));
|
||||
positionChild(cardId, cardRect.topLeft);
|
||||
}
|
||||
|
||||
// Layout the title for index.
|
||||
final Size titleSize = layoutChild('title$index', BoxConstraints.loose(cardRect.size));
|
||||
final double columnTitleY = columnCardRect.centerLeft.dy - titleSize.height / 2.0;
|
||||
final double rowTitleY = rowCardRect.centerLeft.dy - titleSize.height / 2.0;
|
||||
final double centeredRowTitleX = rowTitleX + (rowTitleWidth - titleSize.width) / 2.0;
|
||||
final Offset columnTitleOrigin = Offset(columnTitleX, columnTitleY);
|
||||
final Offset rowTitleOrigin = Offset(centeredRowTitleX, rowTitleY);
|
||||
final Offset titleOrigin = _interpolatePoint(columnTitleOrigin, rowTitleOrigin);
|
||||
positionChild('title$index', titleOrigin + offset);
|
||||
|
||||
// Layout the selection indicator for index.
|
||||
final Size indicatorSize = layoutChild('indicator$index', BoxConstraints.loose(cardRect.size));
|
||||
final double columnIndicatorX = cardRect.centerRight.dx - indicatorSize.width - 16.0;
|
||||
final double columnIndicatorY = cardRect.bottomRight.dy - indicatorSize.height - 16.0;
|
||||
final Offset columnIndicatorOrigin = Offset(columnIndicatorX, columnIndicatorY);
|
||||
final Rect titleRect = Rect.fromPoints(titleOrigin, titleSize.bottomRight(titleOrigin));
|
||||
final double centeredRowIndicatorX = rowIndicatorX + (rowIndicatorWidth - indicatorSize.width) / 2.0;
|
||||
final double rowIndicatorY = titleRect.bottomCenter.dy + 16.0;
|
||||
final Offset rowIndicatorOrigin = Offset(centeredRowIndicatorX, rowIndicatorY);
|
||||
final Offset indicatorOrigin = _interpolatePoint(columnIndicatorOrigin, rowIndicatorOrigin);
|
||||
positionChild('indicator$index', indicatorOrigin + offset);
|
||||
|
||||
columnCardY += columnCardHeight;
|
||||
rowCardX += rowCardWidth;
|
||||
rowTitleX += rowTitleWidth;
|
||||
rowIndicatorX += rowIndicatorWidth;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRelayout(_AllSectionsLayout oldDelegate) {
|
||||
return tColumnToRow != oldDelegate.tColumnToRow
|
||||
|| cardCount != oldDelegate.cardCount
|
||||
|| selectedIndex != oldDelegate.selectedIndex;
|
||||
}
|
||||
}
|
||||
|
||||
class _AllSectionsView extends AnimatedWidget {
|
||||
_AllSectionsView({
|
||||
Key key,
|
||||
this.sectionIndex,
|
||||
@required this.sections,
|
||||
@required this.selectedIndex,
|
||||
this.minHeight,
|
||||
this.midHeight,
|
||||
this.maxHeight,
|
||||
this.sectionCards = const <Widget>[],
|
||||
}) : assert(sections != null),
|
||||
assert(sectionCards != null),
|
||||
assert(sectionCards.length == sections.length),
|
||||
assert(sectionIndex >= 0 && sectionIndex < sections.length),
|
||||
assert(selectedIndex != null),
|
||||
assert(selectedIndex.value >= 0.0 && selectedIndex.value < sections.length.toDouble()),
|
||||
super(key: key, listenable: selectedIndex);
|
||||
|
||||
final int sectionIndex;
|
||||
final List<Section> sections;
|
||||
final ValueNotifier<double> selectedIndex;
|
||||
final double minHeight;
|
||||
final double midHeight;
|
||||
final double maxHeight;
|
||||
final List<Widget> sectionCards;
|
||||
|
||||
double _selectedIndexDelta(int index) {
|
||||
return (index.toDouble() - selectedIndex.value).abs().clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
Widget _build(BuildContext context, BoxConstraints constraints) {
|
||||
final Size size = constraints.biggest;
|
||||
|
||||
// The layout's progress from from a column to a row. Its value is
|
||||
// 0.0 when size.height equals the maxHeight, 1.0 when the size.height
|
||||
// equals the midHeight.
|
||||
final double tColumnToRow =
|
||||
1.0 - ((size.height - midHeight) /
|
||||
(maxHeight - midHeight)).clamp(0.0, 1.0);
|
||||
|
||||
|
||||
// The layout's progress from from the midHeight row layout to
|
||||
// a minHeight row layout. Its value is 0.0 when size.height equals
|
||||
// midHeight and 1.0 when size.height equals minHeight.
|
||||
final double tCollapsed =
|
||||
1.0 - ((size.height - minHeight) /
|
||||
(midHeight - minHeight)).clamp(0.0, 1.0);
|
||||
|
||||
double _indicatorOpacity(int index) {
|
||||
return 1.0 - _selectedIndexDelta(index) * 0.5;
|
||||
}
|
||||
|
||||
double _titleOpacity(int index) {
|
||||
return 1.0 - _selectedIndexDelta(index) * tColumnToRow * 0.5;
|
||||
}
|
||||
|
||||
double _titleScale(int index) {
|
||||
return 1.0 - _selectedIndexDelta(index) * tColumnToRow * 0.15;
|
||||
}
|
||||
|
||||
final List<Widget> children = List<Widget>.from(sectionCards);
|
||||
|
||||
for (int index = 0; index < sections.length; index++) {
|
||||
final Section section = sections[index];
|
||||
children.add(LayoutId(
|
||||
id: 'title$index',
|
||||
child: SectionTitle(
|
||||
section: section,
|
||||
scale: _titleScale(index),
|
||||
opacity: _titleOpacity(index),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
for (int index = 0; index < sections.length; index++) {
|
||||
children.add(LayoutId(
|
||||
id: 'indicator$index',
|
||||
child: SectionIndicator(
|
||||
opacity: _indicatorOpacity(index),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
return CustomMultiChildLayout(
|
||||
delegate: _AllSectionsLayout(
|
||||
translation: Alignment((selectedIndex.value - sectionIndex) * 2.0 - 1.0, -1.0),
|
||||
tColumnToRow: tColumnToRow,
|
||||
tCollapsed: tCollapsed,
|
||||
cardCount: sections.length,
|
||||
selectedIndex: selectedIndex.value,
|
||||
),
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: _build);
|
||||
}
|
||||
}
|
||||
|
||||
// Support snapping scrolls to the midScrollOffset: the point at which the
|
||||
// app bar's height is _kAppBarMidHeight and only one section heading is
|
||||
// visible.
|
||||
class _SnappingScrollPhysics extends ClampingScrollPhysics {
|
||||
const _SnappingScrollPhysics({
|
||||
ScrollPhysics parent,
|
||||
@required this.midScrollOffset,
|
||||
}) : assert(midScrollOffset != null),
|
||||
super(parent: parent);
|
||||
|
||||
final double midScrollOffset;
|
||||
|
||||
@override
|
||||
_SnappingScrollPhysics applyTo(ScrollPhysics ancestor) {
|
||||
return _SnappingScrollPhysics(parent: buildParent(ancestor), midScrollOffset: midScrollOffset);
|
||||
}
|
||||
|
||||
Simulation _toMidScrollOffsetSimulation(double offset, double dragVelocity) {
|
||||
final double velocity = math.max(dragVelocity, minFlingVelocity);
|
||||
return ScrollSpringSimulation(spring, offset, midScrollOffset, velocity, tolerance: tolerance);
|
||||
}
|
||||
|
||||
Simulation _toZeroScrollOffsetSimulation(double offset, double dragVelocity) {
|
||||
final double velocity = math.max(dragVelocity, minFlingVelocity);
|
||||
return ScrollSpringSimulation(spring, offset, 0.0, velocity, tolerance: tolerance);
|
||||
}
|
||||
|
||||
@override
|
||||
Simulation createBallisticSimulation(ScrollMetrics position, double dragVelocity) {
|
||||
final Simulation simulation = super.createBallisticSimulation(position, dragVelocity);
|
||||
final double offset = position.pixels;
|
||||
|
||||
if (simulation != null) {
|
||||
// The drag ended with sufficient velocity to trigger creating a simulation.
|
||||
// If the simulation is headed up towards midScrollOffset but will not reach it,
|
||||
// then snap it there. Similarly if the simulation is headed down past
|
||||
// midScrollOffset but will not reach zero, then snap it to zero.
|
||||
final double simulationEnd = simulation.x(double.infinity);
|
||||
if (simulationEnd >= midScrollOffset)
|
||||
return simulation;
|
||||
if (dragVelocity > 0.0)
|
||||
return _toMidScrollOffsetSimulation(offset, dragVelocity);
|
||||
if (dragVelocity < 0.0)
|
||||
return _toZeroScrollOffsetSimulation(offset, dragVelocity);
|
||||
} else {
|
||||
// The user ended the drag with little or no velocity. If they
|
||||
// didn't leave the offset above midScrollOffset, then
|
||||
// snap to midScrollOffset if they're more than halfway there,
|
||||
// otherwise snap to zero.
|
||||
final double snapThreshold = midScrollOffset / 2.0;
|
||||
if (offset >= snapThreshold && offset < midScrollOffset)
|
||||
return _toMidScrollOffsetSimulation(offset, dragVelocity);
|
||||
if (offset > 0.0 && offset < snapThreshold)
|
||||
return _toZeroScrollOffsetSimulation(offset, dragVelocity);
|
||||
}
|
||||
return simulation;
|
||||
}
|
||||
}
|
||||
|
||||
class AnimationDemoHome extends StatefulWidget {
|
||||
const AnimationDemoHome({ Key key }) : super(key: key);
|
||||
|
||||
static const String routeName = '/animation';
|
||||
|
||||
@override
|
||||
_AnimationDemoHomeState createState() => _AnimationDemoHomeState();
|
||||
}
|
||||
|
||||
class _AnimationDemoHomeState extends State<AnimationDemoHome> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final PageController _headingPageController = PageController();
|
||||
final PageController _detailsPageController = PageController();
|
||||
ScrollPhysics _headingScrollPhysics = const NeverScrollableScrollPhysics();
|
||||
ValueNotifier<double> selectedIndex = ValueNotifier<double>(0.0);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: _kAppBackgroundColor,
|
||||
body: Builder(
|
||||
// Insert an element so that _buildBody can find the PrimaryScrollController.
|
||||
builder: _buildBody,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleBackButton(double midScrollOffset) {
|
||||
if (_scrollController.offset >= midScrollOffset)
|
||||
_scrollController.animateTo(0.0, curve: _kScrollCurve, duration: _kScrollDuration);
|
||||
else
|
||||
Navigator.maybePop(context);
|
||||
}
|
||||
|
||||
// Only enable paging for the heading when the user has scrolled to midScrollOffset.
|
||||
// Paging is enabled/disabled by setting the heading's PageView scroll physics.
|
||||
bool _handleScrollNotification(ScrollNotification notification, double midScrollOffset) {
|
||||
if (notification.depth == 0 && notification is ScrollUpdateNotification) {
|
||||
final ScrollPhysics physics = _scrollController.position.pixels >= midScrollOffset
|
||||
? const PageScrollPhysics()
|
||||
: const NeverScrollableScrollPhysics();
|
||||
if (physics != _headingScrollPhysics) {
|
||||
setState(() {
|
||||
_headingScrollPhysics = physics;
|
||||
});
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _maybeScroll(double midScrollOffset, int pageIndex, double xOffset) {
|
||||
if (_scrollController.offset < midScrollOffset) {
|
||||
// Scroll the overall list to the point where only one section card shows.
|
||||
// At the same time scroll the PageViews to the page at pageIndex.
|
||||
_headingPageController.animateToPage(pageIndex, curve: _kScrollCurve, duration: _kScrollDuration);
|
||||
_scrollController.animateTo(midScrollOffset, curve: _kScrollCurve, duration: _kScrollDuration);
|
||||
} else {
|
||||
// One one section card is showing: scroll one page forward or back.
|
||||
final double centerX = _headingPageController.position.viewportDimension / 2.0;
|
||||
final int newPageIndex = xOffset > centerX ? pageIndex + 1 : pageIndex - 1;
|
||||
_headingPageController.animateToPage(newPageIndex, curve: _kScrollCurve, duration: _kScrollDuration);
|
||||
}
|
||||
}
|
||||
|
||||
bool _handlePageNotification(ScrollNotification notification, PageController leader, PageController follower) {
|
||||
if (notification.depth == 0 && notification is ScrollUpdateNotification) {
|
||||
selectedIndex.value = leader.page;
|
||||
if (follower.page != leader.page)
|
||||
follower.position.jumpToWithoutSettling(leader.position.pixels); // ignore: deprecated_member_use
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Iterable<Widget> _detailItemsFor(Section section) {
|
||||
final Iterable<Widget> detailItems = section.details.map<Widget>((SectionDetail detail) {
|
||||
return SectionDetailView(detail: detail);
|
||||
});
|
||||
return ListTile.divideTiles(context: context, tiles: detailItems);
|
||||
}
|
||||
|
||||
Iterable<Widget> _allHeadingItems(double maxHeight, double midScrollOffset) {
|
||||
final List<Widget> sectionCards = <Widget>[];
|
||||
for (int index = 0; index < allSections.length; index++) {
|
||||
sectionCards.add(LayoutId(
|
||||
id: 'card$index',
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: SectionCard(section: allSections[index]),
|
||||
onTapUp: (TapUpDetails details) {
|
||||
final double xOffset = details.globalPosition.dx;
|
||||
setState(() {
|
||||
_maybeScroll(midScrollOffset, index, xOffset);
|
||||
});
|
||||
},
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
final List<Widget> headings = <Widget>[];
|
||||
for (int index = 0; index < allSections.length; index++) {
|
||||
headings.add(Container(
|
||||
color: _kAppBackgroundColor,
|
||||
child: ClipRect(
|
||||
child: _AllSectionsView(
|
||||
sectionIndex: index,
|
||||
sections: allSections,
|
||||
selectedIndex: selectedIndex,
|
||||
minHeight: _kAppBarMinHeight,
|
||||
midHeight: _kAppBarMidHeight,
|
||||
maxHeight: maxHeight,
|
||||
sectionCards: sectionCards,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
return headings;
|
||||
}
|
||||
|
||||
Widget _buildBody(BuildContext context) {
|
||||
final MediaQueryData mediaQueryData = MediaQuery.of(context);
|
||||
final double statusBarHeight = mediaQueryData.padding.top;
|
||||
final double screenHeight = mediaQueryData.size.height;
|
||||
final double appBarMaxHeight = screenHeight - statusBarHeight;
|
||||
|
||||
// The scroll offset that reveals the appBarMidHeight appbar.
|
||||
final double appBarMidScrollOffset = statusBarHeight + appBarMaxHeight - _kAppBarMidHeight;
|
||||
|
||||
return SizedBox.expand(
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
NotificationListener<ScrollNotification>(
|
||||
onNotification: (ScrollNotification notification) {
|
||||
return _handleScrollNotification(notification, appBarMidScrollOffset);
|
||||
},
|
||||
child: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
physics: _SnappingScrollPhysics(midScrollOffset: appBarMidScrollOffset),
|
||||
slivers: <Widget>[
|
||||
// Start out below the status bar, gradually move to the top of the screen.
|
||||
_StatusBarPaddingSliver(
|
||||
maxHeight: statusBarHeight,
|
||||
scrollFactor: 7.0,
|
||||
),
|
||||
// Section Headings
|
||||
SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: _SliverAppBarDelegate(
|
||||
minHeight: _kAppBarMinHeight,
|
||||
maxHeight: appBarMaxHeight,
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (ScrollNotification notification) {
|
||||
return _handlePageNotification(notification, _headingPageController, _detailsPageController);
|
||||
},
|
||||
child: PageView(
|
||||
physics: _headingScrollPhysics,
|
||||
controller: _headingPageController,
|
||||
children: _allHeadingItems(appBarMaxHeight, appBarMidScrollOffset),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Details
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: 610.0,
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (ScrollNotification notification) {
|
||||
return _handlePageNotification(notification, _detailsPageController, _headingPageController);
|
||||
},
|
||||
child: PageView(
|
||||
controller: _detailsPageController,
|
||||
children: allSections.map<Widget>((Section section) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: _detailItemsFor(section).toList(),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: statusBarHeight,
|
||||
left: 0.0,
|
||||
child: IconTheme(
|
||||
data: const IconThemeData(color: Colors.white),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: IconButton(
|
||||
icon: const BackButtonIcon(),
|
||||
tooltip: 'Back',
|
||||
onPressed: () {
|
||||
_handleBackButton(appBarMidScrollOffset);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,170 +0,0 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// Raw data for the animation demo.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const Color _mariner = Color(0xFF3B5F8F);
|
||||
const Color _mediumPurple = Color(0xFF8266D4);
|
||||
const Color _tomato = Color(0xFFF95B57);
|
||||
const Color _mySin = Color(0xFFF3A646);
|
||||
|
||||
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
|
||||
|
||||
class SectionDetail {
|
||||
const SectionDetail({
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.imageAsset,
|
||||
this.imageAssetPackage,
|
||||
});
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final String imageAsset;
|
||||
final String imageAssetPackage;
|
||||
}
|
||||
|
||||
class Section {
|
||||
const Section({
|
||||
this.title,
|
||||
this.backgroundAsset,
|
||||
this.backgroundAssetPackage,
|
||||
this.leftColor,
|
||||
this.rightColor,
|
||||
this.details,
|
||||
});
|
||||
final String title;
|
||||
final String backgroundAsset;
|
||||
final String backgroundAssetPackage;
|
||||
final Color leftColor;
|
||||
final Color rightColor;
|
||||
final List<SectionDetail> details;
|
||||
|
||||
@override
|
||||
bool operator==(Object other) {
|
||||
if (other is! Section)
|
||||
return false;
|
||||
final Section otherSection = other;
|
||||
return title == otherSection.title;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => title.hashCode;
|
||||
}
|
||||
|
||||
// TODO(hansmuller): replace the SectionDetail images and text. Get rid of
|
||||
// the const vars like _eyeglassesDetail and insert a variety of titles and
|
||||
// image SectionDetails in the allSections list.
|
||||
|
||||
const SectionDetail _eyeglassesDetail = SectionDetail(
|
||||
imageAsset: 'products/sunnies.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Flutter enables interactive animation',
|
||||
subtitle: '3K views - 5 days',
|
||||
);
|
||||
|
||||
const SectionDetail _eyeglassesImageDetail = SectionDetail(
|
||||
imageAsset: 'products/sunnies.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
);
|
||||
|
||||
const SectionDetail _seatingDetail = SectionDetail(
|
||||
imageAsset: 'products/table.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Flutter enables interactive animation',
|
||||
subtitle: '3K views - 5 days',
|
||||
);
|
||||
|
||||
const SectionDetail _seatingImageDetail = SectionDetail(
|
||||
imageAsset: 'products/table.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
);
|
||||
|
||||
const SectionDetail _decorationDetail = SectionDetail(
|
||||
imageAsset: 'products/earrings.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Flutter enables interactive animation',
|
||||
subtitle: '3K views - 5 days',
|
||||
);
|
||||
|
||||
const SectionDetail _decorationImageDetail = SectionDetail(
|
||||
imageAsset: 'products/earrings.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
);
|
||||
|
||||
const SectionDetail _protectionDetail = SectionDetail(
|
||||
imageAsset: 'products/hat.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Flutter enables interactive animation',
|
||||
subtitle: '3K views - 5 days',
|
||||
);
|
||||
|
||||
const SectionDetail _protectionImageDetail = SectionDetail(
|
||||
imageAsset: 'products/hat.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
);
|
||||
|
||||
final List<Section> allSections = <Section>[
|
||||
const Section(
|
||||
title: 'SUNGLASSES',
|
||||
leftColor: _mediumPurple,
|
||||
rightColor: _mariner,
|
||||
backgroundAsset: 'products/sunnies.png',
|
||||
backgroundAssetPackage: _kGalleryAssetsPackage,
|
||||
details: <SectionDetail>[
|
||||
_eyeglassesDetail,
|
||||
_eyeglassesImageDetail,
|
||||
_eyeglassesDetail,
|
||||
_eyeglassesDetail,
|
||||
_eyeglassesDetail,
|
||||
_eyeglassesDetail,
|
||||
],
|
||||
),
|
||||
const Section(
|
||||
title: 'FURNITURE',
|
||||
leftColor: _tomato,
|
||||
rightColor: _mediumPurple,
|
||||
backgroundAsset: 'products/table.png',
|
||||
backgroundAssetPackage: _kGalleryAssetsPackage,
|
||||
details: <SectionDetail>[
|
||||
_seatingDetail,
|
||||
_seatingImageDetail,
|
||||
_seatingDetail,
|
||||
_seatingDetail,
|
||||
_seatingDetail,
|
||||
_seatingDetail,
|
||||
],
|
||||
),
|
||||
const Section(
|
||||
title: 'JEWELRY',
|
||||
leftColor: _mySin,
|
||||
rightColor: _tomato,
|
||||
backgroundAsset: 'products/earrings.png',
|
||||
backgroundAssetPackage: _kGalleryAssetsPackage,
|
||||
details: <SectionDetail>[
|
||||
_decorationDetail,
|
||||
_decorationImageDetail,
|
||||
_decorationDetail,
|
||||
_decorationDetail,
|
||||
_decorationDetail,
|
||||
_decorationDetail,
|
||||
],
|
||||
),
|
||||
const Section(
|
||||
title: 'HEADWEAR',
|
||||
leftColor: Colors.white,
|
||||
rightColor: _tomato,
|
||||
backgroundAsset: 'products/hat.png',
|
||||
backgroundAssetPackage: _kGalleryAssetsPackage,
|
||||
details: <SectionDetail>[
|
||||
_protectionDetail,
|
||||
_protectionImageDetail,
|
||||
_protectionDetail,
|
||||
_protectionDetail,
|
||||
_protectionDetail,
|
||||
_protectionDetail,
|
||||
],
|
||||
),
|
||||
];
|
||||
@ -1,167 +0,0 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'sections.dart';
|
||||
|
||||
const double kSectionIndicatorWidth = 32.0;
|
||||
|
||||
// The card for a single section. Displays the section's gradient and background image.
|
||||
class SectionCard extends StatelessWidget {
|
||||
const SectionCard({ Key key, @required this.section })
|
||||
: assert(section != null),
|
||||
super(key: key);
|
||||
|
||||
final Section section;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Semantics(
|
||||
label: section.title,
|
||||
button: true,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: <Color>[
|
||||
section.leftColor,
|
||||
section.rightColor,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Image.asset(
|
||||
section.backgroundAsset,
|
||||
package: section.backgroundAssetPackage,
|
||||
color: const Color.fromRGBO(255, 255, 255, 0.075),
|
||||
colorBlendMode: BlendMode.modulate,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// The title is rendered with two overlapping text widgets that are vertically
|
||||
// offset a little. It's supposed to look sort-of 3D.
|
||||
class SectionTitle extends StatelessWidget {
|
||||
const SectionTitle({
|
||||
Key key,
|
||||
@required this.section,
|
||||
@required this.scale,
|
||||
@required this.opacity,
|
||||
}) : assert(section != null),
|
||||
assert(scale != null),
|
||||
assert(opacity != null && opacity >= 0.0 && opacity <= 1.0),
|
||||
super(key: key);
|
||||
|
||||
final Section section;
|
||||
final double scale;
|
||||
final double opacity;
|
||||
|
||||
static const TextStyle sectionTitleStyle = TextStyle(
|
||||
fontFamily: 'Raleway',
|
||||
inherit: false,
|
||||
fontSize: 24.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
);
|
||||
|
||||
static final TextStyle sectionTitleShadowStyle = sectionTitleStyle.copyWith(
|
||||
color: const Color(0x19000000),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IgnorePointer(
|
||||
child: Opacity(
|
||||
opacity: opacity,
|
||||
child: Transform(
|
||||
transform: Matrix4.identity()..scale(scale),
|
||||
alignment: Alignment.center,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Positioned(
|
||||
top: 4.0,
|
||||
child: Text(section.title, style: sectionTitleShadowStyle),
|
||||
),
|
||||
Text(section.title, style: sectionTitleStyle),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Small horizontal bar that indicates the selected section.
|
||||
class SectionIndicator extends StatelessWidget {
|
||||
const SectionIndicator({ Key key, this.opacity = 1.0 }) : super(key: key);
|
||||
|
||||
final double opacity;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IgnorePointer(
|
||||
child: Container(
|
||||
width: kSectionIndicatorWidth,
|
||||
height: 3.0,
|
||||
color: Colors.white.withOpacity(opacity),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Display a single SectionDetail.
|
||||
class SectionDetailView extends StatelessWidget {
|
||||
SectionDetailView({ Key key, @required this.detail })
|
||||
: assert(detail != null && detail.imageAsset != null),
|
||||
assert((detail.imageAsset ?? detail.title) != null),
|
||||
super(key: key);
|
||||
|
||||
final SectionDetail detail;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Widget image = DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
image: DecorationImage(
|
||||
image: AssetImage(
|
||||
detail.imageAsset,
|
||||
package: detail.imageAssetPackage,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget item;
|
||||
if (detail.title == null && detail.subtitle == null) {
|
||||
item = Container(
|
||||
height: 240.0,
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: image,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
item = ListTile(
|
||||
title: Text(detail.title),
|
||||
subtitle: Text(detail.subtitle),
|
||||
leading: SizedBox(width: 32.0, height: 32.0, child: image),
|
||||
);
|
||||
}
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(color: Colors.grey.shade200),
|
||||
child: item,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'animation/home.dart';
|
||||
|
||||
class AnimationDemo extends StatelessWidget {
|
||||
const AnimationDemo({Key key}) : super(key: key);
|
||||
|
||||
static const String routeName = '/animation';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => const AnimationDemoHome();
|
||||
}
|
||||
@ -1,270 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'logic.dart';
|
||||
|
||||
class Calculator extends StatefulWidget {
|
||||
const Calculator({Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_CalculatorState createState() => _CalculatorState();
|
||||
}
|
||||
|
||||
class _CalculatorState extends State<Calculator> {
|
||||
/// As the user taps keys we update the current `_expression` and we also
|
||||
/// keep a stack of previous expressions so we can return to earlier states
|
||||
/// when the user hits the DEL key.
|
||||
final List<CalcExpression> _expressionStack = <CalcExpression>[];
|
||||
CalcExpression _expression = CalcExpression.empty();
|
||||
|
||||
// Make `expression` the current expression and push the previous current
|
||||
// expression onto the stack.
|
||||
void pushExpression(CalcExpression expression) {
|
||||
_expressionStack.add(_expression);
|
||||
_expression = expression;
|
||||
}
|
||||
|
||||
/// Pop the top expression off of the stack and make it the current expression.
|
||||
void popCalcExpression() {
|
||||
if (_expressionStack.isNotEmpty) {
|
||||
_expression = _expressionStack.removeLast();
|
||||
} else {
|
||||
_expression = CalcExpression.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/// Set `resultExpression` to the current expression and clear the stack.
|
||||
void setResult(CalcExpression resultExpression) {
|
||||
_expressionStack.clear();
|
||||
_expression = resultExpression;
|
||||
}
|
||||
|
||||
void handleNumberTap(int n) {
|
||||
final CalcExpression expression = _expression.appendDigit(n);
|
||||
if (expression != null) {
|
||||
setState(() {
|
||||
pushExpression(expression);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void handlePointTap() {
|
||||
final CalcExpression expression = _expression.appendPoint();
|
||||
if (expression != null) {
|
||||
setState(() {
|
||||
pushExpression(expression);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void handlePlusTap() {
|
||||
final CalcExpression expression = _expression.appendOperation(Operation.Addition);
|
||||
if (expression != null) {
|
||||
setState(() {
|
||||
pushExpression(expression);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void handleMinusTap() {
|
||||
final CalcExpression expression = _expression.appendMinus();
|
||||
if (expression != null) {
|
||||
setState(() {
|
||||
pushExpression(expression);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void handleMultTap() {
|
||||
final CalcExpression expression = _expression.appendOperation(Operation.Multiplication);
|
||||
if (expression != null) {
|
||||
setState(() {
|
||||
pushExpression(expression);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void handleDivTap() {
|
||||
final CalcExpression expression = _expression.appendOperation(Operation.Division);
|
||||
if (expression != null) {
|
||||
setState(() {
|
||||
pushExpression(expression);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void handleEqualsTap() {
|
||||
final CalcExpression resultExpression = _expression.computeResult();
|
||||
if (resultExpression != null) {
|
||||
setState(() {
|
||||
setResult(resultExpression);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void handleDelTap() {
|
||||
setState(() {
|
||||
popCalcExpression();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).canvasColor,
|
||||
elevation: 0.0,
|
||||
),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
// Give the key-pad 3/5 of the vertical space and the display 2/5.
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: CalcDisplay(content: _expression.toString()),
|
||||
),
|
||||
const Divider(height: 1.0),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: KeyPad(calcState: this),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CalcDisplay extends StatelessWidget {
|
||||
const CalcDisplay({ this.content });
|
||||
|
||||
final String content;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Text(
|
||||
content,
|
||||
style: const TextStyle(fontSize: 24.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class KeyPad extends StatelessWidget {
|
||||
const KeyPad({ this.calcState });
|
||||
|
||||
final _CalculatorState calcState;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData themeData = ThemeData(
|
||||
primarySwatch: Colors.purple,
|
||||
brightness: Brightness.dark,
|
||||
platform: Theme.of(context).platform,
|
||||
);
|
||||
return Theme(
|
||||
data: themeData,
|
||||
child: Material(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
// We set flex equal to the number of columns so that the main keypad
|
||||
// and the op keypad have sizes proportional to their number of
|
||||
// columns.
|
||||
flex: 3,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
KeyRow(<Widget>[
|
||||
NumberKey(7, calcState),
|
||||
NumberKey(8, calcState),
|
||||
NumberKey(9, calcState),
|
||||
]),
|
||||
KeyRow(<Widget>[
|
||||
NumberKey(4, calcState),
|
||||
NumberKey(5, calcState),
|
||||
NumberKey(6, calcState),
|
||||
]),
|
||||
KeyRow(<Widget>[
|
||||
NumberKey(1, calcState),
|
||||
NumberKey(2, calcState),
|
||||
NumberKey(3, calcState),
|
||||
]),
|
||||
KeyRow(<Widget>[
|
||||
CalcKey('.', calcState.handlePointTap),
|
||||
NumberKey(0, calcState),
|
||||
CalcKey('=', calcState.handleEqualsTap),
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Material(
|
||||
color: themeData.backgroundColor,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
CalcKey('\u232B', calcState.handleDelTap),
|
||||
CalcKey('\u00F7', calcState.handleDivTap),
|
||||
CalcKey('\u00D7', calcState.handleMultTap),
|
||||
CalcKey('-', calcState.handleMinusTap),
|
||||
CalcKey('+', calcState.handlePlusTap),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class KeyRow extends StatelessWidget {
|
||||
const KeyRow(this.keys);
|
||||
|
||||
final List<Widget> keys;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: keys,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CalcKey extends StatelessWidget {
|
||||
const CalcKey(this.text, this.onTap);
|
||||
|
||||
final String text;
|
||||
final GestureTapCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Orientation orientation = MediaQuery.of(context).orientation;
|
||||
return Expanded(
|
||||
child: InkResponse(
|
||||
onTap: onTap,
|
||||
child: Center(
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: (orientation == Orientation.portrait) ? 32.0 : 24.0
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NumberKey extends CalcKey {
|
||||
NumberKey(int value, _CalculatorState calcState)
|
||||
: super('$value', () {
|
||||
calcState.handleNumberTap(value);
|
||||
});
|
||||
}
|
||||
@ -1,343 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
/// A token that composes an expression. There are several kinds of tokens
|
||||
/// that represent arithmetic operation symbols, numbers and pieces of numbers.
|
||||
/// We need to represent pieces of numbers because the user may have only
|
||||
/// entered a partial expression so far.
|
||||
class ExpressionToken {
|
||||
ExpressionToken(this.stringRep);
|
||||
|
||||
final String stringRep;
|
||||
|
||||
@override
|
||||
String toString() => stringRep;
|
||||
}
|
||||
|
||||
/// A token that represents a number.
|
||||
class NumberToken extends ExpressionToken {
|
||||
NumberToken(String stringRep, this.number) : super(stringRep);
|
||||
|
||||
NumberToken.fromNumber(num number) : this('$number', number);
|
||||
|
||||
final num number;
|
||||
}
|
||||
|
||||
/// A token that represents an integer.
|
||||
class IntToken extends NumberToken {
|
||||
IntToken(String stringRep) : super(stringRep, int.parse(stringRep));
|
||||
}
|
||||
|
||||
/// A token that represents a floating point number.
|
||||
class FloatToken extends NumberToken {
|
||||
FloatToken(String stringRep) : super(stringRep, _parse(stringRep));
|
||||
|
||||
static double _parse(String stringRep) {
|
||||
String toParse = stringRep;
|
||||
if (toParse.startsWith('.'))
|
||||
toParse = '0' + toParse;
|
||||
if (toParse.endsWith('.'))
|
||||
toParse = toParse + '0';
|
||||
return double.parse(toParse);
|
||||
}
|
||||
}
|
||||
|
||||
/// A token that represents a number that is the result of a computation.
|
||||
class ResultToken extends NumberToken {
|
||||
ResultToken(num number) : super.fromNumber(round(number));
|
||||
|
||||
/// rounds `number` to 14 digits of precision. A double precision
|
||||
/// floating point number is guaranteed to have at least this many
|
||||
/// decimal digits of precision.
|
||||
static num round(num number) {
|
||||
if (number is int)
|
||||
return number;
|
||||
return double.parse(number.toStringAsPrecision(14));
|
||||
}
|
||||
}
|
||||
|
||||
/// A token that represents the unary minus prefix.
|
||||
class LeadingNegToken extends ExpressionToken {
|
||||
LeadingNegToken() : super('-');
|
||||
}
|
||||
|
||||
enum Operation { Addition, Subtraction, Multiplication, Division }
|
||||
|
||||
/// A token that represents an arithmetic operation symbol.
|
||||
class OperationToken extends ExpressionToken {
|
||||
OperationToken(this.operation)
|
||||
: super(opString(operation));
|
||||
|
||||
Operation operation;
|
||||
|
||||
static String opString(Operation operation) {
|
||||
switch (operation) {
|
||||
case Operation.Addition:
|
||||
return ' + ';
|
||||
case Operation.Subtraction:
|
||||
return ' - ';
|
||||
case Operation.Multiplication:
|
||||
return ' \u00D7 ';
|
||||
case Operation.Division:
|
||||
return ' \u00F7 ';
|
||||
}
|
||||
assert(operation != null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// As the user taps different keys the current expression can be in one
|
||||
/// of several states.
|
||||
enum ExpressionState {
|
||||
/// The expression is empty or an operation symbol was just entered.
|
||||
/// A new number must be started now.
|
||||
Start,
|
||||
|
||||
/// A minus sign was entered as a leading negative prefix.
|
||||
LeadingNeg,
|
||||
|
||||
/// We are in the midst of a number without a point.
|
||||
Number,
|
||||
|
||||
/// A point was just entered.
|
||||
Point,
|
||||
|
||||
/// We are in the midst of a number with a point.
|
||||
NumberWithPoint,
|
||||
|
||||
/// A result is being displayed
|
||||
Result,
|
||||
}
|
||||
|
||||
/// An expression that can be displayed in a calculator. It is the result
|
||||
/// of a sequence of user entries. It is represented by a sequence of tokens.
|
||||
///
|
||||
/// The tokens are not in one to one correspondence with the key taps because we
|
||||
/// use one token per number, not one token per digit. A [CalcExpression] is
|
||||
/// immutable. The `append*` methods return a new [CalcExpression] that
|
||||
/// represents the appropriate expression when one additional key tap occurs.
|
||||
class CalcExpression {
|
||||
CalcExpression(this._list, this.state);
|
||||
|
||||
CalcExpression.empty()
|
||||
: this(<ExpressionToken>[], ExpressionState.Start);
|
||||
|
||||
CalcExpression.result(FloatToken result)
|
||||
: _list = <ExpressionToken>[],
|
||||
state = ExpressionState.Result {
|
||||
_list.add(result);
|
||||
}
|
||||
|
||||
/// The tokens comprising the expression.
|
||||
final List<ExpressionToken> _list;
|
||||
/// The state of the expression.
|
||||
final ExpressionState state;
|
||||
|
||||
/// The string representation of the expression. This will be displayed
|
||||
/// in the calculator's display panel.
|
||||
@override
|
||||
String toString() {
|
||||
final StringBuffer buffer = StringBuffer('');
|
||||
buffer.writeAll(_list);
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// Append a digit to the current expression and return a new expression
|
||||
/// representing the result. Returns null to indicate that it is not legal
|
||||
/// to append a digit in the current state.
|
||||
CalcExpression appendDigit(int digit) {
|
||||
ExpressionState newState = ExpressionState.Number;
|
||||
ExpressionToken newToken;
|
||||
final List<ExpressionToken> outList = _list.toList();
|
||||
switch (state) {
|
||||
case ExpressionState.Start:
|
||||
// Start a new number with digit.
|
||||
newToken = IntToken('$digit');
|
||||
break;
|
||||
case ExpressionState.LeadingNeg:
|
||||
// Replace the leading neg with a negative number starting with digit.
|
||||
outList.removeLast();
|
||||
newToken = IntToken('-$digit');
|
||||
break;
|
||||
case ExpressionState.Number:
|
||||
final ExpressionToken last = outList.removeLast();
|
||||
newToken = IntToken('${last.stringRep}$digit');
|
||||
break;
|
||||
case ExpressionState.Point:
|
||||
case ExpressionState.NumberWithPoint:
|
||||
final ExpressionToken last = outList.removeLast();
|
||||
newState = ExpressionState.NumberWithPoint;
|
||||
newToken = FloatToken('${last.stringRep}$digit');
|
||||
break;
|
||||
case ExpressionState.Result:
|
||||
// Cannot enter a number now
|
||||
return null;
|
||||
}
|
||||
outList.add(newToken);
|
||||
return CalcExpression(outList, newState);
|
||||
}
|
||||
|
||||
/// Append a point to the current expression and return a new expression
|
||||
/// representing the result. Returns null to indicate that it is not legal
|
||||
/// to append a point in the current state.
|
||||
CalcExpression appendPoint() {
|
||||
ExpressionToken newToken;
|
||||
final List<ExpressionToken> outList = _list.toList();
|
||||
switch (state) {
|
||||
case ExpressionState.Start:
|
||||
newToken = FloatToken('.');
|
||||
break;
|
||||
case ExpressionState.LeadingNeg:
|
||||
case ExpressionState.Number:
|
||||
final ExpressionToken last = outList.removeLast();
|
||||
newToken = FloatToken(last.stringRep + '.');
|
||||
break;
|
||||
case ExpressionState.Point:
|
||||
case ExpressionState.NumberWithPoint:
|
||||
case ExpressionState.Result:
|
||||
// Cannot enter a point now
|
||||
return null;
|
||||
}
|
||||
outList.add(newToken);
|
||||
return CalcExpression(outList, ExpressionState.Point);
|
||||
}
|
||||
|
||||
/// Append an operation symbol to the current expression and return a new
|
||||
/// expression representing the result. Returns null to indicate that it is not
|
||||
/// legal to append an operation symbol in the current state.
|
||||
CalcExpression appendOperation(Operation op) {
|
||||
switch (state) {
|
||||
case ExpressionState.Start:
|
||||
case ExpressionState.LeadingNeg:
|
||||
case ExpressionState.Point:
|
||||
// Cannot enter operation now.
|
||||
return null;
|
||||
case ExpressionState.Number:
|
||||
case ExpressionState.NumberWithPoint:
|
||||
case ExpressionState.Result:
|
||||
break;
|
||||
}
|
||||
final List<ExpressionToken> outList = _list.toList();
|
||||
outList.add(OperationToken(op));
|
||||
return CalcExpression(outList, ExpressionState.Start);
|
||||
}
|
||||
|
||||
/// Append a leading minus sign to the current expression and return a new
|
||||
/// expression representing the result. Returns null to indicate that it is not
|
||||
/// legal to append a leading minus sign in the current state.
|
||||
CalcExpression appendLeadingNeg() {
|
||||
switch (state) {
|
||||
case ExpressionState.Start:
|
||||
break;
|
||||
case ExpressionState.LeadingNeg:
|
||||
case ExpressionState.Point:
|
||||
case ExpressionState.Number:
|
||||
case ExpressionState.NumberWithPoint:
|
||||
case ExpressionState.Result:
|
||||
// Cannot enter leading neg now.
|
||||
return null;
|
||||
}
|
||||
final List<ExpressionToken> outList = _list.toList();
|
||||
outList.add(LeadingNegToken());
|
||||
return CalcExpression(outList, ExpressionState.LeadingNeg);
|
||||
}
|
||||
|
||||
/// Append a minus sign to the current expression and return a new expression
|
||||
/// representing the result. Returns null to indicate that it is not legal
|
||||
/// to append a minus sign in the current state. Depending on the current
|
||||
/// state the minus sign will be interpreted as either a leading negative
|
||||
/// sign or a subtraction operation.
|
||||
CalcExpression appendMinus() {
|
||||
switch (state) {
|
||||
case ExpressionState.Start:
|
||||
return appendLeadingNeg();
|
||||
case ExpressionState.LeadingNeg:
|
||||
case ExpressionState.Point:
|
||||
case ExpressionState.Number:
|
||||
case ExpressionState.NumberWithPoint:
|
||||
case ExpressionState.Result:
|
||||
return appendOperation(Operation.Subtraction);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the result of the current expression and returns a new
|
||||
/// ResultExpression containing the result. Returns null to indicate that
|
||||
/// it is not legal to compute a result in the current state.
|
||||
CalcExpression computeResult() {
|
||||
switch (state) {
|
||||
case ExpressionState.Start:
|
||||
case ExpressionState.LeadingNeg:
|
||||
case ExpressionState.Point:
|
||||
case ExpressionState.Result:
|
||||
// Cannot compute result now.
|
||||
return null;
|
||||
case ExpressionState.Number:
|
||||
case ExpressionState.NumberWithPoint:
|
||||
break;
|
||||
}
|
||||
|
||||
// We make a copy of _list because CalcExpressions are supposed to
|
||||
// be immutable.
|
||||
final List<ExpressionToken> list = _list.toList();
|
||||
// We obey order-of-operations by computing the sum of the 'terms',
|
||||
// where a "term" is defined to be a sequence of numbers separated by
|
||||
// multiplication or division symbols.
|
||||
num currentTermValue = removeNextTerm(list);
|
||||
while (list.isNotEmpty) {
|
||||
final OperationToken opToken = list.removeAt(0);
|
||||
final num nextTermValue = removeNextTerm(list);
|
||||
switch (opToken.operation) {
|
||||
case Operation.Addition:
|
||||
currentTermValue += nextTermValue;
|
||||
break;
|
||||
case Operation.Subtraction:
|
||||
currentTermValue -= nextTermValue;
|
||||
break;
|
||||
case Operation.Multiplication:
|
||||
case Operation.Division:
|
||||
// Logic error.
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
final List<ExpressionToken> outList = <ExpressionToken>[
|
||||
ResultToken(currentTermValue),
|
||||
];
|
||||
return CalcExpression(outList, ExpressionState.Result);
|
||||
}
|
||||
|
||||
/// Removes the next "term" from `list` and returns its numeric value.
|
||||
/// A "term" is a sequence of number tokens separated by multiplication
|
||||
/// and division symbols.
|
||||
static num removeNextTerm(List<ExpressionToken> list) {
|
||||
assert(list != null && list.isNotEmpty);
|
||||
final NumberToken firstNumToken = list.removeAt(0);
|
||||
num currentValue = firstNumToken.number;
|
||||
while (list.isNotEmpty) {
|
||||
bool isDivision = false;
|
||||
final OperationToken nextOpToken = list.first;
|
||||
switch (nextOpToken.operation) {
|
||||
case Operation.Addition:
|
||||
case Operation.Subtraction:
|
||||
// We have reached the end of the current term
|
||||
return currentValue;
|
||||
case Operation.Multiplication:
|
||||
break;
|
||||
case Operation.Division:
|
||||
isDivision = true;
|
||||
}
|
||||
// Remove the operation token.
|
||||
list.removeAt(0);
|
||||
// Remove the next number token.
|
||||
final NumberToken nextNumToken = list.removeAt(0);
|
||||
final num nextNumber = nextNumToken.number;
|
||||
if (isDivision)
|
||||
currentValue /= nextNumber;
|
||||
else
|
||||
currentValue *= nextNumber;
|
||||
}
|
||||
return currentValue;
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'calculator/home.dart';
|
||||
|
||||
class CalculatorDemo extends StatelessWidget {
|
||||
const CalculatorDemo({Key key}) : super(key: key);
|
||||
|
||||
static const String routeName = '/calculator';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => const Calculator();
|
||||
}
|
||||
@ -1,149 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const double kColorItemHeight = 48.0;
|
||||
|
||||
class Palette {
|
||||
Palette({ this.name, this.primary, this.accent, this.threshold = 900});
|
||||
|
||||
final String name;
|
||||
final MaterialColor primary;
|
||||
final MaterialAccentColor accent;
|
||||
final int threshold; // titles for indices > threshold are white, otherwise black
|
||||
|
||||
bool get isValid => name != null && primary != null && threshold != null;
|
||||
}
|
||||
|
||||
final List<Palette> allPalettes = <Palette>[
|
||||
Palette(name: 'RED', primary: Colors.red, accent: Colors.redAccent, threshold: 300),
|
||||
Palette(name: 'PINK', primary: Colors.pink, accent: Colors.pinkAccent, threshold: 200),
|
||||
Palette(name: 'PURPLE', primary: Colors.purple, accent: Colors.purpleAccent, threshold: 200),
|
||||
Palette(name: 'DEEP PURPLE', primary: Colors.deepPurple, accent: Colors.deepPurpleAccent, threshold: 200),
|
||||
Palette(name: 'INDIGO', primary: Colors.indigo, accent: Colors.indigoAccent, threshold: 200),
|
||||
Palette(name: 'BLUE', primary: Colors.blue, accent: Colors.blueAccent, threshold: 400),
|
||||
Palette(name: 'LIGHT BLUE', primary: Colors.lightBlue, accent: Colors.lightBlueAccent, threshold: 500),
|
||||
Palette(name: 'CYAN', primary: Colors.cyan, accent: Colors.cyanAccent, threshold: 600),
|
||||
Palette(name: 'TEAL', primary: Colors.teal, accent: Colors.tealAccent, threshold: 400),
|
||||
Palette(name: 'GREEN', primary: Colors.green, accent: Colors.greenAccent, threshold: 500),
|
||||
Palette(name: 'LIGHT GREEN', primary: Colors.lightGreen, accent: Colors.lightGreenAccent, threshold: 600),
|
||||
Palette(name: 'LIME', primary: Colors.lime, accent: Colors.limeAccent, threshold: 800),
|
||||
Palette(name: 'YELLOW', primary: Colors.yellow, accent: Colors.yellowAccent),
|
||||
Palette(name: 'AMBER', primary: Colors.amber, accent: Colors.amberAccent),
|
||||
Palette(name: 'ORANGE', primary: Colors.orange, accent: Colors.orangeAccent, threshold: 700),
|
||||
Palette(name: 'DEEP ORANGE', primary: Colors.deepOrange, accent: Colors.deepOrangeAccent, threshold: 400),
|
||||
Palette(name: 'BROWN', primary: Colors.brown, threshold: 200),
|
||||
Palette(name: 'GREY', primary: Colors.grey, threshold: 500),
|
||||
Palette(name: 'BLUE GREY', primary: Colors.blueGrey, threshold: 500),
|
||||
];
|
||||
|
||||
|
||||
class ColorItem extends StatelessWidget {
|
||||
const ColorItem({
|
||||
Key key,
|
||||
@required this.index,
|
||||
@required this.color,
|
||||
this.prefix = '',
|
||||
}) : assert(index != null),
|
||||
assert(color != null),
|
||||
assert(prefix != null),
|
||||
super(key: key);
|
||||
|
||||
final int index;
|
||||
final Color color;
|
||||
final String prefix;
|
||||
|
||||
String colorString() => "#${color.value.toRadixString(16).padLeft(8, '0').toUpperCase()}";
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Semantics(
|
||||
container: true,
|
||||
child: Container(
|
||||
height: kColorItemHeight,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
color: color,
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text('$prefix$index'),
|
||||
Text(colorString()),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PaletteTabView extends StatelessWidget {
|
||||
PaletteTabView({
|
||||
Key key,
|
||||
@required this.colors,
|
||||
}) : assert(colors != null && colors.isValid),
|
||||
super(key: key);
|
||||
|
||||
final Palette colors;
|
||||
|
||||
static const List<int> primaryKeys = <int>[50, 100, 200, 300, 400, 500, 600, 700, 800, 900];
|
||||
static const List<int> accentKeys = <int>[100, 200, 400, 700];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TextTheme textTheme = Theme.of(context).textTheme;
|
||||
final TextStyle whiteTextStyle = textTheme.body1.copyWith(color: Colors.white);
|
||||
final TextStyle blackTextStyle = textTheme.body1.copyWith(color: Colors.black);
|
||||
return Scrollbar(
|
||||
child: ListView(
|
||||
itemExtent: kColorItemHeight,
|
||||
children: <Widget>[
|
||||
...primaryKeys.map<Widget>((int index) {
|
||||
return DefaultTextStyle(
|
||||
style: index > colors.threshold ? whiteTextStyle : blackTextStyle,
|
||||
child: ColorItem(index: index, color: colors.primary[index]),
|
||||
);
|
||||
}),
|
||||
if (colors.accent != null)
|
||||
...accentKeys.map<Widget>((int index) {
|
||||
return DefaultTextStyle(
|
||||
style: index > colors.threshold ? whiteTextStyle : blackTextStyle,
|
||||
child: ColorItem(index: index, color: colors.accent[index], prefix: 'A'),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ColorsDemo extends StatelessWidget {
|
||||
static const String routeName = '/colors';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: allPalettes.length,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: 0.0,
|
||||
title: const Text('Colors'),
|
||||
bottom: TabBar(
|
||||
isScrollable: true,
|
||||
tabs: allPalettes.map<Widget>((Palette swatch) => Tab(text: swatch.name)).toList(),
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
children: allPalettes.map<Widget>((Palette colors) {
|
||||
return PaletteTabView(colors: colors);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,345 +0,0 @@
|
||||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class _ContactCategory extends StatelessWidget {
|
||||
const _ContactCategory({ Key key, this.icon, this.children }) : super(key: key);
|
||||
|
||||
final IconData icon;
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData themeData = Theme.of(context);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: themeData.dividerColor))
|
||||
),
|
||||
child: DefaultTextStyle(
|
||||
style: Theme.of(context).textTheme.subhead,
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24.0),
|
||||
width: 72.0,
|
||||
child: Icon(icon, color: themeData.primaryColor),
|
||||
),
|
||||
Expanded(child: Column(children: children)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ContactItem extends StatelessWidget {
|
||||
const _ContactItem({ Key key, this.icon, this.lines, this.tooltip, this.onPressed })
|
||||
: assert(lines.length > 1),
|
||||
super(key: key);
|
||||
|
||||
final IconData icon;
|
||||
final List<String> lines;
|
||||
final String tooltip;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData themeData = Theme.of(context);
|
||||
return MergeSemantics(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
...lines.sublist(0, lines.length - 1).map<Widget>((String line) => Text(line)),
|
||||
Text(lines.last, style: themeData.textTheme.caption),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (icon != null)
|
||||
SizedBox(
|
||||
width: 72.0,
|
||||
child: IconButton(
|
||||
icon: Icon(icon),
|
||||
color: themeData.primaryColor,
|
||||
onPressed: onPressed,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ContactsDemo extends StatefulWidget {
|
||||
static const String routeName = '/contacts';
|
||||
|
||||
@override
|
||||
ContactsDemoState createState() => ContactsDemoState();
|
||||
}
|
||||
|
||||
enum AppBarBehavior { normal, pinned, floating, snapping }
|
||||
|
||||
class ContactsDemoState extends State<ContactsDemo> {
|
||||
static final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
final double _appBarHeight = 256.0;
|
||||
|
||||
AppBarBehavior _appBarBehavior = AppBarBehavior.pinned;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: ThemeData(
|
||||
brightness: Brightness.light,
|
||||
primarySwatch: Colors.indigo,
|
||||
platform: Theme.of(context).platform,
|
||||
),
|
||||
child: Scaffold(
|
||||
key: _scaffoldKey,
|
||||
body: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
SliverAppBar(
|
||||
expandedHeight: _appBarHeight,
|
||||
pinned: _appBarBehavior == AppBarBehavior.pinned,
|
||||
floating: _appBarBehavior == AppBarBehavior.floating || _appBarBehavior == AppBarBehavior.snapping,
|
||||
snap: _appBarBehavior == AppBarBehavior.snapping,
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.create),
|
||||
tooltip: 'Edit',
|
||||
onPressed: () {
|
||||
_scaffoldKey.currentState.showSnackBar(const SnackBar(
|
||||
content: Text("Editing isn't supported in this screen."),
|
||||
));
|
||||
},
|
||||
),
|
||||
PopupMenuButton<AppBarBehavior>(
|
||||
onSelected: (AppBarBehavior value) {
|
||||
setState(() {
|
||||
_appBarBehavior = value;
|
||||
});
|
||||
},
|
||||
itemBuilder: (BuildContext context) => <PopupMenuItem<AppBarBehavior>>[
|
||||
const PopupMenuItem<AppBarBehavior>(
|
||||
value: AppBarBehavior.normal,
|
||||
child: Text('App bar scrolls away'),
|
||||
),
|
||||
const PopupMenuItem<AppBarBehavior>(
|
||||
value: AppBarBehavior.pinned,
|
||||
child: Text('App bar stays put'),
|
||||
),
|
||||
const PopupMenuItem<AppBarBehavior>(
|
||||
value: AppBarBehavior.floating,
|
||||
child: Text('App bar floats'),
|
||||
),
|
||||
const PopupMenuItem<AppBarBehavior>(
|
||||
value: AppBarBehavior.snapping,
|
||||
child: Text('App bar snaps'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
title: const Text('Ali Connors'),
|
||||
background: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: <Widget>[
|
||||
Image.asset(
|
||||
'people/ali_landscape.png',
|
||||
package: 'flutter_gallery_assets',
|
||||
fit: BoxFit.cover,
|
||||
height: _appBarHeight,
|
||||
),
|
||||
// This gradient ensures that the toolbar icons are distinct
|
||||
// against the background image.
|
||||
const DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment(0.0, -1.0),
|
||||
end: Alignment(0.0, -0.4),
|
||||
colors: <Color>[Color(0x60000000), Color(0x00000000)],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate(<Widget>[
|
||||
AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.dark,
|
||||
child: _ContactCategory(
|
||||
icon: Icons.call,
|
||||
children: <Widget>[
|
||||
_ContactItem(
|
||||
icon: Icons.message,
|
||||
tooltip: 'Send message',
|
||||
onPressed: () {
|
||||
_scaffoldKey.currentState.showSnackBar(const SnackBar(
|
||||
content: Text('Pretend that this opened your SMS application.'),
|
||||
));
|
||||
},
|
||||
lines: const <String>[
|
||||
'(650) 555-1234',
|
||||
'Mobile',
|
||||
],
|
||||
),
|
||||
_ContactItem(
|
||||
icon: Icons.message,
|
||||
tooltip: 'Send message',
|
||||
onPressed: () {
|
||||
_scaffoldKey.currentState.showSnackBar(const SnackBar(
|
||||
content: Text('A messaging app appears.'),
|
||||
));
|
||||
},
|
||||
lines: const <String>[
|
||||
'(323) 555-6789',
|
||||
'Work',
|
||||
],
|
||||
),
|
||||
_ContactItem(
|
||||
icon: Icons.message,
|
||||
tooltip: 'Send message',
|
||||
onPressed: () {
|
||||
_scaffoldKey.currentState.showSnackBar(const SnackBar(
|
||||
content: Text('Imagine if you will, a messaging application.'),
|
||||
));
|
||||
},
|
||||
lines: const <String>[
|
||||
'(650) 555-6789',
|
||||
'Home',
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_ContactCategory(
|
||||
icon: Icons.contact_mail,
|
||||
children: <Widget>[
|
||||
_ContactItem(
|
||||
icon: Icons.email,
|
||||
tooltip: 'Send personal e-mail',
|
||||
onPressed: () {
|
||||
_scaffoldKey.currentState.showSnackBar(const SnackBar(
|
||||
content: Text('Here, your e-mail application would open.'),
|
||||
));
|
||||
},
|
||||
lines: const <String>[
|
||||
'ali_connors@example.com',
|
||||
'Personal',
|
||||
],
|
||||
),
|
||||
_ContactItem(
|
||||
icon: Icons.email,
|
||||
tooltip: 'Send work e-mail',
|
||||
onPressed: () {
|
||||
_scaffoldKey.currentState.showSnackBar(const SnackBar(
|
||||
content: Text('Summon your favorite e-mail application here.'),
|
||||
));
|
||||
},
|
||||
lines: const <String>[
|
||||
'aliconnors@example.com',
|
||||
'Work',
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
_ContactCategory(
|
||||
icon: Icons.location_on,
|
||||
children: <Widget>[
|
||||
_ContactItem(
|
||||
icon: Icons.map,
|
||||
tooltip: 'Open map',
|
||||
onPressed: () {
|
||||
_scaffoldKey.currentState.showSnackBar(const SnackBar(
|
||||
content: Text('This would show a map of San Francisco.'),
|
||||
));
|
||||
},
|
||||
lines: const <String>[
|
||||
'2000 Main Street',
|
||||
'San Francisco, CA',
|
||||
'Home',
|
||||
],
|
||||
),
|
||||
_ContactItem(
|
||||
icon: Icons.map,
|
||||
tooltip: 'Open map',
|
||||
onPressed: () {
|
||||
_scaffoldKey.currentState.showSnackBar(const SnackBar(
|
||||
content: Text('This would show a map of Mountain View.'),
|
||||
));
|
||||
},
|
||||
lines: const <String>[
|
||||
'1600 Amphitheater Parkway',
|
||||
'Mountain View, CA',
|
||||
'Work',
|
||||
],
|
||||
),
|
||||
_ContactItem(
|
||||
icon: Icons.map,
|
||||
tooltip: 'Open map',
|
||||
onPressed: () {
|
||||
_scaffoldKey.currentState.showSnackBar(const SnackBar(
|
||||
content: Text('This would also show a map, if this was not a demo.'),
|
||||
));
|
||||
},
|
||||
lines: const <String>[
|
||||
'126 Severyns Ave',
|
||||
'Mountain View, CA',
|
||||
'Jet Travel',
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
_ContactCategory(
|
||||
icon: Icons.today,
|
||||
children: <Widget>[
|
||||
_ContactItem(
|
||||
lines: const <String>[
|
||||
'Birthday',
|
||||
'January 9th, 1989',
|
||||
],
|
||||
),
|
||||
_ContactItem(
|
||||
lines: const <String>[
|
||||
'Wedding anniversary',
|
||||
'June 21st, 2014',
|
||||
],
|
||||
),
|
||||
_ContactItem(
|
||||
lines: const <String>[
|
||||
'First day in office',
|
||||
'January 20th, 2015',
|
||||
],
|
||||
),
|
||||
_ContactItem(
|
||||
lines: const <String>[
|
||||
'Last day in office',
|
||||
'August 9th, 2018',
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
export 'cupertino_activity_indicator_demo.dart';
|
||||
export 'cupertino_alert_demo.dart';
|
||||
export 'cupertino_buttons_demo.dart';
|
||||
export 'cupertino_navigation_demo.dart';
|
||||
export 'cupertino_picker_demo.dart';
|
||||
export 'cupertino_refresh_demo.dart';
|
||||
export 'cupertino_segmented_control_demo.dart';
|
||||
export 'cupertino_slider_demo.dart';
|
||||
export 'cupertino_switch_demo.dart';
|
||||
export 'cupertino_text_field_demo.dart';
|
||||
@ -1,28 +0,0 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class CupertinoProgressIndicatorDemo extends StatelessWidget {
|
||||
static const String routeName = '/cupertino/progress_indicator';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
// We're specifying a back label here because the previous page is a
|
||||
// Material page. CupertinoPageRoutes could auto-populate these back
|
||||
// labels.
|
||||
previousPageTitle: 'Cupertino',
|
||||
middle: const Text('Activity Indicator'),
|
||||
trailing: CupertinoDemoDocumentationButton(routeName),
|
||||
),
|
||||
child: const Center(
|
||||
child: CupertinoActivityIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,262 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class CupertinoAlertDemo extends StatefulWidget {
|
||||
static const String routeName = '/cupertino/alert';
|
||||
|
||||
@override
|
||||
_CupertinoAlertDemoState createState() => _CupertinoAlertDemoState();
|
||||
}
|
||||
|
||||
class _CupertinoAlertDemoState extends State<CupertinoAlertDemo> {
|
||||
String lastSelectedValue;
|
||||
|
||||
void showDemoDialog({BuildContext context, Widget child}) {
|
||||
showCupertinoDialog<String>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => child,
|
||||
).then((String value) {
|
||||
if (value != null) {
|
||||
setState(() { lastSelectedValue = value; });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void showDemoActionSheet({BuildContext context, Widget child}) {
|
||||
showCupertinoModalPopup<String>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => child,
|
||||
).then((String value) {
|
||||
if (value != null) {
|
||||
setState(() { lastSelectedValue = value; });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: const Text('Alerts'),
|
||||
// We're specifying a back label here because the previous page is a
|
||||
// Material page. CupertinoPageRoutes could auto-populate these back
|
||||
// labels.
|
||||
previousPageTitle: 'Cupertino',
|
||||
trailing: CupertinoDemoDocumentationButton(CupertinoAlertDemo.routeName),
|
||||
),
|
||||
child: DefaultTextStyle(
|
||||
style: CupertinoTheme.of(context).textTheme.textStyle,
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: <Widget>[
|
||||
CupertinoScrollbar(
|
||||
child: ListView(
|
||||
// Add more padding to the normal safe area.
|
||||
padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 72.0)
|
||||
+ MediaQuery.of(context).padding,
|
||||
children: <Widget>[
|
||||
CupertinoButton.filled(
|
||||
child: const Text('Alert'),
|
||||
onPressed: () => _onAlertPress(context),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(8.0)),
|
||||
CupertinoButton.filled(
|
||||
child: const Text('Alert with Title'),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 36.0),
|
||||
onPressed: () => _onAlertWithTitlePress(context),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(8.0)),
|
||||
CupertinoButton.filled(
|
||||
child: const Text('Alert with Buttons'),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 36.0),
|
||||
onPressed: () => _onAlertWithButtonsPress(context),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(8.0)),
|
||||
CupertinoButton.filled(
|
||||
child: const Text('Alert Buttons Only'),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 36.0),
|
||||
onPressed: () {
|
||||
showDemoDialog(
|
||||
context: context,
|
||||
child: const CupertinoDessertDialog(),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(8.0)),
|
||||
CupertinoButton.filled(
|
||||
child: const Text('Action Sheet'),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 36.0),
|
||||
onPressed: () => _onActionSheetPress(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (lastSelectedValue != null)
|
||||
Positioned(
|
||||
bottom: 32.0,
|
||||
child: Text('You selected: $lastSelectedValue'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onAlertPress(BuildContext context) {
|
||||
showDemoDialog(
|
||||
context: context,
|
||||
child: CupertinoAlertDialog(
|
||||
title: const Text('Discard draft?'),
|
||||
actions: <Widget>[
|
||||
CupertinoDialogAction(
|
||||
child: const Text('Discard'),
|
||||
isDestructiveAction: true,
|
||||
onPressed: () => Navigator.pop(context, 'Discard'),
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
child: const Text('Cancel'),
|
||||
isDefaultAction: true,
|
||||
onPressed: () => Navigator.pop(context, 'Cancel'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onAlertWithTitlePress(BuildContext context) {
|
||||
showDemoDialog(
|
||||
context: context,
|
||||
child: CupertinoAlertDialog(
|
||||
title: const Text('Allow "Maps" to access your location while you are using the app?'),
|
||||
content: const Text('Your current location will be displayed on the map and used '
|
||||
'for directions, nearby search results, and estimated travel times.'),
|
||||
actions: <Widget>[
|
||||
CupertinoDialogAction(
|
||||
child: const Text('Don\'t Allow'),
|
||||
onPressed: () => Navigator.pop(context, 'Disallow'),
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
child: const Text('Allow'),
|
||||
onPressed: () => Navigator.pop(context, 'Allow'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onAlertWithButtonsPress(BuildContext context) {
|
||||
showDemoDialog(
|
||||
context: context,
|
||||
child: const CupertinoDessertDialog(
|
||||
title: Text('Select Favorite Dessert'),
|
||||
content: Text('Please select your favorite type of dessert from the '
|
||||
'list below. Your selection will be used to customize the suggested '
|
||||
'list of eateries in your area.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onActionSheetPress(BuildContext context) {
|
||||
showDemoActionSheet(
|
||||
context: context,
|
||||
child: CupertinoActionSheet(
|
||||
title: const Text('Favorite Dessert'),
|
||||
message: const Text('Please select the best dessert from the options below.'),
|
||||
actions: <Widget>[
|
||||
CupertinoActionSheetAction(
|
||||
child: const Text('Profiteroles'),
|
||||
onPressed: () => Navigator.pop(context, 'Profiteroles'),
|
||||
),
|
||||
CupertinoActionSheetAction(
|
||||
child: const Text('Cannolis'),
|
||||
onPressed: () => Navigator.pop(context, 'Cannolis'),
|
||||
),
|
||||
CupertinoActionSheetAction(
|
||||
child: const Text('Trifle'),
|
||||
onPressed: () => Navigator.pop(context, 'Trifle'),
|
||||
),
|
||||
],
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
child: const Text('Cancel'),
|
||||
isDefaultAction: true,
|
||||
onPressed: () => Navigator.pop(context, 'Cancel'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CupertinoDessertDialog extends StatelessWidget {
|
||||
const CupertinoDessertDialog({Key key, this.title, this.content}) : super(key: key);
|
||||
|
||||
final Widget title;
|
||||
final Widget content;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: title,
|
||||
content: content,
|
||||
actions: <Widget>[
|
||||
CupertinoDialogAction(
|
||||
child: const Text('Cheesecake'),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, 'Cheesecake');
|
||||
},
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
child: const Text('Tiramisu'),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, 'Tiramisu');
|
||||
},
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
child: const Text('Apple Pie'),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, 'Apple Pie');
|
||||
},
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
child: const Text("Devil's food cake"),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, "Devil's food cake");
|
||||
},
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
child: const Text('Banana Split'),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, 'Banana Split');
|
||||
},
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
child: const Text('Oatmeal Cookie'),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, 'Oatmeal Cookies');
|
||||
},
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
child: const Text('Chocolate Brownie'),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, 'Chocolate Brownies');
|
||||
},
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
child: const Text('Cancel'),
|
||||
isDestructiveAction: true,
|
||||
onPressed: () {
|
||||
Navigator.pop(context, 'Cancel');
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,89 +0,0 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class CupertinoButtonsDemo extends StatefulWidget {
|
||||
static const String routeName = '/cupertino/buttons';
|
||||
|
||||
@override
|
||||
_CupertinoButtonDemoState createState() => _CupertinoButtonDemoState();
|
||||
}
|
||||
|
||||
class _CupertinoButtonDemoState extends State<CupertinoButtonsDemo> {
|
||||
int _pressedCount = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: const Text('Buttons'),
|
||||
// We're specifying a back label here because the previous page is a
|
||||
// Material page. CupertinoPageRoutes could auto-populate these back
|
||||
// labels.
|
||||
previousPageTitle: 'Cupertino',
|
||||
trailing: CupertinoDemoDocumentationButton(CupertinoButtonsDemo.routeName),
|
||||
),
|
||||
child: DefaultTextStyle(
|
||||
style: CupertinoTheme.of(context).textTheme.textStyle,
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
'iOS themed buttons are flat. They can have borders or backgrounds but '
|
||||
'only when necessary.'
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget> [
|
||||
Text(_pressedCount > 0
|
||||
? 'Button pressed $_pressedCount time${_pressedCount == 1 ? "" : "s"}'
|
||||
: ' '),
|
||||
const Padding(padding: EdgeInsets.all(12.0)),
|
||||
Align(
|
||||
alignment: const Alignment(0.0, -0.2),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
CupertinoButton(
|
||||
child: const Text('Cupertino Button'),
|
||||
onPressed: () {
|
||||
setState(() { _pressedCount += 1; });
|
||||
},
|
||||
),
|
||||
const CupertinoButton(
|
||||
child: Text('Disabled'),
|
||||
onPressed: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(12.0)),
|
||||
CupertinoButton.filled(
|
||||
child: const Text('With Background'),
|
||||
onPressed: () {
|
||||
setState(() { _pressedCount += 1; });
|
||||
},
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(12.0)),
|
||||
const CupertinoButton.filled(
|
||||
child: Text('Disabled'),
|
||||
onPressed: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,812 +0,0 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
|
||||
|
||||
const List<Color> coolColors = <Color>[
|
||||
Color.fromARGB(255, 255, 59, 48),
|
||||
Color.fromARGB(255, 255, 149, 0),
|
||||
Color.fromARGB(255, 255, 204, 0),
|
||||
Color.fromARGB(255, 76, 217, 100),
|
||||
Color.fromARGB(255, 90, 200, 250),
|
||||
Color.fromARGB(255, 0, 122, 255),
|
||||
Color.fromARGB(255, 88, 86, 214),
|
||||
Color.fromARGB(255, 255, 45, 85),
|
||||
];
|
||||
|
||||
const List<String> coolColorNames = <String>[
|
||||
'Sarcoline', 'Coquelicot', 'Smaragdine', 'Mikado', 'Glaucous', 'Wenge',
|
||||
'Fulvous', 'Xanadu', 'Falu', 'Eburnean', 'Amaranth', 'Australien',
|
||||
'Banan', 'Falu', 'Gingerline', 'Incarnadine', 'Labrador', 'Nattier',
|
||||
'Pervenche', 'Sinoper', 'Verditer', 'Watchet', 'Zaffre',
|
||||
];
|
||||
|
||||
const int _kChildCount = 50;
|
||||
|
||||
class CupertinoNavigationDemo extends StatelessWidget {
|
||||
CupertinoNavigationDemo()
|
||||
: colorItems = List<Color>.generate(_kChildCount, (int index) {
|
||||
return coolColors[math.Random().nextInt(coolColors.length)];
|
||||
}) ,
|
||||
colorNameItems = List<String>.generate(_kChildCount, (int index) {
|
||||
return coolColorNames[math.Random().nextInt(coolColorNames.length)];
|
||||
});
|
||||
|
||||
static const String routeName = '/cupertino/navigation';
|
||||
|
||||
final List<Color> colorItems;
|
||||
final List<String> colorNameItems;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
// Prevent swipe popping of this page. Use explicit exit buttons only.
|
||||
onWillPop: () => Future<bool>.value(true),
|
||||
child: DefaultTextStyle(
|
||||
style: CupertinoTheme.of(context).textTheme.textStyle,
|
||||
child: CupertinoTabScaffold(
|
||||
tabBar: CupertinoTabBar(
|
||||
items: const <BottomNavigationBarItem>[
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(CupertinoIcons.home),
|
||||
title: Text('Home'),
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(CupertinoIcons.conversation_bubble),
|
||||
title: Text('Support'),
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(CupertinoIcons.profile_circled),
|
||||
title: Text('Profile'),
|
||||
),
|
||||
],
|
||||
),
|
||||
tabBuilder: (BuildContext context, int index) {
|
||||
assert(index >= 0 && index <= 2);
|
||||
switch (index) {
|
||||
case 0:
|
||||
return CupertinoTabView(
|
||||
builder: (BuildContext context) {
|
||||
return CupertinoDemoTab1(
|
||||
colorItems: colorItems,
|
||||
colorNameItems: colorNameItems,
|
||||
);
|
||||
},
|
||||
defaultTitle: 'Colors',
|
||||
);
|
||||
break;
|
||||
case 1:
|
||||
return CupertinoTabView(
|
||||
builder: (BuildContext context) => CupertinoDemoTab2(),
|
||||
defaultTitle: 'Support Chat',
|
||||
);
|
||||
break;
|
||||
case 2:
|
||||
return CupertinoTabView(
|
||||
builder: (BuildContext context) => CupertinoDemoTab3(),
|
||||
defaultTitle: 'Account',
|
||||
);
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ExitButton extends StatelessWidget {
|
||||
const ExitButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: const Tooltip(
|
||||
message: 'Back',
|
||||
child: Text('Exit'),
|
||||
excludeFromSemantics: true,
|
||||
),
|
||||
onPressed: () {
|
||||
// The demo is on the root navigator.
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final Widget trailingButtons = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
CupertinoDemoDocumentationButton(CupertinoNavigationDemo.routeName),
|
||||
const Padding(padding: EdgeInsets.only(left: 8.0)),
|
||||
const ExitButton(),
|
||||
],
|
||||
);
|
||||
|
||||
class CupertinoDemoTab1 extends StatelessWidget {
|
||||
const CupertinoDemoTab1({this.colorItems, this.colorNameItems});
|
||||
|
||||
final List<Color> colorItems;
|
||||
final List<String> colorNameItems;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
backgroundColor: CupertinoColors.systemGroupedBackground,
|
||||
child: CustomScrollView(
|
||||
semanticChildCount: _kChildCount,
|
||||
slivers: <Widget>[
|
||||
CupertinoSliverNavigationBar(
|
||||
trailing: trailingButtons,
|
||||
),
|
||||
SliverPadding(
|
||||
// Top media padding consumed by CupertinoSliverNavigationBar.
|
||||
// Left/Right media padding consumed by Tab1RowItem.
|
||||
padding: MediaQuery.of(context).removePadding(
|
||||
removeTop: true,
|
||||
removeLeft: true,
|
||||
removeRight: true,
|
||||
).padding,
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
return Tab1RowItem(
|
||||
index: index,
|
||||
lastItem: index == _kChildCount - 1,
|
||||
color: colorItems[index],
|
||||
colorName: colorNameItems[index],
|
||||
);
|
||||
},
|
||||
childCount: _kChildCount,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Tab1RowItem extends StatelessWidget {
|
||||
const Tab1RowItem({this.index, this.lastItem, this.color, this.colorName});
|
||||
|
||||
final int index;
|
||||
final bool lastItem;
|
||||
final Color color;
|
||||
final String colorName;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Widget row = GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(CupertinoPageRoute<void>(
|
||||
title: colorName,
|
||||
builder: (BuildContext context) => Tab1ItemPage(
|
||||
color: color,
|
||||
colorName: colorName,
|
||||
index: index,
|
||||
),
|
||||
));
|
||||
},
|
||||
child: Container(
|
||||
color: CupertinoDynamicColor.resolve(CupertinoColors.systemBackground, context),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0, top: 8.0, bottom: 8.0, right: 8.0),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
height: 60.0,
|
||||
width: 60.0,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(colorName),
|
||||
const Padding(padding: EdgeInsets.only(top: 8.0)),
|
||||
Text(
|
||||
'Buy this cool color',
|
||||
style: TextStyle(
|
||||
color: CupertinoDynamicColor.resolve(CupertinoColors.secondaryLabel, context),
|
||||
fontSize: 13.0,
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: const Icon(CupertinoIcons.plus_circled,
|
||||
semanticLabel: 'Add',
|
||||
),
|
||||
onPressed: () { },
|
||||
),
|
||||
CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: const Icon(CupertinoIcons.share,
|
||||
semanticLabel: 'Share',
|
||||
),
|
||||
onPressed: () { },
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (lastItem) {
|
||||
return row;
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
row,
|
||||
Container(
|
||||
height: 1.0,
|
||||
color: CupertinoDynamicColor.resolve(CupertinoColors.separator, context),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Tab1ItemPage extends StatefulWidget {
|
||||
const Tab1ItemPage({this.color, this.colorName, this.index});
|
||||
|
||||
final Color color;
|
||||
final String colorName;
|
||||
final int index;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => Tab1ItemPageState();
|
||||
}
|
||||
|
||||
class Tab1ItemPageState extends State<Tab1ItemPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
relatedColors = List<Color>.generate(10, (int index) {
|
||||
final math.Random random = math.Random();
|
||||
return Color.fromARGB(
|
||||
255,
|
||||
(widget.color.red + random.nextInt(100) - 50).clamp(0, 255),
|
||||
(widget.color.green + random.nextInt(100) - 50).clamp(0, 255),
|
||||
(widget.color.blue + random.nextInt(100) - 50).clamp(0, 255),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
List<Color> relatedColors;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: const CupertinoNavigationBar(
|
||||
trailing: ExitButton(),
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
const Padding(padding: EdgeInsets.only(top: 16.0)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
height: 128.0,
|
||||
width: 128.0,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.color,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(left: 18.0)),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
widget.colorName,
|
||||
style: const TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(top: 6.0)),
|
||||
Text(
|
||||
'Item number ${widget.index}',
|
||||
style: TextStyle(
|
||||
color: CupertinoDynamicColor.resolve(CupertinoColors.secondaryLabel, context),
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w100,
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(top: 20.0)),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
CupertinoButton.filled(
|
||||
minSize: 30.0,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
borderRadius: BorderRadius.circular(32.0),
|
||||
child: const Text(
|
||||
'GET',
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.28,
|
||||
),
|
||||
),
|
||||
onPressed: () { },
|
||||
),
|
||||
CupertinoButton.filled(
|
||||
minSize: 30.0,
|
||||
padding: EdgeInsets.zero,
|
||||
borderRadius: BorderRadius.circular(32.0),
|
||||
child: const Icon(CupertinoIcons.ellipsis),
|
||||
onPressed: () { },
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 16.0, top: 28.0, bottom: 8.0),
|
||||
child: Text(
|
||||
'USERS ALSO LIKED',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF646464),
|
||||
letterSpacing: -0.60,
|
||||
fontSize: 15.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 200.0,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: 10,
|
||||
itemExtent: 160.0,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
color: relatedColors[index],
|
||||
),
|
||||
child: Center(
|
||||
child: CupertinoButton(
|
||||
child: const Icon(
|
||||
CupertinoIcons.plus_circled,
|
||||
color: CupertinoColors.white,
|
||||
size: 36.0,
|
||||
),
|
||||
onPressed: () { },
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CupertinoDemoTab2 extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
trailing: trailingButtons,
|
||||
),
|
||||
child: CupertinoScrollbar(
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
CupertinoUserInterfaceLevel(
|
||||
data: CupertinoUserInterfaceLevelData.elevated,
|
||||
child: Tab2Header(),
|
||||
),
|
||||
...buildTab2Conversation(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Tab2Header extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoDynamicColor.resolve(CupertinoColors.systemFill, context),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'SUPPORT TICKET',
|
||||
style: TextStyle(
|
||||
color: CupertinoDynamicColor.resolve(CupertinoColors.secondaryLabel, context),
|
||||
letterSpacing: -0.9,
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Show More',
|
||||
style: TextStyle(
|
||||
color: CupertinoDynamicColor.resolve(CupertinoColors.secondaryLabel, context),
|
||||
letterSpacing: -0.6,
|
||||
fontSize: 12.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoDynamicColor.resolve(CupertinoColors.quaternarySystemFill, context),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
'Product or product packaging damaged during transit',
|
||||
style: TextStyle(
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.46,
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(top: 16.0)),
|
||||
const Text(
|
||||
'REVIEWERS',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF646464),
|
||||
fontSize: 12.0,
|
||||
letterSpacing: -0.6,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(top: 8.0)),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 44.0,
|
||||
height: 44.0,
|
||||
decoration: const BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(
|
||||
'people/square/trevor.png',
|
||||
package: _kGalleryAssetsPackage,
|
||||
),
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(left: 8.0)),
|
||||
Container(
|
||||
width: 44.0,
|
||||
height: 44.0,
|
||||
decoration: const BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(
|
||||
'people/square/sandra.png',
|
||||
package: _kGalleryAssetsPackage,
|
||||
),
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(left: 2.0)),
|
||||
const Icon(
|
||||
CupertinoIcons.check_mark_circled,
|
||||
color: Color(0xFF646464),
|
||||
size: 20.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum Tab2ConversationBubbleColor {
|
||||
blue,
|
||||
gray,
|
||||
}
|
||||
|
||||
class Tab2ConversationBubble extends StatelessWidget {
|
||||
const Tab2ConversationBubble({this.text, this.color});
|
||||
|
||||
final String text;
|
||||
final Tab2ConversationBubbleColor color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color backgroundColor;
|
||||
Color foregroundColor;
|
||||
|
||||
switch (color) {
|
||||
case Tab2ConversationBubbleColor.gray:
|
||||
backgroundColor = CupertinoDynamicColor.resolve(CupertinoColors.systemFill, context);
|
||||
foregroundColor = CupertinoDynamicColor.resolve(CupertinoColors.label, context);
|
||||
break;
|
||||
case Tab2ConversationBubbleColor.blue:
|
||||
backgroundColor = CupertinoTheme.of(context).primaryColor;
|
||||
foregroundColor = CupertinoColors.white;
|
||||
break;
|
||||
}
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(18.0)),
|
||||
color: backgroundColor,
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14.0, vertical: 10.0),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: foregroundColor,
|
||||
letterSpacing: -0.4,
|
||||
fontSize: 15.0,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Tab2ConversationAvatar extends StatelessWidget {
|
||||
const Tab2ConversationAvatar({this.text, this.color});
|
||||
|
||||
final String text;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
begin: FractionalOffset.topCenter,
|
||||
end: FractionalOffset.bottomCenter,
|
||||
colors: <Color>[
|
||||
color,
|
||||
Color.fromARGB(
|
||||
color.alpha,
|
||||
(color.red - 60).clamp(0, 255),
|
||||
(color.green - 60).clamp(0, 255),
|
||||
(color.blue - 60).clamp(0, 255),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.only(left: 8.0, bottom: 8.0),
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.white,
|
||||
fontSize: 13.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Tab2ConversationRow extends StatelessWidget {
|
||||
const Tab2ConversationRow({this.avatar, this.text});
|
||||
|
||||
final Tab2ConversationAvatar avatar;
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool isSelf = avatar == null;
|
||||
return SafeArea(
|
||||
child: Row(
|
||||
mainAxisAlignment: isSelf ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: isSelf ? CrossAxisAlignment.center : CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
if (avatar != null) avatar,
|
||||
CupertinoUserInterfaceLevel(
|
||||
data: CupertinoUserInterfaceLevelData.elevated,
|
||||
child: Tab2ConversationBubble(
|
||||
text: text,
|
||||
color: isSelf
|
||||
? Tab2ConversationBubbleColor.blue
|
||||
: Tab2ConversationBubbleColor.gray,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> buildTab2Conversation() {
|
||||
return <Widget>[
|
||||
const Tab2ConversationRow(
|
||||
text: "My Xanadu doesn't look right",
|
||||
),
|
||||
const Tab2ConversationRow(
|
||||
avatar: Tab2ConversationAvatar(
|
||||
text: 'KL',
|
||||
color: Color(0xFFFD5015),
|
||||
),
|
||||
text: "We'll rush you a new one.\nIt's gonna be incredible",
|
||||
),
|
||||
const Tab2ConversationRow(
|
||||
text: 'Awesome thanks!',
|
||||
),
|
||||
const Tab2ConversationRow(
|
||||
avatar: Tab2ConversationAvatar(
|
||||
text: 'SJ',
|
||||
color: Color(0xFF34CAD6),
|
||||
),
|
||||
text: "We'll send you our\nnewest Labrador too!",
|
||||
),
|
||||
const Tab2ConversationRow(
|
||||
text: 'Yay',
|
||||
),
|
||||
const Tab2ConversationRow(
|
||||
avatar: Tab2ConversationAvatar(
|
||||
text: 'KL',
|
||||
color: Color(0xFFFD5015),
|
||||
),
|
||||
text: "Actually there's one more thing...",
|
||||
),
|
||||
const Tab2ConversationRow(
|
||||
text: "What's that?",
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
class CupertinoDemoTab3 extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(trailing: trailingButtons),
|
||||
backgroundColor: CupertinoColors.systemBackground,
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
const Padding(padding: EdgeInsets.only(top: 32.0)),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context, rootNavigator: true).push(
|
||||
CupertinoPageRoute<bool>(
|
||||
fullscreenDialog: true,
|
||||
builder: (BuildContext context) => Tab3Dialog(),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoTheme.of(context).scaffoldBackgroundColor,
|
||||
border: const Border(
|
||||
top: BorderSide(color: Color(0xFFBCBBC1), width: 0.0),
|
||||
bottom: BorderSide(color: Color(0xFFBCBBC1), width: 0.0),
|
||||
),
|
||||
),
|
||||
height: 44.0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Sign in',
|
||||
style: TextStyle(color: CupertinoTheme.of(context).primaryColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Tab3Dialog extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
leading: CupertinoButton(
|
||||
child: const Text('Cancel'),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
CupertinoIcons.profile_circled,
|
||||
size: 160.0,
|
||||
color: Color(0xFF646464),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(top: 18.0)),
|
||||
CupertinoButton.filled(
|
||||
child: const Text('Sign in'),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,292 +0,0 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
import 'cupertino_navigation_demo.dart' show coolColorNames;
|
||||
|
||||
const double _kPickerSheetHeight = 216.0;
|
||||
const double _kPickerItemHeight = 32.0;
|
||||
|
||||
class CupertinoPickerDemo extends StatefulWidget {
|
||||
static const String routeName = '/cupertino/picker';
|
||||
|
||||
@override
|
||||
_CupertinoPickerDemoState createState() => _CupertinoPickerDemoState();
|
||||
}
|
||||
|
||||
class _BottomPicker extends StatelessWidget {
|
||||
const _BottomPicker({
|
||||
Key key,
|
||||
@required this.child,
|
||||
}) : assert(child != null),
|
||||
super(key: key);
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: _kPickerSheetHeight,
|
||||
padding: const EdgeInsets.only(top: 6.0),
|
||||
color: CupertinoColors.systemBackground.resolveFrom(context),
|
||||
child: DefaultTextStyle(
|
||||
style: TextStyle(
|
||||
color: CupertinoColors.label.resolveFrom(context),
|
||||
fontSize: 22.0,
|
||||
),
|
||||
child: GestureDetector(
|
||||
// Blocks taps from propagating to the modal sheet and popping.
|
||||
onTap: () { },
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Menu extends StatelessWidget {
|
||||
const _Menu({
|
||||
Key key,
|
||||
@required this.children,
|
||||
}) : assert(children != null),
|
||||
super(key: key);
|
||||
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoTheme.of(context).scaffoldBackgroundColor,
|
||||
border: const Border(
|
||||
top: BorderSide(color: Color(0xFFBCBBC1), width: 0.0),
|
||||
bottom: BorderSide(color: Color(0xFFBCBBC1), width: 0.0),
|
||||
),
|
||||
),
|
||||
height: 44.0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
|
||||
int _selectedColorIndex = 0;
|
||||
|
||||
Duration timer = const Duration();
|
||||
|
||||
// Value that is shown in the date picker in date mode.
|
||||
DateTime date = DateTime.now();
|
||||
|
||||
// Value that is shown in the date picker in time mode.
|
||||
DateTime time = DateTime.now();
|
||||
|
||||
// Value that is shown in the date picker in dateAndTime mode.
|
||||
DateTime dateTime = DateTime.now();
|
||||
|
||||
Widget _buildColorPicker(BuildContext context) {
|
||||
final FixedExtentScrollController scrollController =
|
||||
FixedExtentScrollController(initialItem: _selectedColorIndex);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
await showCupertinoModalPopup<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return _BottomPicker(
|
||||
child: CupertinoPicker(
|
||||
scrollController: scrollController,
|
||||
itemExtent: _kPickerItemHeight,
|
||||
backgroundColor: CupertinoColors.systemBackground.resolveFrom(context),
|
||||
onSelectedItemChanged: (int index) {
|
||||
setState(() => _selectedColorIndex = index);
|
||||
},
|
||||
children: List<Widget>.generate(coolColorNames.length, (int index) {
|
||||
return Center(
|
||||
child: Text(coolColorNames[index]),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: _Menu(
|
||||
children: <Widget>[
|
||||
const Text('Favorite Color'),
|
||||
Text(
|
||||
coolColorNames[_selectedColorIndex],
|
||||
style: TextStyle(color: CupertinoDynamicColor.resolve(CupertinoColors.inactiveGray, context)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCountdownTimerPicker(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
showCupertinoModalPopup<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return _BottomPicker(
|
||||
child: CupertinoTimerPicker(
|
||||
backgroundColor: CupertinoColors.systemBackground.resolveFrom(context),
|
||||
initialTimerDuration: timer,
|
||||
onTimerDurationChanged: (Duration newTimer) {
|
||||
setState(() => timer = newTimer);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: _Menu(
|
||||
children: <Widget>[
|
||||
const Text('Countdown Timer'),
|
||||
Text(
|
||||
'${timer.inHours}:'
|
||||
'${(timer.inMinutes % 60).toString().padLeft(2,'0')}:'
|
||||
'${(timer.inSeconds % 60).toString().padLeft(2,'0')}',
|
||||
style: TextStyle(color: CupertinoDynamicColor.resolve(CupertinoColors.inactiveGray, context)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDatePicker(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
showCupertinoModalPopup<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return _BottomPicker(
|
||||
child: CupertinoDatePicker(
|
||||
backgroundColor: CupertinoColors.systemBackground.resolveFrom(context),
|
||||
mode: CupertinoDatePickerMode.date,
|
||||
initialDateTime: date,
|
||||
onDateTimeChanged: (DateTime newDateTime) {
|
||||
setState(() => date = newDateTime);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: _Menu(
|
||||
children: <Widget>[
|
||||
const Text('Date'),
|
||||
Text(
|
||||
DateFormat.yMMMMd().format(date),
|
||||
style: TextStyle(color: CupertinoDynamicColor.resolve(CupertinoColors.inactiveGray, context)),
|
||||
),
|
||||
]
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTimePicker(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
showCupertinoModalPopup<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return _BottomPicker(
|
||||
child: CupertinoDatePicker(
|
||||
backgroundColor: CupertinoColors.systemBackground.resolveFrom(context),
|
||||
mode: CupertinoDatePickerMode.time,
|
||||
initialDateTime: time,
|
||||
onDateTimeChanged: (DateTime newDateTime) {
|
||||
setState(() => time = newDateTime);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: _Menu(
|
||||
children: <Widget>[
|
||||
const Text('Time'),
|
||||
Text(
|
||||
DateFormat.jm().format(time),
|
||||
style: TextStyle(color: CupertinoDynamicColor.resolve(CupertinoColors.inactiveGray, context)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDateAndTimePicker(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
showCupertinoModalPopup<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return _BottomPicker(
|
||||
child: CupertinoDatePicker(
|
||||
backgroundColor: CupertinoColors.systemBackground.resolveFrom(context),
|
||||
mode: CupertinoDatePickerMode.dateAndTime,
|
||||
initialDateTime: dateTime,
|
||||
onDateTimeChanged: (DateTime newDateTime) {
|
||||
setState(() => dateTime = newDateTime);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: _Menu(
|
||||
children: <Widget>[
|
||||
const Text('Date and Time'),
|
||||
Text(
|
||||
DateFormat.yMMMd().add_jm().format(dateTime),
|
||||
style: TextStyle(color: CupertinoDynamicColor.resolve(CupertinoColors.inactiveGray, context)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: const Text('Picker'),
|
||||
// We're specifying a back label here because the previous page is a
|
||||
// Material page. CupertinoPageRoutes could auto-populate these back
|
||||
// labels.
|
||||
previousPageTitle: 'Cupertino',
|
||||
trailing: CupertinoDemoDocumentationButton(CupertinoPickerDemo.routeName),
|
||||
),
|
||||
child: DefaultTextStyle(
|
||||
style: CupertinoTheme.of(context).textTheme.textStyle,
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
const Padding(padding: EdgeInsets.only(top: 32.0)),
|
||||
_buildColorPicker(context),
|
||||
_buildCountdownTimerPicker(context),
|
||||
_buildDatePicker(context),
|
||||
_buildTimePicker(context),
|
||||
_buildDateAndTimePicker(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,238 +0,0 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
import 'dart:async';
|
||||
import 'dart:math' show Random;
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class CupertinoRefreshControlDemo extends StatefulWidget {
|
||||
static const String routeName = '/cupertino/refresh';
|
||||
|
||||
@override
|
||||
_CupertinoRefreshControlDemoState createState() => _CupertinoRefreshControlDemoState();
|
||||
}
|
||||
|
||||
class _CupertinoRefreshControlDemoState extends State<CupertinoRefreshControlDemo> {
|
||||
List<List<String>> randomizedContacts;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
repopulateList();
|
||||
}
|
||||
|
||||
void repopulateList() {
|
||||
final Random random = Random();
|
||||
randomizedContacts = List<List<String>>.generate(
|
||||
100,
|
||||
(int index) {
|
||||
return contacts[random.nextInt(contacts.length)]
|
||||
// Randomly adds a telephone icon next to the contact or not.
|
||||
..add(random.nextBool().toString());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTextStyle(
|
||||
style: CupertinoTheme.of(context).textTheme.textStyle,
|
||||
child: CupertinoPageScaffold(
|
||||
backgroundColor: CupertinoColors.systemGroupedBackground,
|
||||
child: CustomScrollView(
|
||||
// If left unspecified, the [CustomScrollView] appends an
|
||||
// [AlwaysScrollableScrollPhysics]. Behind the scene, the ScrollableState
|
||||
// will attach that [AlwaysScrollableScrollPhysics] to the output of
|
||||
// [ScrollConfiguration.of] which will be a [ClampingScrollPhysics]
|
||||
// on Android.
|
||||
// To demonstrate the iOS behavior in this demo and to ensure that the list
|
||||
// always scrolls, we specifically use a [BouncingScrollPhysics] combined
|
||||
// with a [AlwaysScrollableScrollPhysics]
|
||||
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
|
||||
slivers: <Widget>[
|
||||
CupertinoSliverNavigationBar(
|
||||
largeTitle: const Text('Refresh'),
|
||||
// We're specifying a back label here because the previous page
|
||||
// is a Material page. CupertinoPageRoutes could auto-populate
|
||||
// these back labels.
|
||||
previousPageTitle: 'Cupertino',
|
||||
trailing: CupertinoDemoDocumentationButton(CupertinoRefreshControlDemo.routeName),
|
||||
),
|
||||
CupertinoSliverRefreshControl(
|
||||
onRefresh: () {
|
||||
return Future<void>.delayed(const Duration(seconds: 2))
|
||||
..then<void>((_) {
|
||||
if (mounted) {
|
||||
setState(() => repopulateList());
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
SliverSafeArea(
|
||||
top: false, // Top safe area is consumed by the navigation bar.
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
return _ListItem(
|
||||
name: randomizedContacts[index][0],
|
||||
place: randomizedContacts[index][1],
|
||||
date: randomizedContacts[index][2],
|
||||
called: randomizedContacts[index][3] == 'true',
|
||||
);
|
||||
},
|
||||
childCount: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<List<String>> contacts = <List<String>>[
|
||||
<String>['George Washington', 'Westmoreland County', ' 4/30/1789'],
|
||||
<String>['John Adams', 'Braintree', ' 3/4/1797'],
|
||||
<String>['Thomas Jefferson', 'Shadwell', ' 3/4/1801'],
|
||||
<String>['James Madison', 'Port Conway', ' 3/4/1809'],
|
||||
<String>['James Monroe', 'Monroe Hall', ' 3/4/1817'],
|
||||
<String>['Andrew Jackson', 'Waxhaws Region South/North', ' 3/4/1829'],
|
||||
<String>['John Quincy Adams', 'Braintree', ' 3/4/1825'],
|
||||
<String>['William Henry Harrison', 'Charles City County', ' 3/4/1841'],
|
||||
<String>['Martin Van Buren', 'Kinderhook New', ' 3/4/1837'],
|
||||
<String>['Zachary Taylor', 'Barboursville', ' 3/4/1849'],
|
||||
<String>['John Tyler', 'Charles City County', ' 4/4/1841'],
|
||||
<String>['James Buchanan', 'Cove Gap', ' 3/4/1857'],
|
||||
<String>['James K. Polk', 'Pineville North', ' 3/4/1845'],
|
||||
<String>['Millard Fillmore', 'Summerhill New', '7/9/1850'],
|
||||
<String>['Franklin Pierce', 'Hillsborough New', ' 3/4/1853'],
|
||||
<String>['Andrew Johnson', 'Raleigh North', ' 4/15/1865'],
|
||||
<String>['Abraham Lincoln', 'Sinking Spring', ' 3/4/1861'],
|
||||
<String>['Ulysses S. Grant', 'Point Pleasant', ' 3/4/1869'],
|
||||
<String>['Rutherford B. Hayes', 'Delaware', ' 3/4/1877'],
|
||||
<String>['Chester A. Arthur', 'Fairfield', ' 9/19/1881'],
|
||||
<String>['James A. Garfield', 'Moreland Hills', ' 3/4/1881'],
|
||||
<String>['Benjamin Harrison', 'North Bend', ' 3/4/1889'],
|
||||
<String>['Grover Cleveland', 'Caldwell New', ' 3/4/1885'],
|
||||
<String>['William McKinley', 'Niles', ' 3/4/1897'],
|
||||
<String>['Woodrow Wilson', 'Staunton', ' 3/4/1913'],
|
||||
<String>['William H. Taft', 'Cincinnati', ' 3/4/1909'],
|
||||
<String>['Theodore Roosevelt', 'New York City New', ' 9/14/1901'],
|
||||
<String>['Warren G. Harding', 'Blooming Grove', ' 3/4/1921'],
|
||||
<String>['Calvin Coolidge', 'Plymouth', '8/2/1923'],
|
||||
<String>['Herbert Hoover', 'West Branch', ' 3/4/1929'],
|
||||
<String>['Franklin D. Roosevelt', 'Hyde Park New', ' 3/4/1933'],
|
||||
<String>['Harry S. Truman', 'Lamar', ' 4/12/1945'],
|
||||
<String>['Dwight D. Eisenhower', 'Denison', ' 1/20/1953'],
|
||||
<String>['Lyndon B. Johnson', 'Stonewall', '11/22/1963'],
|
||||
<String>['Ronald Reagan', 'Tampico', ' 1/20/1981'],
|
||||
<String>['Richard Nixon', 'Yorba Linda', ' 1/20/1969'],
|
||||
<String>['Gerald Ford', 'Omaha', 'August 9/1974'],
|
||||
<String>['John F. Kennedy', 'Brookline', ' 1/20/1961'],
|
||||
<String>['George H. W. Bush', 'Milton', ' 1/20/1989'],
|
||||
<String>['Jimmy Carter', 'Plains', ' 1/20/1977'],
|
||||
<String>['George W. Bush', 'New Haven', ' 1/20, 2001'],
|
||||
<String>['Bill Clinton', 'Hope', ' 1/20/1993'],
|
||||
<String>['Barack Obama', 'Honolulu', ' 1/20/2009'],
|
||||
<String>['Donald J. Trump', 'New York City', ' 1/20/2017'],
|
||||
];
|
||||
|
||||
class _ListItem extends StatelessWidget {
|
||||
const _ListItem({
|
||||
this.name,
|
||||
this.place,
|
||||
this.date,
|
||||
this.called,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String place;
|
||||
final String date;
|
||||
final bool called;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: CupertinoDynamicColor.resolve(CupertinoColors.systemBackground, context),
|
||||
height: 60.0,
|
||||
padding: const EdgeInsets.only(top: 9.0),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 38.0,
|
||||
child: called
|
||||
? Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Icon(
|
||||
CupertinoIcons.phone_solid,
|
||||
color: CupertinoColors.inactiveGray.resolveFrom(context),
|
||||
size: 18.0,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Color(0xFFBCBBC1), width: 0.0),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.only(left: 1.0, bottom: 9.0, right: 10.0),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: -0.18,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
place,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 15.0,
|
||||
letterSpacing: -0.24,
|
||||
color: CupertinoColors.inactiveGray.resolveFrom(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
date,
|
||||
style: TextStyle(
|
||||
color: CupertinoColors.inactiveGray.resolveFrom(context),
|
||||
fontSize: 15.0,
|
||||
letterSpacing: -0.41,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 9.0),
|
||||
child: Icon(
|
||||
CupertinoIcons.info,
|
||||
color: CupertinoTheme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,145 +0,0 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
const Color _kKeyUmbraOpacity = Color(0x33000000); // alpha = 0.2
|
||||
const Color _kKeyPenumbraOpacity = Color(0x24000000); // alpha = 0.14
|
||||
const Color _kAmbientShadowOpacity = Color(0x1F000000); // alpha = 0.12
|
||||
|
||||
class CupertinoSegmentedControlDemo extends StatefulWidget {
|
||||
static const String routeName = 'cupertino/segmented_control';
|
||||
|
||||
@override
|
||||
_CupertinoSegmentedControlDemoState createState() => _CupertinoSegmentedControlDemoState();
|
||||
}
|
||||
|
||||
class _CupertinoSegmentedControlDemoState extends State<CupertinoSegmentedControlDemo> {
|
||||
final Map<int, Widget> children = const <int, Widget>{
|
||||
0: Text('Midnight'),
|
||||
1: Text('Viridian'),
|
||||
2: Text('Cerulean'),
|
||||
};
|
||||
|
||||
final Map<int, Widget> icons = const <int, Widget>{
|
||||
0: Center(
|
||||
child: FlutterLogo(
|
||||
colors: Colors.indigo,
|
||||
size: 200.0,
|
||||
),
|
||||
),
|
||||
1: Center(
|
||||
child: FlutterLogo(
|
||||
colors: Colors.teal,
|
||||
size: 200.0,
|
||||
),
|
||||
),
|
||||
2: Center(
|
||||
child: FlutterLogo(
|
||||
colors: Colors.cyan,
|
||||
size: 200.0,
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
int currentSegment = 0;
|
||||
|
||||
void onValueChanged(int newValue) {
|
||||
setState(() {
|
||||
currentSegment = newValue;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: const Text('Segmented Control'),
|
||||
// We're specifying a back label here because the previous page is a
|
||||
// Material page. CupertinoPageRoutes could auto-populate these back
|
||||
// labels.
|
||||
previousPageTitle: 'Cupertino',
|
||||
trailing: CupertinoDemoDocumentationButton(CupertinoSegmentedControlDemo.routeName),
|
||||
),
|
||||
child: DefaultTextStyle(
|
||||
style: CupertinoTheme.of(context).textTheme.textStyle.copyWith(fontSize: 13),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
const Padding(padding: EdgeInsets.all(16.0)),
|
||||
SizedBox(
|
||||
width: 500.0,
|
||||
child: CupertinoSegmentedControl<int>(
|
||||
children: children,
|
||||
onValueChanged: onValueChanged,
|
||||
groupValue: currentSegment,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 500,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: CupertinoSlidingSegmentedControl<int>(
|
||||
children: children,
|
||||
onValueChanged: onValueChanged,
|
||||
groupValue: currentSegment,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 32.0,
|
||||
horizontal: 16.0,
|
||||
),
|
||||
child: CupertinoUserInterfaceLevel(
|
||||
data: CupertinoUserInterfaceLevelData.elevated,
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 64.0,
|
||||
horizontal: 16.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoTheme.of(context).scaffoldBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(3.0),
|
||||
boxShadow: const <BoxShadow>[
|
||||
BoxShadow(
|
||||
offset: Offset(0.0, 3.0),
|
||||
blurRadius: 5.0,
|
||||
spreadRadius: -1.0,
|
||||
color: _kKeyUmbraOpacity,
|
||||
),
|
||||
BoxShadow(
|
||||
offset: Offset(0.0, 6.0),
|
||||
blurRadius: 10.0,
|
||||
spreadRadius: 0.0,
|
||||
color: _kKeyPenumbraOpacity,
|
||||
),
|
||||
BoxShadow(
|
||||
offset: Offset(0.0, 1.0),
|
||||
blurRadius: 18.0,
|
||||
spreadRadius: 0.0,
|
||||
color: _kAmbientShadowOpacity,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: icons[currentSegment],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,78 +0,0 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class CupertinoSliderDemo extends StatefulWidget {
|
||||
static const String routeName = '/cupertino/slider';
|
||||
|
||||
@override
|
||||
_CupertinoSliderDemoState createState() => _CupertinoSliderDemoState();
|
||||
}
|
||||
|
||||
class _CupertinoSliderDemoState extends State<CupertinoSliderDemo> {
|
||||
double _value = 25.0;
|
||||
double _discreteValue = 20.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: const Text('Sliders'),
|
||||
// We're specifying a back label here because the previous page is a
|
||||
// Material page. CupertinoPageRoutes could auto-populate these back
|
||||
// labels.
|
||||
previousPageTitle: 'Cupertino',
|
||||
trailing: CupertinoDemoDocumentationButton(CupertinoSliderDemo.routeName),
|
||||
),
|
||||
child: DefaultTextStyle(
|
||||
style: CupertinoTheme.of(context).textTheme.textStyle,
|
||||
child: SafeArea(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget> [
|
||||
CupertinoSlider(
|
||||
value: _value,
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
_value = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
Text('Cupertino Continuous: ${_value.toStringAsFixed(1)}'),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget> [
|
||||
CupertinoSlider(
|
||||
value: _discreteValue,
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
divisions: 5,
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
_discreteValue = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
Text('Cupertino Discrete: $_discreteValue'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,91 +0,0 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class CupertinoSwitchDemo extends StatefulWidget {
|
||||
static const String routeName = '/cupertino/switch';
|
||||
|
||||
@override
|
||||
_CupertinoSwitchDemoState createState() => _CupertinoSwitchDemoState();
|
||||
}
|
||||
|
||||
class _CupertinoSwitchDemoState extends State<CupertinoSwitchDemo> {
|
||||
|
||||
bool _switchValue = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: const Text('Switch'),
|
||||
// We're specifying a back label here because the previous page is a
|
||||
// Material page. CupertinoPageRoutes could auto-populate these back
|
||||
// labels.
|
||||
previousPageTitle: 'Cupertino',
|
||||
trailing: CupertinoDemoDocumentationButton(CupertinoSwitchDemo.routeName),
|
||||
),
|
||||
child: DefaultTextStyle(
|
||||
style: CupertinoTheme.of(context).textTheme.textStyle,
|
||||
child: SafeArea(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: <Widget>[
|
||||
Semantics(
|
||||
container: true,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
CupertinoSwitch(
|
||||
value: _switchValue,
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
_switchValue = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
Text(
|
||||
"Enabled - ${_switchValue ? "On" : "Off"}"
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Semantics(
|
||||
container: true,
|
||||
child: Column(
|
||||
children: const <Widget>[
|
||||
CupertinoSwitch(
|
||||
value: true,
|
||||
onChanged: null,
|
||||
),
|
||||
Text(
|
||||
'Disabled - On'
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Semantics(
|
||||
container: true,
|
||||
child: Column(
|
||||
children: const <Widget>[
|
||||
CupertinoSwitch(
|
||||
value: false,
|
||||
onChanged: null,
|
||||
),
|
||||
Text(
|
||||
'Disabled - Off'
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,194 +0,0 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class CupertinoTextFieldDemo extends StatefulWidget {
|
||||
static const String routeName = '/cupertino/text_fields';
|
||||
|
||||
@override
|
||||
_CupertinoTextFieldDemoState createState() {
|
||||
return _CupertinoTextFieldDemoState();
|
||||
}
|
||||
}
|
||||
|
||||
class _CupertinoTextFieldDemoState extends State<CupertinoTextFieldDemo> {
|
||||
TextEditingController _chatTextController;
|
||||
TextEditingController _locationTextController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_chatTextController = TextEditingController();
|
||||
_locationTextController = TextEditingController(text: 'Montreal, Canada');
|
||||
}
|
||||
|
||||
Widget _buildChatTextField() {
|
||||
return CupertinoTextField(
|
||||
controller: _chatTextController,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
placeholder: 'Text Message',
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 0.0,
|
||||
color: CupertinoColors.inactiveGray,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
),
|
||||
maxLines: null,
|
||||
keyboardType: TextInputType.multiline,
|
||||
prefix: const Padding(padding: EdgeInsets.symmetric(horizontal: 4.0)),
|
||||
suffix: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: CupertinoButton(
|
||||
color: CupertinoColors.activeGreen,
|
||||
minSize: 0.0,
|
||||
child: const Icon(
|
||||
CupertinoIcons.up_arrow,
|
||||
size: 21.0,
|
||||
color: CupertinoColors.white,
|
||||
),
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
onPressed: ()=> setState(()=> _chatTextController.clear()),
|
||||
),
|
||||
),
|
||||
autofocus: true,
|
||||
suffixMode: OverlayVisibilityMode.editing,
|
||||
onSubmitted: (String text)=> setState(()=> _chatTextController.clear()),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNameField() {
|
||||
return const CupertinoTextField(
|
||||
prefix: Icon(
|
||||
CupertinoIcons.person_solid,
|
||||
color: CupertinoColors.lightBackgroundGray,
|
||||
size: 28.0,
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 6.0, vertical: 12.0),
|
||||
clearButtonMode: OverlayVisibilityMode.editing,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
autocorrect: false,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(width: 0.0, color: CupertinoColors.inactiveGray)),
|
||||
),
|
||||
placeholder: 'Name',
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmailField() {
|
||||
return const CupertinoTextField(
|
||||
prefix: Icon(
|
||||
CupertinoIcons.mail_solid,
|
||||
color: CupertinoColors.lightBackgroundGray,
|
||||
size: 28.0,
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 6.0, vertical: 12.0),
|
||||
clearButtonMode: OverlayVisibilityMode.editing,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autocorrect: false,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(width: 0.0, color: CupertinoColors.inactiveGray)),
|
||||
),
|
||||
placeholder: 'Email',
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLocationField() {
|
||||
return CupertinoTextField(
|
||||
controller: _locationTextController,
|
||||
prefix: const Icon(
|
||||
CupertinoIcons.location_solid,
|
||||
color: CupertinoColors.lightBackgroundGray,
|
||||
size: 28.0,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 12.0),
|
||||
clearButtonMode: OverlayVisibilityMode.editing,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(bottom: BorderSide(width: 0.0, color: CupertinoColors.inactiveGray)),
|
||||
),
|
||||
placeholder: 'Location',
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPinField() {
|
||||
return const CupertinoTextField(
|
||||
prefix: Icon(
|
||||
CupertinoIcons.padlock_solid,
|
||||
color: CupertinoColors.lightBackgroundGray,
|
||||
size: 28.0,
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 6.0, vertical: 12.0),
|
||||
clearButtonMode: OverlayVisibilityMode.editing,
|
||||
keyboardType: TextInputType.number,
|
||||
autocorrect: false,
|
||||
obscureText: true,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(width: 0.0, color: CupertinoColors.inactiveGray)),
|
||||
),
|
||||
placeholder: 'Create a PIN',
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTagsField() {
|
||||
return CupertinoTextField(
|
||||
controller: TextEditingController(text: 'colleague, reading club'),
|
||||
prefix: const Icon(
|
||||
CupertinoIcons.tags_solid,
|
||||
color: CupertinoColors.lightBackgroundGray,
|
||||
size: 28.0,
|
||||
),
|
||||
enabled: false,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 12.0),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(bottom: BorderSide(width: 0.0, color: CupertinoColors.inactiveGray)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTextStyle(
|
||||
style: const TextStyle(
|
||||
fontFamily: '.SF UI Text',
|
||||
inherit: false,
|
||||
fontSize: 17.0,
|
||||
color: CupertinoColors.black,
|
||||
),
|
||||
child: CupertinoPageScaffold(
|
||||
navigationBar: const CupertinoNavigationBar(
|
||||
// We're specifying a back label here because the previous page is a
|
||||
// Material page. CupertinoPageRoutes could auto-populate these back
|
||||
// labels.
|
||||
previousPageTitle: 'Cupertino',
|
||||
middle: Text('Text Fields'),
|
||||
),
|
||||
child: CupertinoScrollbar(
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 32.0, horizontal: 16.0),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
_buildNameField(),
|
||||
_buildEmailField(),
|
||||
_buildLocationField(),
|
||||
_buildPinField(),
|
||||
_buildTagsField(),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 32.0, horizontal: 16.0),
|
||||
child: _buildChatTextField(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
# Fortnightly
|
||||
|
||||
A Flutter sample app based on the Material study Fortnightly (a hypothetical, online newspaper.) It
|
||||
showcases print-quality, custom typography, Material Theming, and text-heavy UI design and layout.
|
||||
|
||||
For info on the Fortnightly Material Study, see: https://material.io/design/material-studies/fortnightly.html
|
||||
|
||||
## Goals for this sample
|
||||
|
||||
* Help you understand how to customize and layout text.
|
||||
* Provide you with example code for
|
||||
* Text
|
||||
* A short app bar (the menu button top left.)
|
||||
* Avatar images
|
||||
|
||||
## Widgets / APIs
|
||||
|
||||
* BeveledRectangleBorder
|
||||
* BoxConstraints on Container
|
||||
* CircleAvatar
|
||||
* ExactAssetImage
|
||||
* Fonts
|
||||
* SafeArea
|
||||
* Stack
|
||||
* SingleChildScrollView
|
||||
* Text
|
||||
* TextStyle
|
||||
* TextTheme
|
||||
|
||||
## Notice
|
||||
|
||||
* Theming is passed as a parameter in the constructor of `MaterialApp` (`theme:`).
|
||||
* `SafeArea` adds padding around notches and virtual home buttons on screens that have them (like
|
||||
iPhone X+). Here, it protects the `ShortAppBar` from overlapping with the status bar (time)
|
||||
and makes sure the bottom of the newspaper article has padding beneath it if necessary.
|
||||
* The entire newspaper article is wrapped in a `SingleChildScrollView` widget which ensures that the
|
||||
entire article can be viewed no matter what the screen's size or orientation is.
|
||||
* The `Text` widget with text ' ¬ ' has a `TextStyle` that changes one parameter of an inherited
|
||||
`TextStyle` using `.apply()``.
|
||||
* The `Text` widget with text 'Connor Eghan' has a `TextStyle` created explicitly instead of
|
||||
inheriting from theming.
|
||||
* You can break up long strings in your source files by putting them on multiple lines.
|
||||
* Fonts are imported with multiple files expressing their weights (Bold, Light, Medium, Regular)
|
||||
but are accessed with a `FontWeight` value like `FontWeight.w800` for Merriweather-Bold.ttf.
|
||||
|
||||
## Questions/issues
|
||||
|
||||
If you have a general question about developing in Flutter, the best places to go are:
|
||||
|
||||
* [The FlutterDev Google Group](https://groups.google.com/forum/#!forum/flutter-dev)
|
||||
* [The Flutter Gitter channel](https://gitter.im/flutter/flutter)
|
||||
* [StackOverflow](https://stackoverflow.com/questions/tagged/flutter)
|
||||
@ -1,236 +0,0 @@
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FortnightlyDemo extends StatelessWidget {
|
||||
static const String routeName = '/fortnightly';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Fortnightly Demo',
|
||||
theme: _fortnightlyTheme,
|
||||
home: Scaffold(
|
||||
body: Stack(
|
||||
children: <Widget>[
|
||||
FruitPage(),
|
||||
SafeArea(
|
||||
child: ShortAppBar(
|
||||
onBackPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShortAppBar extends StatelessWidget {
|
||||
const ShortAppBar({ this.onBackPressed });
|
||||
|
||||
final VoidCallback onBackPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 96,
|
||||
height: 50,
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
elevation: 4,
|
||||
shape: const BeveledRectangleBorder(
|
||||
borderRadius: BorderRadius.only(bottomRight: Radius.circular(22)),
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
tooltip: 'Back',
|
||||
onPressed: onBackPressed,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Image.asset(
|
||||
'logos/fortnightly/fortnightly_logo.png',
|
||||
package: 'flutter_gallery_assets',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FruitPage extends StatelessWidget {
|
||||
static final String paragraph1 = '''Have you ever held a quince? It\'s strange;
|
||||
covered in a fuzz somewhere between peach skin and a spider web. And it\'s
|
||||
hard as soft lumber. You\'d be forgiven for thinking it\'s veneered Larch-wood.
|
||||
But inhale the aroma and you\'ll instantly know you have something wonderful.
|
||||
Its scent can fill a room for days. And all this before you\'ve even cooked it.
|
||||
'''.replaceAll('\n', '');
|
||||
|
||||
static final String paragraph2 = '''Pomegranates on the other hand have become
|
||||
almost ubiquitous. You can find its juice in any bodega, Walmart, and even some
|
||||
gas stations. But at what cost? The pomegranate juice craze of the aughts made
|
||||
\"megafarmers\" Lynda and Stewart Resnick billions. Unfortunately, it takes a lot
|
||||
of water to make that much pomegranate juice. Water the Resnicks get from their
|
||||
majority stake in the Kern Water Bank. How did one family come to hold control
|
||||
over water meant for the whole central valley of California? The story will shock you.
|
||||
'''.replaceAll('\n', '');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TextTheme textTheme = Theme.of(context).primaryTextTheme;
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
constraints: const BoxConstraints.expand(height: 248),
|
||||
child: Image.asset(
|
||||
'food/fruits.png',
|
||||
package: 'flutter_gallery_assets',
|
||||
fit: BoxFit.fitWidth,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 17),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'US',
|
||||
style: textTheme.overline,
|
||||
),
|
||||
Text(
|
||||
' ¬ ',
|
||||
// TODO(larche): Replace textTheme.display3.color with a ColorScheme value when known.
|
||||
style: textTheme.overline.apply(color: textTheme.display3.color),
|
||||
),
|
||||
Text(
|
||||
'CULTURE',
|
||||
style: textTheme.overline,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'Quince for Wisdom, Persimmon for Luck, Pomegranate for Love',
|
||||
style: textTheme.display1,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'How these crazy fruits sweetened our hearts, relationships,'
|
||||
'and puffed pastries',
|
||||
style: textTheme.body1,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
const CircleAvatar(
|
||||
backgroundImage: ExactAssetImage(
|
||||
'people/square/trevor.png',
|
||||
package: 'flutter_gallery_assets',
|
||||
),
|
||||
radius: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'by',
|
||||
style: textTheme.display3,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Text(
|
||||
'Connor Eghan',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Merriweather',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$paragraph1\n\n$paragraph2',
|
||||
style: textTheme.body2,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final ThemeData _fortnightlyTheme = _buildFortnightlyTheme();
|
||||
|
||||
ThemeData _buildFortnightlyTheme() {
|
||||
final ThemeData base = ThemeData.light();
|
||||
return base.copyWith(
|
||||
primaryTextTheme: _buildTextTheme(base.primaryTextTheme),
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
);
|
||||
}
|
||||
|
||||
TextTheme _buildTextTheme(TextTheme base) {
|
||||
TextTheme theme = base.apply(bodyColor: Colors.black);
|
||||
theme = theme.apply(displayColor: Colors.black);
|
||||
|
||||
theme = theme.copyWith(
|
||||
display1: base.display1.copyWith(
|
||||
fontFamily: 'Merriweather',
|
||||
fontStyle: FontStyle.italic,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.black,
|
||||
height: .88,
|
||||
),
|
||||
display3: base.display3.copyWith(
|
||||
fontFamily: 'LibreFranklin',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black.withAlpha(153),
|
||||
),
|
||||
headline: base.headline.copyWith(fontWeight: FontWeight.w500),
|
||||
body1: base.body1.copyWith(
|
||||
fontFamily: 'Merriweather',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w300,
|
||||
color: const Color(0xFF666666),
|
||||
height: 1.11,
|
||||
),
|
||||
body2: base.body2.copyWith(
|
||||
fontFamily: 'Merriweather',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w300,
|
||||
color: const Color(0xFF666666),
|
||||
height: 1.4,
|
||||
letterSpacing: .25,
|
||||
),
|
||||
overline: const TextStyle(
|
||||
fontFamily: 'LibreFranklin',
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black,
|
||||
),
|
||||
);
|
||||
|
||||
return theme;
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
// Copyright 2014 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../gallery/demo.dart';
|
||||
|
||||
class ImagesDemo extends StatelessWidget {
|
||||
static const String routeName = '/images';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TabbedComponentDemoScaffold(
|
||||
title: 'Animated images',
|
||||
demos: <ComponentDemoTabData>[
|
||||
ComponentDemoTabData(
|
||||
tabName: 'WEBP',
|
||||
description: '',
|
||||
exampleCodeTag: 'animated_image',
|
||||
demoWidget: Semantics(
|
||||
label: 'Example of animated WEBP',
|
||||
child: Image.asset(
|
||||
'animated_images/animated_flutter_stickers.webp',
|
||||
package: 'flutter_gallery_assets',
|
||||
),
|
||||
),
|
||||
),
|
||||
ComponentDemoTabData(
|
||||
tabName: 'GIF',
|
||||
description: '',
|
||||
exampleCodeTag: 'animated_image',
|
||||
demoWidget: Semantics(
|
||||
label: 'Example of animated GIF',
|
||||
child:Image.asset(
|
||||
'animated_images/animated_flutter_lgtm.gif',
|
||||
package: 'flutter_gallery_assets',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,411 +0,0 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// This demo displays one Category at a time. The backdrop show a list
|
||||
// of all of the categories and the selected category is displayed
|
||||
// (CategoryView) on top of the backdrop.
|
||||
|
||||
class Category {
|
||||
const Category({ this.title, this.assets });
|
||||
final String title;
|
||||
final List<String> assets;
|
||||
@override
|
||||
String toString() => '$runtimeType("$title")';
|
||||
}
|
||||
|
||||
const List<Category> allCategories = <Category>[
|
||||
Category(
|
||||
title: 'Accessories',
|
||||
assets: <String>[
|
||||
'products/belt.png',
|
||||
'products/earrings.png',
|
||||
'products/backpack.png',
|
||||
'products/hat.png',
|
||||
'products/scarf.png',
|
||||
'products/sunnies.png',
|
||||
],
|
||||
),
|
||||
Category(
|
||||
title: 'Blue',
|
||||
assets: <String>[
|
||||
'products/backpack.png',
|
||||
'products/cup.png',
|
||||
'products/napkins.png',
|
||||
'products/top.png',
|
||||
],
|
||||
),
|
||||
Category(
|
||||
title: 'Cold Weather',
|
||||
assets: <String>[
|
||||
'products/jacket.png',
|
||||
'products/jumper.png',
|
||||
'products/scarf.png',
|
||||
'products/sweater.png',
|
||||
'products/sweats.png',
|
||||
],
|
||||
),
|
||||
Category(
|
||||
title: 'Home',
|
||||
assets: <String>[
|
||||
'products/cup.png',
|
||||
'products/napkins.png',
|
||||
'products/planters.png',
|
||||
'products/table.png',
|
||||
'products/teaset.png',
|
||||
],
|
||||
),
|
||||
Category(
|
||||
title: 'Tops',
|
||||
assets: <String>[
|
||||
'products/jumper.png',
|
||||
'products/shirt.png',
|
||||
'products/sweater.png',
|
||||
'products/top.png',
|
||||
],
|
||||
),
|
||||
Category(
|
||||
title: 'Everything',
|
||||
assets: <String>[
|
||||
'products/backpack.png',
|
||||
'products/belt.png',
|
||||
'products/cup.png',
|
||||
'products/dress.png',
|
||||
'products/earrings.png',
|
||||
'products/flatwear.png',
|
||||
'products/hat.png',
|
||||
'products/jacket.png',
|
||||
'products/jumper.png',
|
||||
'products/napkins.png',
|
||||
'products/planters.png',
|
||||
'products/scarf.png',
|
||||
'products/shirt.png',
|
||||
'products/sunnies.png',
|
||||
'products/sweater.png',
|
||||
'products/sweats.png',
|
||||
'products/table.png',
|
||||
'products/teaset.png',
|
||||
'products/top.png',
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
class CategoryView extends StatelessWidget {
|
||||
const CategoryView({ Key key, this.category }) : super(key: key);
|
||||
|
||||
final Category category;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return Scrollbar(
|
||||
child: ListView(
|
||||
key: PageStorageKey<Category>(category),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16.0,
|
||||
horizontal: 64.0,
|
||||
),
|
||||
children: category.assets.map<Widget>((String asset) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Card(
|
||||
child: Container(
|
||||
width: 144.0,
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Image.asset(
|
||||
asset,
|
||||
package: 'flutter_gallery_assets',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
alignment: AlignmentDirectional.center,
|
||||
child: Text(
|
||||
asset,
|
||||
style: theme.textTheme.caption,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// One BackdropPanel is visible at a time. It's stacked on top of the
|
||||
// the BackdropDemo.
|
||||
class BackdropPanel extends StatelessWidget {
|
||||
const BackdropPanel({
|
||||
Key key,
|
||||
this.onTap,
|
||||
this.onVerticalDragUpdate,
|
||||
this.onVerticalDragEnd,
|
||||
this.title,
|
||||
this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
final VoidCallback onTap;
|
||||
final GestureDragUpdateCallback onVerticalDragUpdate;
|
||||
final GestureDragEndCallback onVerticalDragEnd;
|
||||
final Widget title;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return Material(
|
||||
elevation: 2.0,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16.0),
|
||||
topRight: Radius.circular(16.0),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onVerticalDragUpdate: onVerticalDragUpdate,
|
||||
onVerticalDragEnd: onVerticalDragEnd,
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 48.0,
|
||||
padding: const EdgeInsetsDirectional.only(start: 16.0),
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: DefaultTextStyle(
|
||||
style: theme.textTheme.subhead,
|
||||
child: Tooltip(
|
||||
message: 'Tap to dismiss',
|
||||
child: title,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(height: 1.0),
|
||||
Expanded(child: child),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Cross fades between 'Select a Category' and 'Asset Viewer'.
|
||||
class BackdropTitle extends AnimatedWidget {
|
||||
const BackdropTitle({
|
||||
Key key,
|
||||
Listenable listenable,
|
||||
}) : super(key: key, listenable: listenable);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Animation<double> animation = listenable;
|
||||
return DefaultTextStyle(
|
||||
style: Theme.of(context).primaryTextTheme.title,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Opacity(
|
||||
opacity: CurvedAnimation(
|
||||
parent: ReverseAnimation(animation),
|
||||
curve: const Interval(0.5, 1.0),
|
||||
).value,
|
||||
child: const Text('Select a Category'),
|
||||
),
|
||||
Opacity(
|
||||
opacity: CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: const Interval(0.5, 1.0),
|
||||
).value,
|
||||
child: const Text('Asset Viewer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// This widget is essentially the backdrop itself.
|
||||
class BackdropDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/backdrop';
|
||||
|
||||
@override
|
||||
_BackdropDemoState createState() => _BackdropDemoState();
|
||||
}
|
||||
|
||||
class _BackdropDemoState extends State<BackdropDemo> with SingleTickerProviderStateMixin {
|
||||
final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop');
|
||||
AnimationController _controller;
|
||||
Category _category = allCategories[0];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
value: 1.0,
|
||||
vsync: this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _changeCategory(Category category) {
|
||||
setState(() {
|
||||
_category = category;
|
||||
_controller.fling(velocity: 2.0);
|
||||
});
|
||||
}
|
||||
|
||||
bool get _backdropPanelVisible {
|
||||
final AnimationStatus status = _controller.status;
|
||||
return status == AnimationStatus.completed || status == AnimationStatus.forward;
|
||||
}
|
||||
|
||||
void _toggleBackdropPanelVisibility() {
|
||||
_controller.fling(velocity: _backdropPanelVisible ? -2.0 : 2.0);
|
||||
}
|
||||
|
||||
double get _backdropHeight {
|
||||
final RenderBox renderBox = _backdropKey.currentContext.findRenderObject();
|
||||
return renderBox.size.height;
|
||||
}
|
||||
|
||||
// By design: the panel can only be opened with a swipe. To close the panel
|
||||
// the user must either tap its heading or the backdrop's menu icon.
|
||||
|
||||
void _handleDragUpdate(DragUpdateDetails details) {
|
||||
if (_controller.isAnimating || _controller.status == AnimationStatus.completed)
|
||||
return;
|
||||
|
||||
_controller.value -= details.primaryDelta / (_backdropHeight ?? details.primaryDelta);
|
||||
}
|
||||
|
||||
void _handleDragEnd(DragEndDetails details) {
|
||||
if (_controller.isAnimating || _controller.status == AnimationStatus.completed)
|
||||
return;
|
||||
|
||||
final double flingVelocity = details.velocity.pixelsPerSecond.dy / _backdropHeight;
|
||||
if (flingVelocity < 0.0)
|
||||
_controller.fling(velocity: math.max(2.0, -flingVelocity));
|
||||
else if (flingVelocity > 0.0)
|
||||
_controller.fling(velocity: math.min(-2.0, -flingVelocity));
|
||||
else
|
||||
_controller.fling(velocity: _controller.value < 0.5 ? -2.0 : 2.0);
|
||||
}
|
||||
|
||||
// Stacks a BackdropPanel, which displays the selected category, on top
|
||||
// of the backdrop. The categories are displayed with ListTiles. Just one
|
||||
// can be selected at a time. This is a LayoutWidgetBuild function because
|
||||
// we need to know how big the BackdropPanel will be to set up its
|
||||
// animation.
|
||||
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
|
||||
const double panelTitleHeight = 48.0;
|
||||
final Size panelSize = constraints.biggest;
|
||||
final double panelTop = panelSize.height - panelTitleHeight;
|
||||
|
||||
final Animation<RelativeRect> panelAnimation = _controller.drive(
|
||||
RelativeRectTween(
|
||||
begin: RelativeRect.fromLTRB(
|
||||
0.0,
|
||||
panelTop - MediaQuery.of(context).padding.bottom,
|
||||
0.0,
|
||||
panelTop - panelSize.height,
|
||||
),
|
||||
end: const RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
|
||||
),
|
||||
);
|
||||
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final List<Widget> backdropItems = allCategories.map<Widget>((Category category) {
|
||||
final bool selected = category == _category;
|
||||
return Material(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(4.0)),
|
||||
),
|
||||
color: selected
|
||||
? Colors.white.withOpacity(0.25)
|
||||
: Colors.transparent,
|
||||
child: ListTile(
|
||||
title: Text(category.title),
|
||||
selected: selected,
|
||||
onTap: () {
|
||||
_changeCategory(category);
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return Container(
|
||||
key: _backdropKey,
|
||||
color: theme.primaryColor,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
ListTileTheme(
|
||||
iconColor: theme.primaryIconTheme.color,
|
||||
textColor: theme.primaryTextTheme.title.color.withOpacity(0.6),
|
||||
selectedColor: theme.primaryTextTheme.title.color,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: backdropItems,
|
||||
),
|
||||
),
|
||||
),
|
||||
PositionedTransition(
|
||||
rect: panelAnimation,
|
||||
child: BackdropPanel(
|
||||
onTap: _toggleBackdropPanelVisibility,
|
||||
onVerticalDragUpdate: _handleDragUpdate,
|
||||
onVerticalDragEnd: _handleDragEnd,
|
||||
title: Text(_category.title),
|
||||
child: CategoryView(category: _category),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: 0.0,
|
||||
title: BackdropTitle(
|
||||
listenable: _controller.view,
|
||||
),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
onPressed: _toggleBackdropPanelVisibility,
|
||||
icon: AnimatedIcon(
|
||||
icon: AnimatedIcons.close_menu,
|
||||
semanticLabel: 'close',
|
||||
progress: _controller.view,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: LayoutBuilder(
|
||||
builder: _buildStack,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,109 +0,0 @@
|
||||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
enum BannerDemoAction {
|
||||
reset,
|
||||
showMultipleActions,
|
||||
showLeading,
|
||||
}
|
||||
|
||||
class BannerDemo extends StatefulWidget {
|
||||
const BannerDemo({ Key key }) : super(key: key);
|
||||
|
||||
static const String routeName = '/material/banner';
|
||||
|
||||
@override
|
||||
_BannerDemoState createState() => _BannerDemoState();
|
||||
}
|
||||
|
||||
class _BannerDemoState extends State<BannerDemo> {
|
||||
static const int _numItems = 20;
|
||||
bool _displayBanner = true;
|
||||
bool _showMultipleActions = true;
|
||||
bool _showLeading = true;
|
||||
|
||||
void handleDemoAction(BannerDemoAction action) {
|
||||
setState(() {
|
||||
switch (action) {
|
||||
case BannerDemoAction.reset:
|
||||
_displayBanner = true;
|
||||
_showMultipleActions = true;
|
||||
_showLeading = true;
|
||||
break;
|
||||
case BannerDemoAction.showMultipleActions:
|
||||
_showMultipleActions = !_showMultipleActions;
|
||||
break;
|
||||
case BannerDemoAction.showLeading:
|
||||
_showLeading = !_showLeading;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Widget banner = MaterialBanner(
|
||||
content: const Text('Your password was updated on your other device. Please sign in again.'),
|
||||
leading: _showLeading ? const CircleAvatar(child: Icon(Icons.access_alarm)) : null,
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
child: const Text('SIGN IN'),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_displayBanner = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (_showMultipleActions)
|
||||
FlatButton(
|
||||
child: const Text('DISMISS'),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_displayBanner = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Banner'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(BannerDemo.routeName),
|
||||
PopupMenuButton<BannerDemoAction>(
|
||||
onSelected: handleDemoAction,
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry<BannerDemoAction>>[
|
||||
const PopupMenuItem<BannerDemoAction>(
|
||||
value: BannerDemoAction.reset,
|
||||
child: Text('Reset the banner'),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
CheckedPopupMenuItem<BannerDemoAction>(
|
||||
value: BannerDemoAction.showMultipleActions,
|
||||
checked: _showMultipleActions,
|
||||
child: const Text('Multiple actions'),
|
||||
),
|
||||
CheckedPopupMenuItem<BannerDemoAction>(
|
||||
value: BannerDemoAction.showLeading,
|
||||
checked: _showLeading,
|
||||
child: const Text('Leading icon'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView.builder(itemCount: _displayBanner ? _numItems + 1 : _numItems, itemBuilder: (BuildContext context, int index) {
|
||||
if (index == 0 && _displayBanner) {
|
||||
return banner;
|
||||
}
|
||||
return ListTile(title: Text('Item ${_displayBanner ? index : index + 1}'),);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,515 +0,0 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class BottomAppBarDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/bottom_app_bar';
|
||||
|
||||
@override
|
||||
State createState() => _BottomAppBarDemoState();
|
||||
}
|
||||
|
||||
// Flutter generally frowns upon abbrevation however this class uses two
|
||||
// abbrevations extensively: "fab" for floating action button, and "bab"
|
||||
// for bottom application bar.
|
||||
|
||||
class _BottomAppBarDemoState extends State<BottomAppBarDemo> {
|
||||
static final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
// FAB shape
|
||||
|
||||
static const _ChoiceValue<Widget> kNoFab = _ChoiceValue<Widget>(
|
||||
title: 'None',
|
||||
label: 'do not show a floating action button',
|
||||
value: null,
|
||||
);
|
||||
|
||||
static const _ChoiceValue<Widget> kCircularFab = _ChoiceValue<Widget>(
|
||||
title: 'Circular',
|
||||
label: 'circular floating action button',
|
||||
value: FloatingActionButton(
|
||||
onPressed: _showSnackbar,
|
||||
child: Icon(Icons.add, semanticLabel: 'Action'),
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
);
|
||||
|
||||
static const _ChoiceValue<Widget> kDiamondFab = _ChoiceValue<Widget>(
|
||||
title: 'Diamond',
|
||||
label: 'diamond shape floating action button',
|
||||
value: _DiamondFab(
|
||||
onPressed: _showSnackbar,
|
||||
child: Icon(Icons.add, semanticLabel: 'Action'),
|
||||
),
|
||||
);
|
||||
|
||||
// Notch
|
||||
|
||||
static const _ChoiceValue<bool> kShowNotchTrue = _ChoiceValue<bool>(
|
||||
title: 'On',
|
||||
label: 'show bottom appbar notch',
|
||||
value: true,
|
||||
);
|
||||
|
||||
static const _ChoiceValue<bool> kShowNotchFalse = _ChoiceValue<bool>(
|
||||
title: 'Off',
|
||||
label: 'do not show bottom appbar notch',
|
||||
value: false,
|
||||
);
|
||||
|
||||
// FAB Position
|
||||
|
||||
static const _ChoiceValue<FloatingActionButtonLocation> kFabEndDocked = _ChoiceValue<FloatingActionButtonLocation>(
|
||||
title: 'Attached - End',
|
||||
label: 'floating action button is docked at the end of the bottom app bar',
|
||||
value: FloatingActionButtonLocation.endDocked,
|
||||
);
|
||||
|
||||
static const _ChoiceValue<FloatingActionButtonLocation> kFabCenterDocked = _ChoiceValue<FloatingActionButtonLocation>(
|
||||
title: 'Attached - Center',
|
||||
label: 'floating action button is docked at the center of the bottom app bar',
|
||||
value: FloatingActionButtonLocation.centerDocked,
|
||||
);
|
||||
|
||||
static const _ChoiceValue<FloatingActionButtonLocation> kFabEndFloat= _ChoiceValue<FloatingActionButtonLocation>(
|
||||
title: 'Free - End',
|
||||
label: 'floating action button floats above the end of the bottom app bar',
|
||||
value: FloatingActionButtonLocation.endFloat,
|
||||
);
|
||||
|
||||
static const _ChoiceValue<FloatingActionButtonLocation> kFabCenterFloat = _ChoiceValue<FloatingActionButtonLocation>(
|
||||
title: 'Free - Center',
|
||||
label: 'floating action button is floats above the center of the bottom app bar',
|
||||
value: FloatingActionButtonLocation.centerFloat,
|
||||
);
|
||||
|
||||
static void _showSnackbar() {
|
||||
const String text =
|
||||
"When the Scaffold's floating action button location changes, "
|
||||
'the floating action button animates to its new position.'
|
||||
'The BottomAppBar adapts its shape appropriately.';
|
||||
_scaffoldKey.currentState.showSnackBar(
|
||||
const SnackBar(content: Text(text)),
|
||||
);
|
||||
}
|
||||
|
||||
// App bar color
|
||||
|
||||
static const List<_NamedColor> kBabColors = <_NamedColor>[
|
||||
_NamedColor(null, 'Clear'),
|
||||
_NamedColor(Color(0xFFFFC100), 'Orange'),
|
||||
_NamedColor(Color(0xFF91FAFF), 'Light Blue'),
|
||||
_NamedColor(Color(0xFF00D1FF), 'Cyan'),
|
||||
_NamedColor(Color(0xFF00BCFF), 'Cerulean'),
|
||||
_NamedColor(Color(0xFF009BEE), 'Blue'),
|
||||
];
|
||||
|
||||
_ChoiceValue<Widget> _fabShape = kCircularFab;
|
||||
_ChoiceValue<bool> _showNotch = kShowNotchTrue;
|
||||
_ChoiceValue<FloatingActionButtonLocation> _fabLocation = kFabEndDocked;
|
||||
Color _babColor = kBabColors.first.color;
|
||||
|
||||
void _onShowNotchChanged(_ChoiceValue<bool> value) {
|
||||
setState(() {
|
||||
_showNotch = value;
|
||||
});
|
||||
}
|
||||
|
||||
void _onFabShapeChanged(_ChoiceValue<Widget> value) {
|
||||
setState(() {
|
||||
_fabShape = value;
|
||||
});
|
||||
}
|
||||
|
||||
void _onFabLocationChanged(_ChoiceValue<FloatingActionButtonLocation> value) {
|
||||
setState(() {
|
||||
_fabLocation = value;
|
||||
});
|
||||
}
|
||||
|
||||
void _onBabColorChanged(Color value) {
|
||||
setState(() {
|
||||
_babColor = value;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: const Text('Bottom app bar'),
|
||||
elevation: 0.0,
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(BottomAppBarDemo.routeName),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.sentiment_very_satisfied, semanticLabel: 'Update shape'),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_fabShape = _fabShape == kCircularFab ? kDiamondFab : kCircularFab;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Scrollbar(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.only(bottom: 88.0),
|
||||
children: <Widget>[
|
||||
const _Heading('FAB Shape'),
|
||||
|
||||
_RadioItem<Widget>(kCircularFab, _fabShape, _onFabShapeChanged),
|
||||
_RadioItem<Widget>(kDiamondFab, _fabShape, _onFabShapeChanged),
|
||||
_RadioItem<Widget>(kNoFab, _fabShape, _onFabShapeChanged),
|
||||
|
||||
const Divider(),
|
||||
const _Heading('Notch'),
|
||||
|
||||
_RadioItem<bool>(kShowNotchTrue, _showNotch, _onShowNotchChanged),
|
||||
_RadioItem<bool>(kShowNotchFalse, _showNotch, _onShowNotchChanged),
|
||||
|
||||
const Divider(),
|
||||
const _Heading('FAB Position'),
|
||||
|
||||
_RadioItem<FloatingActionButtonLocation>(kFabEndDocked, _fabLocation, _onFabLocationChanged),
|
||||
_RadioItem<FloatingActionButtonLocation>(kFabCenterDocked, _fabLocation, _onFabLocationChanged),
|
||||
_RadioItem<FloatingActionButtonLocation>(kFabEndFloat, _fabLocation, _onFabLocationChanged),
|
||||
_RadioItem<FloatingActionButtonLocation>(kFabCenterFloat, _fabLocation, _onFabLocationChanged),
|
||||
|
||||
const Divider(),
|
||||
const _Heading('App bar color'),
|
||||
|
||||
_ColorsItem(kBabColors, _babColor, _onBabColorChanged),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: _fabShape.value,
|
||||
floatingActionButtonLocation: _fabLocation.value,
|
||||
bottomNavigationBar: _DemoBottomAppBar(
|
||||
color: _babColor,
|
||||
fabLocation: _fabLocation.value,
|
||||
shape: _selectNotch(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
NotchedShape _selectNotch() {
|
||||
if (!_showNotch.value)
|
||||
return null;
|
||||
if (_fabShape == kCircularFab)
|
||||
return const CircularNotchedRectangle();
|
||||
if (_fabShape == kDiamondFab)
|
||||
return const _DiamondNotchedRectangle();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class _ChoiceValue<T> {
|
||||
const _ChoiceValue({ this.value, this.title, this.label });
|
||||
|
||||
final T value;
|
||||
final String title;
|
||||
final String label; // For the Semantics widget that contains title
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType("$title")';
|
||||
}
|
||||
|
||||
class _RadioItem<T> extends StatelessWidget {
|
||||
const _RadioItem(this.value, this.groupValue, this.onChanged);
|
||||
|
||||
final _ChoiceValue<T> value;
|
||||
final _ChoiceValue<T> groupValue;
|
||||
final ValueChanged<_ChoiceValue<T>> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return Container(
|
||||
height: 56.0,
|
||||
padding: const EdgeInsetsDirectional.only(start: 16.0),
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: MergeSemantics(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Radio<_ChoiceValue<T>>(
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
Expanded(
|
||||
child: Semantics(
|
||||
container: true,
|
||||
button: true,
|
||||
label: value.label,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
onChanged(value);
|
||||
},
|
||||
child: Text(
|
||||
value.title,
|
||||
style: theme.textTheme.subhead,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NamedColor {
|
||||
const _NamedColor(this.color, this.name);
|
||||
|
||||
final Color color;
|
||||
final String name;
|
||||
}
|
||||
|
||||
class _ColorsItem extends StatelessWidget {
|
||||
const _ColorsItem(this.colors, this.selectedColor, this.onChanged);
|
||||
|
||||
final List<_NamedColor> colors;
|
||||
final Color selectedColor;
|
||||
final ValueChanged<Color> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: colors.map<Widget>((_NamedColor namedColor) {
|
||||
return RawMaterialButton(
|
||||
onPressed: () {
|
||||
onChanged(namedColor.color);
|
||||
},
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
width: 32.0,
|
||||
height: 32.0,
|
||||
),
|
||||
fillColor: namedColor.color,
|
||||
shape: CircleBorder(
|
||||
side: BorderSide(
|
||||
color: namedColor.color == selectedColor ? Colors.black : const Color(0xFFD5D7DA),
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
child: Semantics(
|
||||
value: namedColor.name,
|
||||
selected: namedColor.color == selectedColor,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Heading extends StatelessWidget {
|
||||
const _Heading(this.text);
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return Container(
|
||||
height: 48.0,
|
||||
padding: const EdgeInsetsDirectional.only(start: 56.0),
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(
|
||||
text,
|
||||
style: theme.textTheme.body1.copyWith(
|
||||
color: theme.primaryColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DemoBottomAppBar extends StatelessWidget {
|
||||
const _DemoBottomAppBar({
|
||||
this.color,
|
||||
this.fabLocation,
|
||||
this.shape,
|
||||
});
|
||||
|
||||
final Color color;
|
||||
final FloatingActionButtonLocation fabLocation;
|
||||
final NotchedShape shape;
|
||||
|
||||
static final List<FloatingActionButtonLocation> kCenterLocations = <FloatingActionButtonLocation>[
|
||||
FloatingActionButtonLocation.centerDocked,
|
||||
FloatingActionButtonLocation.centerFloat,
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BottomAppBar(
|
||||
color: color,
|
||||
shape: shape,
|
||||
child: Row(children: <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.menu, semanticLabel: 'Show bottom sheet'),
|
||||
onPressed: () {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => const _DemoDrawer(),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (kCenterLocations.contains(fabLocation)) const Expanded(child: SizedBox()),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search, semanticLabel: 'show search action',),
|
||||
onPressed: () {
|
||||
Scaffold.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('This is a dummy search action.')),
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Theme.of(context).platform == TargetPlatform.iOS
|
||||
? Icons.more_horiz
|
||||
: Icons.more_vert,
|
||||
semanticLabel: 'Show menu actions',
|
||||
),
|
||||
onPressed: () {
|
||||
Scaffold.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('This is a dummy menu action.')),
|
||||
);
|
||||
},
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// A drawer that pops up from the bottom of the screen.
|
||||
class _DemoDrawer extends StatelessWidget {
|
||||
const _DemoDrawer();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Drawer(
|
||||
child: Column(
|
||||
children: const <Widget>[
|
||||
ListTile(
|
||||
leading: Icon(Icons.search),
|
||||
title: Text('Search'),
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(Icons.threed_rotation),
|
||||
title: Text('3D'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// A diamond-shaped floating action button.
|
||||
class _DiamondFab extends StatelessWidget {
|
||||
const _DiamondFab({
|
||||
this.child,
|
||||
this.onPressed,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
shape: const _DiamondBorder(),
|
||||
color: Colors.orange,
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
child: Container(
|
||||
width: 56.0,
|
||||
height: 56.0,
|
||||
child: IconTheme.merge(
|
||||
data: IconThemeData(color: Theme.of(context).accentIconTheme.color),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
elevation: 6.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DiamondNotchedRectangle implements NotchedShape {
|
||||
const _DiamondNotchedRectangle();
|
||||
|
||||
@override
|
||||
Path getOuterPath(Rect host, Rect guest) {
|
||||
if (!host.overlaps(guest))
|
||||
return Path()..addRect(host);
|
||||
assert(guest.width > 0.0);
|
||||
|
||||
final Rect intersection = guest.intersect(host);
|
||||
// We are computing a "V" shaped notch, as in this diagram:
|
||||
// -----\**** /-----
|
||||
// \ /
|
||||
// \ /
|
||||
// \ /
|
||||
//
|
||||
// "-" marks the top edge of the bottom app bar.
|
||||
// "\" and "/" marks the notch outline
|
||||
//
|
||||
// notchToCenter is the horizontal distance between the guest's center and
|
||||
// the host's top edge where the notch starts (marked with "*").
|
||||
// We compute notchToCenter by similar triangles:
|
||||
final double notchToCenter =
|
||||
intersection.height * (guest.height / 2.0)
|
||||
/ (guest.width / 2.0);
|
||||
|
||||
return Path()
|
||||
..moveTo(host.left, host.top)
|
||||
..lineTo(guest.center.dx - notchToCenter, host.top)
|
||||
..lineTo(guest.left + guest.width / 2.0, guest.bottom)
|
||||
..lineTo(guest.center.dx + notchToCenter, host.top)
|
||||
..lineTo(host.right, host.top)
|
||||
..lineTo(host.right, host.bottom)
|
||||
..lineTo(host.left, host.bottom)
|
||||
..close();
|
||||
}
|
||||
}
|
||||
|
||||
class _DiamondBorder extends ShapeBorder {
|
||||
const _DiamondBorder();
|
||||
|
||||
@override
|
||||
EdgeInsetsGeometry get dimensions {
|
||||
return const EdgeInsets.only();
|
||||
}
|
||||
|
||||
@override
|
||||
Path getInnerPath(Rect rect, { TextDirection textDirection }) {
|
||||
return getOuterPath(rect, textDirection: textDirection);
|
||||
}
|
||||
|
||||
@override
|
||||
Path getOuterPath(Rect rect, { TextDirection textDirection }) {
|
||||
return Path()
|
||||
..moveTo(rect.left + rect.width / 2.0, rect.top)
|
||||
..lineTo(rect.right, rect.top + rect.height / 2.0)
|
||||
..lineTo(rect.left + rect.width / 2.0, rect.bottom)
|
||||
..lineTo(rect.left, rect.top + rect.height / 2.0)
|
||||
..close();
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Rect rect, { TextDirection textDirection }) { }
|
||||
|
||||
// This border doesn't support scaling.
|
||||
@override
|
||||
ShapeBorder scale(double t) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -1,230 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class NavigationIconView {
|
||||
NavigationIconView({
|
||||
Widget icon,
|
||||
Widget activeIcon,
|
||||
String title,
|
||||
Color color,
|
||||
TickerProvider vsync,
|
||||
}) : _icon = icon,
|
||||
_color = color,
|
||||
_title = title,
|
||||
item = BottomNavigationBarItem(
|
||||
icon: icon,
|
||||
activeIcon: activeIcon,
|
||||
title: Text(title),
|
||||
backgroundColor: color,
|
||||
),
|
||||
controller = AnimationController(
|
||||
duration: kThemeAnimationDuration,
|
||||
vsync: vsync,
|
||||
) {
|
||||
_animation = controller.drive(CurveTween(
|
||||
curve: const Interval(0.5, 1.0, curve: Curves.fastOutSlowIn),
|
||||
));
|
||||
}
|
||||
|
||||
final Widget _icon;
|
||||
final Color _color;
|
||||
final String _title;
|
||||
final BottomNavigationBarItem item;
|
||||
final AnimationController controller;
|
||||
Animation<double> _animation;
|
||||
|
||||
FadeTransition transition(BottomNavigationBarType type, BuildContext context) {
|
||||
Color iconColor;
|
||||
if (type == BottomNavigationBarType.shifting) {
|
||||
iconColor = _color;
|
||||
} else {
|
||||
final ThemeData themeData = Theme.of(context);
|
||||
iconColor = themeData.brightness == Brightness.light
|
||||
? themeData.primaryColor
|
||||
: themeData.accentColor;
|
||||
}
|
||||
|
||||
return FadeTransition(
|
||||
opacity: _animation,
|
||||
child: SlideTransition(
|
||||
position: _animation.drive(
|
||||
Tween<Offset>(
|
||||
begin: const Offset(0.0, 0.02), // Slightly down.
|
||||
end: Offset.zero,
|
||||
),
|
||||
),
|
||||
child: IconTheme(
|
||||
data: IconThemeData(
|
||||
color: iconColor,
|
||||
size: 120.0,
|
||||
),
|
||||
child: Semantics(
|
||||
label: 'Placeholder for $_title tab',
|
||||
child: _icon,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CustomIcon extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final IconThemeData iconTheme = IconTheme.of(context);
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(4.0),
|
||||
width: iconTheme.size - 8.0,
|
||||
height: iconTheme.size - 8.0,
|
||||
color: iconTheme.color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CustomInactiveIcon extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final IconThemeData iconTheme = IconTheme.of(context);
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(4.0),
|
||||
width: iconTheme.size - 8.0,
|
||||
height: iconTheme.size - 8.0,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: iconTheme.color, width: 2.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BottomNavigationDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/bottom_navigation';
|
||||
|
||||
@override
|
||||
_BottomNavigationDemoState createState() => _BottomNavigationDemoState();
|
||||
}
|
||||
|
||||
class _BottomNavigationDemoState extends State<BottomNavigationDemo>
|
||||
with TickerProviderStateMixin {
|
||||
int _currentIndex = 0;
|
||||
BottomNavigationBarType _type = BottomNavigationBarType.shifting;
|
||||
List<NavigationIconView> _navigationViews;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_navigationViews = <NavigationIconView>[
|
||||
NavigationIconView(
|
||||
icon: const Icon(Icons.access_alarm),
|
||||
title: 'Alarm',
|
||||
color: Colors.deepPurple,
|
||||
vsync: this,
|
||||
),
|
||||
NavigationIconView(
|
||||
activeIcon: CustomIcon(),
|
||||
icon: CustomInactiveIcon(),
|
||||
title: 'Box',
|
||||
color: Colors.deepOrange,
|
||||
vsync: this,
|
||||
),
|
||||
NavigationIconView(
|
||||
activeIcon: const Icon(Icons.cloud),
|
||||
icon: const Icon(Icons.cloud_queue),
|
||||
title: 'Cloud',
|
||||
color: Colors.teal,
|
||||
vsync: this,
|
||||
),
|
||||
NavigationIconView(
|
||||
activeIcon: const Icon(Icons.favorite),
|
||||
icon: const Icon(Icons.favorite_border),
|
||||
title: 'Favorites',
|
||||
color: Colors.indigo,
|
||||
vsync: this,
|
||||
),
|
||||
NavigationIconView(
|
||||
icon: const Icon(Icons.event_available),
|
||||
title: 'Event',
|
||||
color: Colors.pink,
|
||||
vsync: this,
|
||||
),
|
||||
];
|
||||
|
||||
_navigationViews[_currentIndex].controller.value = 1.0;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (NavigationIconView view in _navigationViews)
|
||||
view.controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _buildTransitionsStack() {
|
||||
final List<FadeTransition> transitions = <FadeTransition>[
|
||||
for (NavigationIconView view in _navigationViews) view.transition(_type, context),
|
||||
];
|
||||
|
||||
// We want to have the newly animating (fading in) views on top.
|
||||
transitions.sort((FadeTransition a, FadeTransition b) {
|
||||
final Animation<double> aAnimation = a.opacity;
|
||||
final Animation<double> bAnimation = b.opacity;
|
||||
final double aValue = aAnimation.value;
|
||||
final double bValue = bAnimation.value;
|
||||
return aValue.compareTo(bValue);
|
||||
});
|
||||
|
||||
return Stack(children: transitions);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final BottomNavigationBar botNavBar = BottomNavigationBar(
|
||||
items: _navigationViews
|
||||
.map<BottomNavigationBarItem>((NavigationIconView navigationView) => navigationView.item)
|
||||
.toList(),
|
||||
currentIndex: _currentIndex,
|
||||
type: _type,
|
||||
onTap: (int index) {
|
||||
setState(() {
|
||||
_navigationViews[_currentIndex].controller.reverse();
|
||||
_currentIndex = index;
|
||||
_navigationViews[_currentIndex].controller.forward();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Bottom navigation'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(BottomNavigationDemo.routeName),
|
||||
PopupMenuButton<BottomNavigationBarType>(
|
||||
onSelected: (BottomNavigationBarType value) {
|
||||
setState(() {
|
||||
_type = value;
|
||||
});
|
||||
},
|
||||
itemBuilder: (BuildContext context) => <PopupMenuItem<BottomNavigationBarType>>[
|
||||
const PopupMenuItem<BottomNavigationBarType>(
|
||||
value: BottomNavigationBarType.fixed,
|
||||
child: Text('Fixed'),
|
||||
),
|
||||
const PopupMenuItem<BottomNavigationBarType>(
|
||||
value: BottomNavigationBarType.shifting,
|
||||
child: Text('Shifting'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Center(
|
||||
child: _buildTransitionsStack(),
|
||||
),
|
||||
bottomNavigationBar: botNavBar,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,386 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
const String _raisedText =
|
||||
'Raised buttons add dimension to mostly flat layouts. They emphasize '
|
||||
'functions on busy or wide spaces.';
|
||||
|
||||
const String _raisedCode = 'buttons_raised';
|
||||
|
||||
const String _flatText = 'A flat button displays an ink splash on press '
|
||||
'but does not lift. Use flat buttons on toolbars, in dialogs and '
|
||||
'inline with padding';
|
||||
|
||||
const String _flatCode = 'buttons_flat';
|
||||
|
||||
const String _outlineText =
|
||||
'Outline buttons become opaque and elevate when pressed. They are often '
|
||||
'paired with raised buttons to indicate an alternative, secondary action.';
|
||||
|
||||
const String _outlineCode = 'buttons_outline';
|
||||
|
||||
const String _dropdownText =
|
||||
'A dropdown button displays a menu that\'s used to select a value from a '
|
||||
'small set of values. The button displays the current value and a down '
|
||||
'arrow.';
|
||||
|
||||
const String _dropdownCode = 'buttons_dropdown';
|
||||
|
||||
const String _iconText =
|
||||
'IconButtons are appropriate for toggle buttons that allow a single choice '
|
||||
'to be selected or deselected, such as adding or removing an item\'s star.';
|
||||
|
||||
const String _iconCode = 'buttons_icon';
|
||||
|
||||
const String _actionText =
|
||||
'Floating action buttons are used for a promoted action. They are '
|
||||
'distinguished by a circled icon floating above the UI and can have motion '
|
||||
'behaviors that include morphing, launching, and a transferring anchor '
|
||||
'point.';
|
||||
|
||||
const String _actionCode = 'buttons_action';
|
||||
|
||||
class ButtonsDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/buttons';
|
||||
|
||||
@override
|
||||
_ButtonsDemoState createState() => _ButtonsDemoState();
|
||||
}
|
||||
|
||||
class _ButtonsDemoState extends State<ButtonsDemo> {
|
||||
ShapeBorder _buttonShape;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ButtonThemeData buttonTheme = ButtonTheme.of(context).copyWith(
|
||||
shape: _buttonShape
|
||||
);
|
||||
|
||||
final List<ComponentDemoTabData> demos = <ComponentDemoTabData>[
|
||||
ComponentDemoTabData(
|
||||
tabName: 'RAISED',
|
||||
description: _raisedText,
|
||||
demoWidget: ButtonTheme.fromButtonThemeData(
|
||||
data: buttonTheme,
|
||||
child: buildRaisedButton(),
|
||||
),
|
||||
exampleCodeTag: _raisedCode,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/RaisedButton-class.html',
|
||||
),
|
||||
ComponentDemoTabData(
|
||||
tabName: 'FLAT',
|
||||
description: _flatText,
|
||||
demoWidget: ButtonTheme.fromButtonThemeData(
|
||||
data: buttonTheme,
|
||||
child: buildFlatButton(),
|
||||
),
|
||||
exampleCodeTag: _flatCode,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/FlatButton-class.html',
|
||||
),
|
||||
ComponentDemoTabData(
|
||||
tabName: 'OUTLINE',
|
||||
description: _outlineText,
|
||||
demoWidget: ButtonTheme.fromButtonThemeData(
|
||||
data: buttonTheme,
|
||||
child: buildOutlineButton(),
|
||||
),
|
||||
exampleCodeTag: _outlineCode,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/OutlineButton-class.html',
|
||||
),
|
||||
ComponentDemoTabData(
|
||||
tabName: 'DROPDOWN',
|
||||
description: _dropdownText,
|
||||
demoWidget: buildDropdownButton(),
|
||||
exampleCodeTag: _dropdownCode,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/DropdownButton-class.html',
|
||||
),
|
||||
ComponentDemoTabData(
|
||||
tabName: 'ICON',
|
||||
description: _iconText,
|
||||
demoWidget: buildIconButton(),
|
||||
exampleCodeTag: _iconCode,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/IconButton-class.html',
|
||||
),
|
||||
ComponentDemoTabData(
|
||||
tabName: 'ACTION',
|
||||
description: _actionText,
|
||||
demoWidget: buildActionButton(),
|
||||
exampleCodeTag: _actionCode,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/FloatingActionButton-class.html',
|
||||
),
|
||||
];
|
||||
|
||||
return TabbedComponentDemoScaffold(
|
||||
title: 'Buttons',
|
||||
demos: demos,
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.sentiment_very_satisfied, semanticLabel: 'Update shape'),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_buttonShape = _buttonShape == null ? const StadiumBorder() : null;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildRaisedButton() {
|
||||
return Align(
|
||||
alignment: const Alignment(0.0, -0.2),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
ButtonBar(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
RaisedButton(
|
||||
child: const Text('RAISED BUTTON', semanticsLabel: 'RAISED BUTTON 1'),
|
||||
onPressed: () {
|
||||
// Perform some action
|
||||
},
|
||||
),
|
||||
const RaisedButton(
|
||||
child: Text('DISABLED', semanticsLabel: 'DISABLED BUTTON 1'),
|
||||
onPressed: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
ButtonBar(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
RaisedButton.icon(
|
||||
icon: const Icon(Icons.add, size: 18.0),
|
||||
label: const Text('RAISED BUTTON', semanticsLabel: 'RAISED BUTTON 2'),
|
||||
onPressed: () {
|
||||
// Perform some action
|
||||
},
|
||||
),
|
||||
RaisedButton.icon(
|
||||
icon: const Icon(Icons.add, size: 18.0),
|
||||
label: const Text('DISABLED', semanticsLabel: 'DISABLED BUTTON 2'),
|
||||
onPressed: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildFlatButton() {
|
||||
return Align(
|
||||
alignment: const Alignment(0.0, -0.2),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
ButtonBar(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
FlatButton(
|
||||
child: const Text('FLAT BUTTON', semanticsLabel: 'FLAT BUTTON 1'),
|
||||
onPressed: () {
|
||||
// Perform some action
|
||||
},
|
||||
),
|
||||
const FlatButton(
|
||||
child: Text('DISABLED', semanticsLabel: 'DISABLED BUTTON 3',),
|
||||
onPressed: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
ButtonBar(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
FlatButton.icon(
|
||||
icon: const Icon(Icons.add_circle_outline, size: 18.0),
|
||||
label: const Text('FLAT BUTTON', semanticsLabel: 'FLAT BUTTON 2'),
|
||||
onPressed: () {
|
||||
// Perform some action
|
||||
},
|
||||
),
|
||||
FlatButton.icon(
|
||||
icon: const Icon(Icons.add_circle_outline, size: 18.0),
|
||||
label: const Text('DISABLED', semanticsLabel: 'DISABLED BUTTON 4'),
|
||||
onPressed: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildOutlineButton() {
|
||||
return Align(
|
||||
alignment: const Alignment(0.0, -0.2),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
ButtonBar(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
OutlineButton(
|
||||
child: const Text('OUTLINE BUTTON', semanticsLabel: 'OUTLINE BUTTON 1'),
|
||||
onPressed: () {
|
||||
// Perform some action
|
||||
},
|
||||
),
|
||||
const OutlineButton(
|
||||
child: Text('DISABLED', semanticsLabel: 'DISABLED BUTTON 5'),
|
||||
onPressed: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
ButtonBar(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
OutlineButton.icon(
|
||||
icon: const Icon(Icons.add, size: 18.0),
|
||||
label: const Text('OUTLINE BUTTON', semanticsLabel: 'OUTLINE BUTTON 2'),
|
||||
onPressed: () {
|
||||
// Perform some action
|
||||
},
|
||||
),
|
||||
OutlineButton.icon(
|
||||
icon: const Icon(Icons.add, size: 18.0),
|
||||
label: const Text('DISABLED', semanticsLabel: 'DISABLED BUTTON 6'),
|
||||
onPressed: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// https://en.wikipedia.org/wiki/Free_Four
|
||||
String dropdown1Value = 'Free';
|
||||
String dropdown2Value;
|
||||
String dropdown3Value = 'Four';
|
||||
|
||||
Widget buildDropdownButton() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: const Text('Simple dropdown:'),
|
||||
trailing: DropdownButton<String>(
|
||||
value: dropdown1Value,
|
||||
onChanged: (String newValue) {
|
||||
setState(() {
|
||||
dropdown1Value = newValue;
|
||||
});
|
||||
},
|
||||
items: <String>['One', 'Two', 'Free', 'Four'].map<DropdownMenuItem<String>>((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24.0,
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Dropdown with a hint:'),
|
||||
trailing: DropdownButton<String>(
|
||||
value: dropdown2Value,
|
||||
hint: const Text('Choose'),
|
||||
onChanged: (String newValue) {
|
||||
setState(() {
|
||||
dropdown2Value = newValue;
|
||||
});
|
||||
},
|
||||
items: <String>['One', 'Two', 'Free', 'Four'].map<DropdownMenuItem<String>>((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24.0,
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Scrollable dropdown:'),
|
||||
trailing: DropdownButton<String>(
|
||||
value: dropdown3Value,
|
||||
onChanged: (String newValue) {
|
||||
setState(() {
|
||||
dropdown3Value = newValue;
|
||||
});
|
||||
},
|
||||
items: <String>[
|
||||
'One', 'Two', 'Free', 'Four', 'Can', 'I', 'Have', 'A', 'Little',
|
||||
'Bit', 'More', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten',
|
||||
]
|
||||
.map<DropdownMenuItem<String>>((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value),
|
||||
);
|
||||
})
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool iconButtonToggle = false;
|
||||
|
||||
Widget buildIconButton() {
|
||||
return Align(
|
||||
alignment: const Alignment(0.0, -0.2),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.thumb_up,
|
||||
semanticLabel: 'Thumbs up',
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() => iconButtonToggle = !iconButtonToggle);
|
||||
},
|
||||
color: iconButtonToggle ? Theme.of(context).primaryColor : null,
|
||||
),
|
||||
const IconButton(
|
||||
icon: Icon(
|
||||
Icons.thumb_up,
|
||||
semanticLabel: 'Thumbs not up',
|
||||
),
|
||||
onPressed: null,
|
||||
),
|
||||
]
|
||||
.map<Widget>((Widget button) => SizedBox(width: 64.0, height: 64.0, child: button))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildActionButton() {
|
||||
return Align(
|
||||
alignment: const Alignment(0.0, -0.2),
|
||||
child: FloatingActionButton(
|
||||
child: const Icon(Icons.add),
|
||||
onPressed: () {
|
||||
// Perform some action
|
||||
},
|
||||
tooltip: 'floating action button',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,409 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
|
||||
|
||||
enum CardDemoType {
|
||||
standard,
|
||||
tappable,
|
||||
selectable,
|
||||
}
|
||||
|
||||
class TravelDestination {
|
||||
const TravelDestination({
|
||||
@required this.assetName,
|
||||
@required this.assetPackage,
|
||||
@required this.title,
|
||||
@required this.description,
|
||||
@required this.city,
|
||||
@required this.location,
|
||||
this.type = CardDemoType.standard,
|
||||
}) : assert(assetName != null),
|
||||
assert(assetPackage != null),
|
||||
assert(title != null),
|
||||
assert(description != null),
|
||||
assert(city != null),
|
||||
assert(location != null);
|
||||
|
||||
final String assetName;
|
||||
final String assetPackage;
|
||||
final String title;
|
||||
final String description;
|
||||
final String city;
|
||||
final String location;
|
||||
final CardDemoType type;
|
||||
}
|
||||
|
||||
const List<TravelDestination> destinations = <TravelDestination>[
|
||||
TravelDestination(
|
||||
assetName: 'places/india_thanjavur_market.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Top 10 Cities to Visit in Tamil Nadu',
|
||||
description: 'Number 10',
|
||||
city: 'Thanjavur',
|
||||
location: 'Thanjavur, Tamil Nadu',
|
||||
),
|
||||
TravelDestination(
|
||||
assetName: 'places/india_chettinad_silk_maker.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Artisans of Southern India',
|
||||
description: 'Silk Spinners',
|
||||
city: 'Chettinad',
|
||||
location: 'Sivaganga, Tamil Nadu',
|
||||
type: CardDemoType.tappable,
|
||||
),
|
||||
TravelDestination(
|
||||
assetName: 'places/india_tanjore_thanjavur_temple.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Brihadisvara Temple',
|
||||
description: 'Temples',
|
||||
city: 'Thanjavur',
|
||||
location: 'Thanjavur, Tamil Nadu',
|
||||
type: CardDemoType.selectable,
|
||||
),
|
||||
];
|
||||
|
||||
class TravelDestinationItem extends StatelessWidget {
|
||||
const TravelDestinationItem({ Key key, @required this.destination, this.shape })
|
||||
: assert(destination != null),
|
||||
super(key: key);
|
||||
|
||||
// This height will allow for all the Card's content to fit comfortably within the card.
|
||||
static const double height = 338.0;
|
||||
final TravelDestination destination;
|
||||
final ShapeBorder shape;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
const SectionTitle(title: 'Normal'),
|
||||
SizedBox(
|
||||
height: height,
|
||||
child: Card(
|
||||
// This ensures that the Card's children are clipped correctly.
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: shape,
|
||||
child: TravelDestinationContent(destination: destination),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TappableTravelDestinationItem extends StatelessWidget {
|
||||
const TappableTravelDestinationItem({ Key key, @required this.destination, this.shape })
|
||||
: assert(destination != null),
|
||||
super(key: key);
|
||||
|
||||
// This height will allow for all the Card's content to fit comfortably within the card.
|
||||
static const double height = 298.0;
|
||||
final TravelDestination destination;
|
||||
final ShapeBorder shape;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
const SectionTitle(title: 'Tappable'),
|
||||
SizedBox(
|
||||
height: height,
|
||||
child: Card(
|
||||
// This ensures that the Card's children (including the ink splash) are clipped correctly.
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: shape,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
print('Card was tapped');
|
||||
},
|
||||
// Generally, material cards use onSurface with 12% opacity for the pressed state.
|
||||
splashColor: Theme.of(context).colorScheme.onSurface.withOpacity(0.12),
|
||||
// Generally, material cards do not have a highlight overlay.
|
||||
highlightColor: Colors.transparent,
|
||||
child: TravelDestinationContent(destination: destination),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SelectableTravelDestinationItem extends StatefulWidget {
|
||||
const SelectableTravelDestinationItem({ Key key, @required this.destination, this.shape })
|
||||
: assert(destination != null),
|
||||
super(key: key);
|
||||
|
||||
final TravelDestination destination;
|
||||
final ShapeBorder shape;
|
||||
|
||||
@override
|
||||
_SelectableTravelDestinationItemState createState() => _SelectableTravelDestinationItemState();
|
||||
}
|
||||
|
||||
class _SelectableTravelDestinationItemState extends State<SelectableTravelDestinationItem> {
|
||||
|
||||
// This height will allow for all the Card's content to fit comfortably within the card.
|
||||
static const double height = 298.0;
|
||||
bool _isSelected = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ColorScheme colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
const SectionTitle(title: 'Selectable (long press)'),
|
||||
SizedBox(
|
||||
height: height,
|
||||
child: Card(
|
||||
// This ensures that the Card's children (including the ink splash) are clipped correctly.
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: widget.shape,
|
||||
child: InkWell(
|
||||
onLongPress: () {
|
||||
print('Selectable card state changed');
|
||||
setState(() {
|
||||
_isSelected = !_isSelected;
|
||||
});
|
||||
},
|
||||
// Generally, material cards use onSurface with 12% opacity for the pressed state.
|
||||
splashColor: colorScheme.onSurface.withOpacity(0.12),
|
||||
// Generally, material cards do not have a highlight overlay.
|
||||
highlightColor: Colors.transparent,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
color: _isSelected
|
||||
// Generally, material cards use primary with 8% opacity for the selected state.
|
||||
// See: https://material.io/design/interaction/states.html#anatomy
|
||||
? colorScheme.primary.withOpacity(0.08)
|
||||
: Colors.transparent,
|
||||
),
|
||||
TravelDestinationContent(destination: widget.destination),
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
Icons.check_circle,
|
||||
color: _isSelected ? colorScheme.primary : Colors.transparent,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SectionTitle extends StatelessWidget {
|
||||
const SectionTitle({
|
||||
Key key,
|
||||
this.title,
|
||||
}) : super(key: key);
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(4.0, 4.0, 4.0, 12.0),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(title, style: Theme.of(context).textTheme.subhead),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TravelDestinationContent extends StatelessWidget {
|
||||
const TravelDestinationContent({ Key key, @required this.destination })
|
||||
: assert(destination != null),
|
||||
super(key: key);
|
||||
|
||||
final TravelDestination destination;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final TextStyle titleStyle = theme.textTheme.headline.copyWith(color: Colors.white);
|
||||
final TextStyle descriptionStyle = theme.textTheme.subhead;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// Photo and title.
|
||||
SizedBox(
|
||||
height: 184.0,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Positioned.fill(
|
||||
// In order to have the ink splash appear above the image, you
|
||||
// must use Ink.image. This allows the image to be painted as part
|
||||
// of the Material and display ink effects above it. Using a
|
||||
// standard Image will obscure the ink splash.
|
||||
child: Ink.image(
|
||||
image: AssetImage(destination.assetName, package: destination.assetPackage),
|
||||
fit: BoxFit.cover,
|
||||
child: Container(),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 16.0,
|
||||
left: 16.0,
|
||||
right: 16.0,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
destination.title,
|
||||
style: titleStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Description and share/explore buttons.
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0.0),
|
||||
child: DefaultTextStyle(
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: descriptionStyle,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// three line description
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
destination.description,
|
||||
style: descriptionStyle.copyWith(color: Colors.black54),
|
||||
),
|
||||
),
|
||||
Text(destination.city),
|
||||
Text(destination.location),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (destination.type == CardDemoType.standard)
|
||||
// share, explore buttons
|
||||
ButtonBar(
|
||||
alignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
FlatButton(
|
||||
child: Text('SHARE', semanticsLabel: 'Share ${destination.title}'),
|
||||
textColor: Colors.amber.shade500,
|
||||
onPressed: () { print('pressed'); },
|
||||
),
|
||||
FlatButton(
|
||||
child: Text('EXPLORE', semanticsLabel: 'Explore ${destination.title}'),
|
||||
textColor: Colors.amber.shade500,
|
||||
onPressed: () { print('pressed'); },
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CardsDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/cards';
|
||||
|
||||
@override
|
||||
_CardsDemoState createState() => _CardsDemoState();
|
||||
}
|
||||
|
||||
class _CardsDemoState extends State<CardsDemo> {
|
||||
ShapeBorder _shape;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Cards'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(CardsDemo.routeName),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.sentiment_very_satisfied,
|
||||
semanticLabel: 'update shape',
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_shape = _shape != null ? null : const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16.0),
|
||||
topRight: Radius.circular(16.0),
|
||||
bottomLeft: Radius.circular(2.0),
|
||||
bottomRight: Radius.circular(2.0),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Scrollbar(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.only(top: 8.0, left: 8.0, right: 8.0),
|
||||
children: destinations.map<Widget>((TravelDestination destination) {
|
||||
Widget child;
|
||||
switch (destination.type) {
|
||||
case CardDemoType.standard:
|
||||
child = TravelDestinationItem(destination: destination, shape: _shape);
|
||||
break;
|
||||
case CardDemoType.tappable:
|
||||
child = TappableTravelDestinationItem(destination: destination, shape: _shape);
|
||||
break;
|
||||
case CardDemoType.selectable:
|
||||
child = SelectableTravelDestinationItem(destination: destination, shape: _shape);
|
||||
break;
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8.0),
|
||||
child: child,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,331 +0,0 @@
|
||||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
const List<String> _defaultMaterials = <String>[
|
||||
'poker',
|
||||
'tortilla',
|
||||
'fish and',
|
||||
'micro',
|
||||
'wood',
|
||||
];
|
||||
|
||||
const List<String> _defaultActions = <String>[
|
||||
'flake',
|
||||
'cut',
|
||||
'fragment',
|
||||
'splinter',
|
||||
'nick',
|
||||
'fry',
|
||||
'solder',
|
||||
'cash in',
|
||||
'eat',
|
||||
];
|
||||
|
||||
const Map<String, String> _results = <String, String>{
|
||||
'flake': 'flaking',
|
||||
'cut': 'cutting',
|
||||
'fragment': 'fragmenting',
|
||||
'splinter': 'splintering',
|
||||
'nick': 'nicking',
|
||||
'fry': 'frying',
|
||||
'solder': 'soldering',
|
||||
'cash in': 'cashing in',
|
||||
'eat': 'eating',
|
||||
};
|
||||
|
||||
const List<String> _defaultTools = <String>[
|
||||
'hammer',
|
||||
'chisel',
|
||||
'fryer',
|
||||
'fabricator',
|
||||
'customer',
|
||||
];
|
||||
|
||||
const Map<String, String> _avatars = <String, String>{
|
||||
'hammer': 'people/square/ali.png',
|
||||
'chisel': 'people/square/sandra.png',
|
||||
'fryer': 'people/square/trevor.png',
|
||||
'fabricator': 'people/square/stella.png',
|
||||
'customer': 'people/square/peter.png',
|
||||
};
|
||||
|
||||
const Map<String, Set<String>> _toolActions = <String, Set<String>>{
|
||||
'hammer': <String>{'flake', 'fragment', 'splinter'},
|
||||
'chisel': <String>{'flake', 'nick', 'splinter'},
|
||||
'fryer': <String>{'fry'},
|
||||
'fabricator': <String>{'solder'},
|
||||
'customer': <String>{'cash in', 'eat'},
|
||||
};
|
||||
|
||||
const Map<String, Set<String>> _materialActions = <String, Set<String>>{
|
||||
'poker': <String>{'cash in'},
|
||||
'tortilla': <String>{'fry', 'eat'},
|
||||
'fish and': <String>{'fry', 'eat'},
|
||||
'micro': <String>{'solder', 'fragment'},
|
||||
'wood': <String>{'flake', 'cut', 'splinter', 'nick'},
|
||||
};
|
||||
|
||||
class _ChipsTile extends StatelessWidget {
|
||||
const _ChipsTile({
|
||||
Key key,
|
||||
this.label,
|
||||
this.children,
|
||||
}) : super(key: key);
|
||||
|
||||
final String label;
|
||||
final List<Widget> children;
|
||||
|
||||
// Wraps a list of chips into a ListTile for display as a section in the demo.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
semanticContainer: false,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: 16.0, bottom: 4.0),
|
||||
alignment: Alignment.center,
|
||||
child: Text(label, textAlign: TextAlign.start),
|
||||
),
|
||||
if (children.isNotEmpty)
|
||||
Wrap(
|
||||
children: children.map<Widget>((Widget chip) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
child: chip,
|
||||
);
|
||||
}).toList(),
|
||||
)
|
||||
else
|
||||
Semantics(
|
||||
container: true,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
constraints: const BoxConstraints(minWidth: 48.0, minHeight: 48.0),
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text('None', style: Theme.of(context).textTheme.caption.copyWith(fontStyle: FontStyle.italic)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChipDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/chip';
|
||||
|
||||
@override
|
||||
_ChipDemoState createState() => _ChipDemoState();
|
||||
}
|
||||
|
||||
class _ChipDemoState extends State<ChipDemo> {
|
||||
_ChipDemoState() {
|
||||
_reset();
|
||||
}
|
||||
|
||||
final Set<String> _materials = <String>{};
|
||||
String _selectedMaterial = '';
|
||||
String _selectedAction = '';
|
||||
final Set<String> _tools = <String>{};
|
||||
final Set<String> _selectedTools = <String>{};
|
||||
final Set<String> _actions = <String>{};
|
||||
bool _showShapeBorder = false;
|
||||
|
||||
// Initialize members with the default data.
|
||||
void _reset() {
|
||||
_materials.clear();
|
||||
_materials.addAll(_defaultMaterials);
|
||||
_actions.clear();
|
||||
_actions.addAll(_defaultActions);
|
||||
_tools.clear();
|
||||
_tools.addAll(_defaultTools);
|
||||
_selectedMaterial = '';
|
||||
_selectedAction = '';
|
||||
_selectedTools.clear();
|
||||
}
|
||||
|
||||
void _removeMaterial(String name) {
|
||||
_materials.remove(name);
|
||||
if (_selectedMaterial == name) {
|
||||
_selectedMaterial = '';
|
||||
}
|
||||
}
|
||||
|
||||
void _removeTool(String name) {
|
||||
_tools.remove(name);
|
||||
_selectedTools.remove(name);
|
||||
}
|
||||
|
||||
String _capitalize(String name) {
|
||||
assert(name != null && name.isNotEmpty);
|
||||
return name.substring(0, 1).toUpperCase() + name.substring(1);
|
||||
}
|
||||
|
||||
// This converts a String to a unique color, based on the hash value of the
|
||||
// String object. It takes the bottom 16 bits of the hash, and uses that to
|
||||
// pick a hue for an HSV color, and then creates the color (with a preset
|
||||
// saturation and value). This means that any unique strings will also have
|
||||
// unique colors, but they'll all be readable, since they have the same
|
||||
// saturation and value.
|
||||
Color _nameToColor(String name) {
|
||||
assert(name.length > 1);
|
||||
final int hash = name.hashCode & 0xffff;
|
||||
final double hue = (360.0 * hash / (1 << 15)) % 360.0;
|
||||
return HSVColor.fromAHSV(1.0, hue, 0.4, 0.90).toColor();
|
||||
}
|
||||
|
||||
AssetImage _nameToAvatar(String name) {
|
||||
assert(_avatars.containsKey(name));
|
||||
return AssetImage(
|
||||
_avatars[name],
|
||||
package: 'flutter_gallery_assets',
|
||||
);
|
||||
}
|
||||
|
||||
String _createResult() {
|
||||
if (_selectedAction.isEmpty) {
|
||||
return '';
|
||||
}
|
||||
return _capitalize(_results[_selectedAction]) + '!';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<Widget> chips = _materials.map<Widget>((String name) {
|
||||
return Chip(
|
||||
key: ValueKey<String>(name),
|
||||
backgroundColor: _nameToColor(name),
|
||||
label: Text(_capitalize(name)),
|
||||
onDeleted: () {
|
||||
setState(() {
|
||||
_removeMaterial(name);
|
||||
});
|
||||
},
|
||||
);
|
||||
}).toList();
|
||||
|
||||
final List<Widget> inputChips = _tools.map<Widget>((String name) {
|
||||
return InputChip(
|
||||
key: ValueKey<String>(name),
|
||||
avatar: CircleAvatar(
|
||||
backgroundImage: _nameToAvatar(name),
|
||||
),
|
||||
label: Text(_capitalize(name)),
|
||||
onDeleted: () {
|
||||
setState(() {
|
||||
_removeTool(name);
|
||||
});
|
||||
});
|
||||
}).toList();
|
||||
|
||||
final List<Widget> choiceChips = _materials.map<Widget>((String name) {
|
||||
return ChoiceChip(
|
||||
key: ValueKey<String>(name),
|
||||
backgroundColor: _nameToColor(name),
|
||||
label: Text(_capitalize(name)),
|
||||
selected: _selectedMaterial == name,
|
||||
onSelected: (bool value) {
|
||||
setState(() {
|
||||
_selectedMaterial = value ? name : '';
|
||||
});
|
||||
},
|
||||
);
|
||||
}).toList();
|
||||
|
||||
final List<Widget> filterChips = _defaultTools.map<Widget>((String name) {
|
||||
return FilterChip(
|
||||
key: ValueKey<String>(name),
|
||||
label: Text(_capitalize(name)),
|
||||
selected: _tools.contains(name) && _selectedTools.contains(name),
|
||||
onSelected: !_tools.contains(name)
|
||||
? null
|
||||
: (bool value) {
|
||||
setState(() {
|
||||
if (!value) {
|
||||
_selectedTools.remove(name);
|
||||
} else {
|
||||
_selectedTools.add(name);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}).toList();
|
||||
|
||||
Set<String> allowedActions = <String>{};
|
||||
if (_selectedMaterial != null && _selectedMaterial.isNotEmpty) {
|
||||
for (String tool in _selectedTools) {
|
||||
allowedActions.addAll(_toolActions[tool]);
|
||||
}
|
||||
allowedActions = allowedActions.intersection(_materialActions[_selectedMaterial]);
|
||||
}
|
||||
|
||||
final List<Widget> actionChips = allowedActions.map<Widget>((String name) {
|
||||
return ActionChip(
|
||||
label: Text(_capitalize(name)),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_selectedAction = name;
|
||||
});
|
||||
},
|
||||
);
|
||||
}).toList();
|
||||
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final List<Widget> tiles = <Widget>[
|
||||
const SizedBox(height: 8.0, width: 0.0),
|
||||
_ChipsTile(label: 'Available Materials (Chip)', children: chips),
|
||||
_ChipsTile(label: 'Available Tools (InputChip)', children: inputChips),
|
||||
_ChipsTile(label: 'Choose a Material (ChoiceChip)', children: choiceChips),
|
||||
_ChipsTile(label: 'Choose Tools (FilterChip)', children: filterChips),
|
||||
_ChipsTile(label: 'Perform Allowed Action (ActionChip)', children: actionChips),
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
_createResult(),
|
||||
style: theme.textTheme.title,
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Chips'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(ChipDemo.routeName),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_showShapeBorder = !_showShapeBorder;
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.vignette, semanticLabel: 'Update border shape'),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ChipTheme(
|
||||
data: _showShapeBorder
|
||||
? theme.chipTheme.copyWith(
|
||||
shape: BeveledRectangleBorder(
|
||||
side: const BorderSide(width: 0.66, style: BorderStyle.solid, color: Colors.grey),
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
))
|
||||
: theme.chipTheme,
|
||||
child: Scrollbar(child: ListView(children: tiles)),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => setState(_reset),
|
||||
child: const Icon(Icons.refresh, semanticLabel: 'Reset chips'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,236 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class Dessert {
|
||||
Dessert(this.name, this.calories, this.fat, this.carbs, this.protein, this.sodium, this.calcium, this.iron);
|
||||
final String name;
|
||||
final int calories;
|
||||
final double fat;
|
||||
final int carbs;
|
||||
final double protein;
|
||||
final int sodium;
|
||||
final int calcium;
|
||||
final int iron;
|
||||
|
||||
bool selected = false;
|
||||
}
|
||||
|
||||
class DessertDataSource extends DataTableSource {
|
||||
final List<Dessert> _desserts = <Dessert>[
|
||||
Dessert('Frozen yogurt', 159, 6.0, 24, 4.0, 87, 14, 1),
|
||||
Dessert('Ice cream sandwich', 237, 9.0, 37, 4.3, 129, 8, 1),
|
||||
Dessert('Eclair', 262, 16.0, 24, 6.0, 337, 6, 7),
|
||||
Dessert('Cupcake', 305, 3.7, 67, 4.3, 413, 3, 8),
|
||||
Dessert('Gingerbread', 356, 16.0, 49, 3.9, 327, 7, 16),
|
||||
Dessert('Jelly bean', 375, 0.0, 94, 0.0, 50, 0, 0),
|
||||
Dessert('Lollipop', 392, 0.2, 98, 0.0, 38, 0, 2),
|
||||
Dessert('Honeycomb', 408, 3.2, 87, 6.5, 562, 0, 45),
|
||||
Dessert('Donut', 452, 25.0, 51, 4.9, 326, 2, 22),
|
||||
Dessert('KitKat', 518, 26.0, 65, 7.0, 54, 12, 6),
|
||||
|
||||
Dessert('Frozen yogurt with sugar', 168, 6.0, 26, 4.0, 87, 14, 1),
|
||||
Dessert('Ice cream sandwich with sugar', 246, 9.0, 39, 4.3, 129, 8, 1),
|
||||
Dessert('Eclair with sugar', 271, 16.0, 26, 6.0, 337, 6, 7),
|
||||
Dessert('Cupcake with sugar', 314, 3.7, 69, 4.3, 413, 3, 8),
|
||||
Dessert('Gingerbread with sugar', 345, 16.0, 51, 3.9, 327, 7, 16),
|
||||
Dessert('Jelly bean with sugar', 364, 0.0, 96, 0.0, 50, 0, 0),
|
||||
Dessert('Lollipop with sugar', 401, 0.2, 100, 0.0, 38, 0, 2),
|
||||
Dessert('Honeycomb with sugar', 417, 3.2, 89, 6.5, 562, 0, 45),
|
||||
Dessert('Donut with sugar', 461, 25.0, 53, 4.9, 326, 2, 22),
|
||||
Dessert('KitKat with sugar', 527, 26.0, 67, 7.0, 54, 12, 6),
|
||||
|
||||
Dessert('Frozen yogurt with honey', 223, 6.0, 36, 4.0, 87, 14, 1),
|
||||
Dessert('Ice cream sandwich with honey', 301, 9.0, 49, 4.3, 129, 8, 1),
|
||||
Dessert('Eclair with honey', 326, 16.0, 36, 6.0, 337, 6, 7),
|
||||
Dessert('Cupcake with honey', 369, 3.7, 79, 4.3, 413, 3, 8),
|
||||
Dessert('Gingerbread with honey', 420, 16.0, 61, 3.9, 327, 7, 16),
|
||||
Dessert('Jelly bean with honey', 439, 0.0, 106, 0.0, 50, 0, 0),
|
||||
Dessert('Lollipop with honey', 456, 0.2, 110, 0.0, 38, 0, 2),
|
||||
Dessert('Honeycomb with honey', 472, 3.2, 99, 6.5, 562, 0, 45),
|
||||
Dessert('Donut with honey', 516, 25.0, 63, 4.9, 326, 2, 22),
|
||||
Dessert('KitKat with honey', 582, 26.0, 77, 7.0, 54, 12, 6),
|
||||
|
||||
Dessert('Frozen yogurt with milk', 262, 8.4, 36, 12.0, 194, 44, 1),
|
||||
Dessert('Ice cream sandwich with milk', 339, 11.4, 49, 12.3, 236, 38, 1),
|
||||
Dessert('Eclair with milk', 365, 18.4, 36, 14.0, 444, 36, 7),
|
||||
Dessert('Cupcake with milk', 408, 6.1, 79, 12.3, 520, 33, 8),
|
||||
Dessert('Gingerbread with milk', 459, 18.4, 61, 11.9, 434, 37, 16),
|
||||
Dessert('Jelly bean with milk', 478, 2.4, 106, 8.0, 157, 30, 0),
|
||||
Dessert('Lollipop with milk', 495, 2.6, 110, 8.0, 145, 30, 2),
|
||||
Dessert('Honeycomb with milk', 511, 5.6, 99, 14.5, 669, 30, 45),
|
||||
Dessert('Donut with milk', 555, 27.4, 63, 12.9, 433, 32, 22),
|
||||
Dessert('KitKat with milk', 621, 28.4, 77, 15.0, 161, 42, 6),
|
||||
|
||||
Dessert('Coconut slice and frozen yogurt', 318, 21.0, 31, 5.5, 96, 14, 7),
|
||||
Dessert('Coconut slice and ice cream sandwich', 396, 24.0, 44, 5.8, 138, 8, 7),
|
||||
Dessert('Coconut slice and eclair', 421, 31.0, 31, 7.5, 346, 6, 13),
|
||||
Dessert('Coconut slice and cupcake', 464, 18.7, 74, 5.8, 422, 3, 14),
|
||||
Dessert('Coconut slice and gingerbread', 515, 31.0, 56, 5.4, 316, 7, 22),
|
||||
Dessert('Coconut slice and jelly bean', 534, 15.0, 101, 1.5, 59, 0, 6),
|
||||
Dessert('Coconut slice and lollipop', 551, 15.2, 105, 1.5, 47, 0, 8),
|
||||
Dessert('Coconut slice and honeycomb', 567, 18.2, 94, 8.0, 571, 0, 51),
|
||||
Dessert('Coconut slice and donut', 611, 40.0, 58, 6.4, 335, 2, 28),
|
||||
Dessert('Coconut slice and KitKat', 677, 41.0, 72, 8.5, 63, 12, 12),
|
||||
];
|
||||
|
||||
void _sort<T>(Comparable<T> getField(Dessert d), bool ascending) {
|
||||
_desserts.sort((Dessert a, Dessert b) {
|
||||
if (!ascending) {
|
||||
final Dessert c = a;
|
||||
a = b;
|
||||
b = c;
|
||||
}
|
||||
final Comparable<T> aValue = getField(a);
|
||||
final Comparable<T> bValue = getField(b);
|
||||
return Comparable.compare(aValue, bValue);
|
||||
});
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
int _selectedCount = 0;
|
||||
|
||||
@override
|
||||
DataRow getRow(int index) {
|
||||
assert(index >= 0);
|
||||
if (index >= _desserts.length)
|
||||
return null;
|
||||
final Dessert dessert = _desserts[index];
|
||||
return DataRow.byIndex(
|
||||
index: index,
|
||||
selected: dessert.selected,
|
||||
onSelectChanged: (bool value) {
|
||||
if (dessert.selected != value) {
|
||||
_selectedCount += value ? 1 : -1;
|
||||
assert(_selectedCount >= 0);
|
||||
dessert.selected = value;
|
||||
notifyListeners();
|
||||
}
|
||||
},
|
||||
cells: <DataCell>[
|
||||
DataCell(Text('${dessert.name}')),
|
||||
DataCell(Text('${dessert.calories}')),
|
||||
DataCell(Text('${dessert.fat.toStringAsFixed(1)}')),
|
||||
DataCell(Text('${dessert.carbs}')),
|
||||
DataCell(Text('${dessert.protein.toStringAsFixed(1)}')),
|
||||
DataCell(Text('${dessert.sodium}')),
|
||||
DataCell(Text('${dessert.calcium}%')),
|
||||
DataCell(Text('${dessert.iron}%')),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
int get rowCount => _desserts.length;
|
||||
|
||||
@override
|
||||
bool get isRowCountApproximate => false;
|
||||
|
||||
@override
|
||||
int get selectedRowCount => _selectedCount;
|
||||
|
||||
void _selectAll(bool checked) {
|
||||
for (Dessert dessert in _desserts)
|
||||
dessert.selected = checked;
|
||||
_selectedCount = checked ? _desserts.length : 0;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class DataTableDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/data-table';
|
||||
|
||||
@override
|
||||
_DataTableDemoState createState() => _DataTableDemoState();
|
||||
}
|
||||
|
||||
class _DataTableDemoState extends State<DataTableDemo> {
|
||||
int _rowsPerPage = PaginatedDataTable.defaultRowsPerPage;
|
||||
int _sortColumnIndex;
|
||||
bool _sortAscending = true;
|
||||
final DessertDataSource _dessertsDataSource = DessertDataSource();
|
||||
|
||||
void _sort<T>(Comparable<T> getField(Dessert d), int columnIndex, bool ascending) {
|
||||
_dessertsDataSource._sort<T>(getField, ascending);
|
||||
setState(() {
|
||||
_sortColumnIndex = columnIndex;
|
||||
_sortAscending = ascending;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Data tables'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(DataTableDemo.routeName),
|
||||
],
|
||||
),
|
||||
body: Scrollbar(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
children: <Widget>[
|
||||
PaginatedDataTable(
|
||||
header: const Text('Nutrition'),
|
||||
rowsPerPage: _rowsPerPage,
|
||||
onRowsPerPageChanged: (int value) { setState(() { _rowsPerPage = value; }); },
|
||||
sortColumnIndex: _sortColumnIndex,
|
||||
sortAscending: _sortAscending,
|
||||
onSelectAll: _dessertsDataSource._selectAll,
|
||||
columns: <DataColumn>[
|
||||
DataColumn(
|
||||
label: const Text('Dessert (100g serving)'),
|
||||
onSort: (int columnIndex, bool ascending) => _sort<String>((Dessert d) => d.name, columnIndex, ascending),
|
||||
),
|
||||
DataColumn(
|
||||
label: const Text('Calories'),
|
||||
tooltip: 'The total amount of food energy in the given serving size.',
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort<num>((Dessert d) => d.calories, columnIndex, ascending),
|
||||
),
|
||||
DataColumn(
|
||||
label: const Text('Fat (g)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort<num>((Dessert d) => d.fat, columnIndex, ascending),
|
||||
),
|
||||
DataColumn(
|
||||
label: const Text('Carbs (g)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort<num>((Dessert d) => d.carbs, columnIndex, ascending),
|
||||
),
|
||||
DataColumn(
|
||||
label: const Text('Protein (g)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort<num>((Dessert d) => d.protein, columnIndex, ascending),
|
||||
),
|
||||
DataColumn(
|
||||
label: const Text('Sodium (mg)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort<num>((Dessert d) => d.sodium, columnIndex, ascending),
|
||||
),
|
||||
DataColumn(
|
||||
label: const Text('Calcium (%)'),
|
||||
tooltip: 'The amount of calcium as a percentage of the recommended daily amount.',
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort<num>((Dessert d) => d.calcium, columnIndex, ascending),
|
||||
),
|
||||
DataColumn(
|
||||
label: const Text('Iron (%)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort<num>((Dessert d) => d.iron, columnIndex, ascending),
|
||||
),
|
||||
],
|
||||
source: _dessertsDataSource,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,219 +0,0 @@
|
||||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class _InputDropdown extends StatelessWidget {
|
||||
const _InputDropdown({
|
||||
Key key,
|
||||
this.child,
|
||||
this.labelText,
|
||||
this.valueText,
|
||||
this.valueStyle,
|
||||
this.onPressed,
|
||||
}) : super(key: key);
|
||||
|
||||
final String labelText;
|
||||
final String valueText;
|
||||
final TextStyle valueStyle;
|
||||
final VoidCallback onPressed;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onPressed,
|
||||
child: InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: labelText,
|
||||
),
|
||||
baseStyle: valueStyle,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(valueText, style: valueStyle),
|
||||
Icon(Icons.arrow_drop_down,
|
||||
color: Theme.of(context).brightness == Brightness.light ? Colors.grey.shade700 : Colors.white70,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DateTimePicker extends StatelessWidget {
|
||||
const _DateTimePicker({
|
||||
Key key,
|
||||
this.labelText,
|
||||
this.selectedDate,
|
||||
this.selectedTime,
|
||||
this.selectDate,
|
||||
this.selectTime,
|
||||
}) : super(key: key);
|
||||
|
||||
final String labelText;
|
||||
final DateTime selectedDate;
|
||||
final TimeOfDay selectedTime;
|
||||
final ValueChanged<DateTime> selectDate;
|
||||
final ValueChanged<TimeOfDay> selectTime;
|
||||
|
||||
Future<void> _selectDate(BuildContext context) async {
|
||||
final DateTime picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: selectedDate,
|
||||
firstDate: DateTime(2015, 8),
|
||||
lastDate: DateTime(2101),
|
||||
);
|
||||
if (picked != null && picked != selectedDate)
|
||||
selectDate(picked);
|
||||
}
|
||||
|
||||
Future<void> _selectTime(BuildContext context) async {
|
||||
final TimeOfDay picked = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: selectedTime,
|
||||
);
|
||||
if (picked != null && picked != selectedTime)
|
||||
selectTime(picked);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TextStyle valueStyle = Theme.of(context).textTheme.title;
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: _InputDropdown(
|
||||
labelText: labelText,
|
||||
valueText: DateFormat.yMMMd().format(selectedDate),
|
||||
valueStyle: valueStyle,
|
||||
onPressed: () { _selectDate(context); },
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12.0),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: _InputDropdown(
|
||||
valueText: selectedTime.format(context),
|
||||
valueStyle: valueStyle,
|
||||
onPressed: () { _selectTime(context); },
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DateAndTimePickerDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/date-and-time-pickers';
|
||||
|
||||
@override
|
||||
_DateAndTimePickerDemoState createState() => _DateAndTimePickerDemoState();
|
||||
}
|
||||
|
||||
class _DateAndTimePickerDemoState extends State<DateAndTimePickerDemo> {
|
||||
DateTime _fromDate = DateTime.now();
|
||||
TimeOfDay _fromTime = const TimeOfDay(hour: 7, minute: 28);
|
||||
DateTime _toDate = DateTime.now();
|
||||
TimeOfDay _toTime = const TimeOfDay(hour: 7, minute: 28);
|
||||
final List<String> _allActivities = <String>['hiking', 'swimming', 'boating', 'fishing'];
|
||||
String _activity = 'fishing';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Date and time pickers'),
|
||||
actions: <Widget>[MaterialDemoDocumentationButton(DateAndTimePickerDemo.routeName)],
|
||||
),
|
||||
body: DropdownButtonHideUnderline(
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: <Widget>[
|
||||
TextField(
|
||||
enabled: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Event name',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
style: Theme.of(context).textTheme.display1,
|
||||
),
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Location',
|
||||
),
|
||||
style: Theme.of(context).textTheme.display1.copyWith(fontSize: 20.0),
|
||||
),
|
||||
_DateTimePicker(
|
||||
labelText: 'From',
|
||||
selectedDate: _fromDate,
|
||||
selectedTime: _fromTime,
|
||||
selectDate: (DateTime date) {
|
||||
setState(() {
|
||||
_fromDate = date;
|
||||
});
|
||||
},
|
||||
selectTime: (TimeOfDay time) {
|
||||
setState(() {
|
||||
_fromTime = time;
|
||||
});
|
||||
},
|
||||
),
|
||||
_DateTimePicker(
|
||||
labelText: 'To',
|
||||
selectedDate: _toDate,
|
||||
selectedTime: _toTime,
|
||||
selectDate: (DateTime date) {
|
||||
setState(() {
|
||||
_toDate = date;
|
||||
});
|
||||
},
|
||||
selectTime: (TimeOfDay time) {
|
||||
setState(() {
|
||||
_toTime = time;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
InputDecorator(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Activity',
|
||||
hintText: 'Choose an activity',
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
isEmpty: _activity == null,
|
||||
child: DropdownButton<String>(
|
||||
value: _activity,
|
||||
onChanged: (String newValue) {
|
||||
setState(() {
|
||||
_activity = newValue;
|
||||
});
|
||||
},
|
||||
items: _allActivities.map<DropdownMenuItem<String>>((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,214 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
import 'full_screen_dialog_demo.dart';
|
||||
|
||||
enum DialogDemoAction {
|
||||
cancel,
|
||||
discard,
|
||||
disagree,
|
||||
agree,
|
||||
}
|
||||
|
||||
const String _alertWithoutTitleText = 'Discard draft?';
|
||||
|
||||
const String _alertWithTitleText =
|
||||
'Let Google help apps determine location. This means sending anonymous location '
|
||||
'data to Google, even when no apps are running.';
|
||||
|
||||
class DialogDemoItem extends StatelessWidget {
|
||||
const DialogDemoItem({ Key key, this.icon, this.color, this.text, this.onPressed }) : super(key: key);
|
||||
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final String text;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SimpleDialogOption(
|
||||
onPressed: onPressed,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Icon(icon, size: 36.0, color: color),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: Text(text),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DialogDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/dialog';
|
||||
|
||||
@override
|
||||
DialogDemoState createState() => DialogDemoState();
|
||||
}
|
||||
|
||||
class DialogDemoState extends State<DialogDemo> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
TimeOfDay _selectedTime;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final DateTime now = DateTime.now();
|
||||
_selectedTime = TimeOfDay(hour: now.hour, minute: now.minute);
|
||||
}
|
||||
|
||||
void showDemoDialog<T>({ BuildContext context, Widget child }) {
|
||||
showDialog<T>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => child,
|
||||
)
|
||||
.then<void>((T value) { // The value passed to Navigator.pop() or null.
|
||||
if (value != null) {
|
||||
_scaffoldKey.currentState.showSnackBar(SnackBar(
|
||||
content: Text('You selected: $value'),
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final TextStyle dialogTextStyle = theme.textTheme.subhead.copyWith(color: theme.textTheme.caption.color);
|
||||
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: const Text('Dialogs'),
|
||||
actions: <Widget>[MaterialDemoDocumentationButton(DialogDemo.routeName)],
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 72.0),
|
||||
children: <Widget>[
|
||||
RaisedButton(
|
||||
child: const Text('ALERT'),
|
||||
onPressed: () {
|
||||
showDemoDialog<DialogDemoAction>(
|
||||
context: context,
|
||||
child: AlertDialog(
|
||||
content: Text(
|
||||
_alertWithoutTitleText,
|
||||
style: dialogTextStyle,
|
||||
),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
child: const Text('CANCEL'),
|
||||
onPressed: () { Navigator.pop(context, DialogDemoAction.cancel); },
|
||||
),
|
||||
FlatButton(
|
||||
child: const Text('DISCARD'),
|
||||
onPressed: () { Navigator.pop(context, DialogDemoAction.discard); },
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
RaisedButton(
|
||||
child: const Text('ALERT WITH TITLE'),
|
||||
onPressed: () {
|
||||
showDemoDialog<DialogDemoAction>(
|
||||
context: context,
|
||||
child: AlertDialog(
|
||||
title: const Text('Use Google\'s location service?'),
|
||||
content: Text(
|
||||
_alertWithTitleText,
|
||||
style: dialogTextStyle,
|
||||
),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
child: const Text('DISAGREE'),
|
||||
onPressed: () { Navigator.pop(context, DialogDemoAction.disagree); },
|
||||
),
|
||||
FlatButton(
|
||||
child: const Text('AGREE'),
|
||||
onPressed: () { Navigator.pop(context, DialogDemoAction.agree); },
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
RaisedButton(
|
||||
child: const Text('SIMPLE'),
|
||||
onPressed: () {
|
||||
showDemoDialog<String>(
|
||||
context: context,
|
||||
child: SimpleDialog(
|
||||
title: const Text('Set backup account'),
|
||||
children: <Widget>[
|
||||
DialogDemoItem(
|
||||
icon: Icons.account_circle,
|
||||
color: theme.primaryColor,
|
||||
text: 'username@gmail.com',
|
||||
onPressed: () { Navigator.pop(context, 'username@gmail.com'); },
|
||||
),
|
||||
DialogDemoItem(
|
||||
icon: Icons.account_circle,
|
||||
color: theme.primaryColor,
|
||||
text: 'user02@gmail.com',
|
||||
onPressed: () { Navigator.pop(context, 'user02@gmail.com'); },
|
||||
),
|
||||
DialogDemoItem(
|
||||
icon: Icons.add_circle,
|
||||
text: 'add account',
|
||||
color: theme.disabledColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
RaisedButton(
|
||||
child: const Text('CONFIRMATION'),
|
||||
onPressed: () {
|
||||
showTimePicker(
|
||||
context: context,
|
||||
initialTime: _selectedTime,
|
||||
)
|
||||
.then<void>((TimeOfDay value) {
|
||||
if (value != null && value != _selectedTime) {
|
||||
_selectedTime = value;
|
||||
_scaffoldKey.currentState.showSnackBar(SnackBar(
|
||||
content: Text('You selected: ${value.format(context)}'),
|
||||
));
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
RaisedButton(
|
||||
child: const Text('FULLSCREEN'),
|
||||
onPressed: () {
|
||||
Navigator.push(context, MaterialPageRoute<DismissDialogAction>(
|
||||
builder: (BuildContext context) => FullScreenDialogDemo(),
|
||||
fullscreenDialog: true,
|
||||
));
|
||||
},
|
||||
),
|
||||
]
|
||||
// Add a little space between the buttons
|
||||
.map<Widget>((Widget button) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: button,
|
||||
);
|
||||
})
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,266 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
const String _kAsset0 = 'people/square/trevor.png';
|
||||
const String _kAsset1 = 'people/square/stella.png';
|
||||
const String _kAsset2 = 'people/square/sandra.png';
|
||||
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
|
||||
|
||||
class DrawerDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/drawer';
|
||||
|
||||
@override
|
||||
_DrawerDemoState createState() => _DrawerDemoState();
|
||||
}
|
||||
|
||||
class _DrawerDemoState extends State<DrawerDemo> with TickerProviderStateMixin {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
static const List<String> _drawerContents = <String>[
|
||||
'A', 'B', 'C', 'D', 'E',
|
||||
];
|
||||
|
||||
static final Animatable<Offset> _drawerDetailsTween = Tween<Offset>(
|
||||
begin: const Offset(0.0, -1.0),
|
||||
end: Offset.zero,
|
||||
).chain(CurveTween(
|
||||
curve: Curves.fastOutSlowIn,
|
||||
));
|
||||
|
||||
AnimationController _controller;
|
||||
Animation<double> _drawerContentsOpacity;
|
||||
Animation<Offset> _drawerDetailsPosition;
|
||||
bool _showDrawerContents = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
_drawerContentsOpacity = CurvedAnimation(
|
||||
parent: ReverseAnimation(_controller),
|
||||
curve: Curves.fastOutSlowIn,
|
||||
);
|
||||
_drawerDetailsPosition = _controller.drive(_drawerDetailsTween);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
IconData _backIcon() {
|
||||
switch (Theme.of(context).platform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
return Icons.arrow_back;
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
return Icons.arrow_back_ios;
|
||||
}
|
||||
assert(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
void _showNotImplementedMessage() {
|
||||
Navigator.pop(context); // Dismiss the drawer.
|
||||
_scaffoldKey.currentState.showSnackBar(const SnackBar(
|
||||
content: Text("The drawer's items don't do anything"),
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
drawerDragStartBehavior: DragStartBehavior.down,
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: Icon(_backIcon()),
|
||||
alignment: Alignment.centerLeft,
|
||||
tooltip: 'Back',
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
title: const Text('Navigation drawer'),
|
||||
actions: <Widget>[MaterialDemoDocumentationButton(DrawerDemo.routeName)],
|
||||
),
|
||||
drawer: Drawer(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
UserAccountsDrawerHeader(
|
||||
accountName: const Text('Trevor Widget'),
|
||||
accountEmail: const Text('trevor.widget@example.com'),
|
||||
currentAccountPicture: const CircleAvatar(
|
||||
backgroundImage: AssetImage(
|
||||
_kAsset0,
|
||||
package: _kGalleryAssetsPackage,
|
||||
),
|
||||
),
|
||||
otherAccountsPictures: <Widget>[
|
||||
GestureDetector(
|
||||
dragStartBehavior: DragStartBehavior.down,
|
||||
onTap: () {
|
||||
_onOtherAccountsTap(context);
|
||||
},
|
||||
child: Semantics(
|
||||
label: 'Switch to Account B',
|
||||
child: const CircleAvatar(
|
||||
backgroundImage: AssetImage(
|
||||
_kAsset1,
|
||||
package: _kGalleryAssetsPackage,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
dragStartBehavior: DragStartBehavior.down,
|
||||
onTap: () {
|
||||
_onOtherAccountsTap(context);
|
||||
},
|
||||
child: Semantics(
|
||||
label: 'Switch to Account C',
|
||||
child: const CircleAvatar(
|
||||
backgroundImage: AssetImage(
|
||||
_kAsset2,
|
||||
package: _kGalleryAssetsPackage,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
margin: EdgeInsets.zero,
|
||||
onDetailsPressed: () {
|
||||
_showDrawerContents = !_showDrawerContents;
|
||||
if (_showDrawerContents)
|
||||
_controller.reverse();
|
||||
else
|
||||
_controller.forward();
|
||||
},
|
||||
),
|
||||
MediaQuery.removePadding(
|
||||
context: context,
|
||||
// DrawerHeader consumes top MediaQuery padding.
|
||||
removeTop: true,
|
||||
child: Expanded(
|
||||
child: ListView(
|
||||
dragStartBehavior: DragStartBehavior.down,
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
children: <Widget>[
|
||||
Stack(
|
||||
children: <Widget>[
|
||||
// The initial contents of the drawer.
|
||||
FadeTransition(
|
||||
opacity: _drawerContentsOpacity,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: _drawerContents.map<Widget>((String id) {
|
||||
return ListTile(
|
||||
leading: CircleAvatar(child: Text(id)),
|
||||
title: Text('Drawer item $id'),
|
||||
onTap: _showNotImplementedMessage,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
// The drawer's "details" view.
|
||||
SlideTransition(
|
||||
position: _drawerDetailsPosition,
|
||||
child: FadeTransition(
|
||||
opacity: ReverseAnimation(_drawerContentsOpacity),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
leading: const Icon(Icons.add),
|
||||
title: const Text('Add account'),
|
||||
onTap: _showNotImplementedMessage,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
title: const Text('Manage accounts'),
|
||||
onTap: _showNotImplementedMessage,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: Center(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
_scaffoldKey.currentState.openDrawer();
|
||||
},
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: 'Open drawer',
|
||||
excludeSemantics: true,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 100.0,
|
||||
height: 100.0,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image: DecorationImage(
|
||||
image: AssetImage(
|
||||
_kAsset0,
|
||||
package: _kGalleryAssetsPackage,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text('Tap here to open the drawer',
|
||||
style: Theme.of(context).textTheme.subhead,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onOtherAccountsTap(BuildContext context) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Account switching not implemented.'),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
child: const Text('OK'),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,67 +0,0 @@
|
||||
// Copyright 2014 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class ElevationDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/elevation';
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ElevationDemoState();
|
||||
}
|
||||
|
||||
class _ElevationDemoState extends State<ElevationDemo> {
|
||||
bool _showElevation = true;
|
||||
|
||||
List<Widget> buildCards() {
|
||||
const List<double> elevations = <double>[
|
||||
0.0,
|
||||
1.0,
|
||||
2.0,
|
||||
3.0,
|
||||
4.0,
|
||||
5.0,
|
||||
8.0,
|
||||
16.0,
|
||||
24.0,
|
||||
];
|
||||
|
||||
return elevations.map<Widget>((double elevation) {
|
||||
return Center(
|
||||
child: Card(
|
||||
margin: const EdgeInsets.all(20.0),
|
||||
elevation: _showElevation ? elevation : 0.0,
|
||||
child: SizedBox(
|
||||
height: 100.0,
|
||||
width: 100.0,
|
||||
child: Center(
|
||||
child: Text('${elevation.toStringAsFixed(0)} pt'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Elevation'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(ElevationDemo.routeName),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.sentiment_very_satisfied),
|
||||
onPressed: () {
|
||||
setState(() => _showElevation = !_showElevation);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Scrollbar(child: ListView(children: buildCards())),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,362 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
@visibleForTesting
|
||||
enum Location {
|
||||
Barbados,
|
||||
Bahamas,
|
||||
Bermuda
|
||||
}
|
||||
|
||||
typedef DemoItemBodyBuilder<T> = Widget Function(DemoItem<T> item);
|
||||
typedef ValueToString<T> = String Function(T value);
|
||||
|
||||
class DualHeaderWithHint extends StatelessWidget {
|
||||
const DualHeaderWithHint({
|
||||
this.name,
|
||||
this.value,
|
||||
this.hint,
|
||||
this.showHint,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String value;
|
||||
final String hint;
|
||||
final bool showHint;
|
||||
|
||||
Widget _crossFade(Widget first, Widget second, bool isExpanded) {
|
||||
return AnimatedCrossFade(
|
||||
firstChild: first,
|
||||
secondChild: second,
|
||||
firstCurve: const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn),
|
||||
secondCurve: const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn),
|
||||
sizeCurve: Curves.fastOutSlowIn,
|
||||
crossFadeState: isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final TextTheme textTheme = theme.textTheme;
|
||||
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 24.0),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
name,
|
||||
style: textTheme.body1.copyWith(fontSize: 15.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 24.0),
|
||||
child: _crossFade(
|
||||
Text(value, style: textTheme.caption.copyWith(fontSize: 15.0)),
|
||||
Text(hint, style: textTheme.caption.copyWith(fontSize: 15.0)),
|
||||
showHint,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CollapsibleBody extends StatelessWidget {
|
||||
const CollapsibleBody({
|
||||
this.margin = EdgeInsets.zero,
|
||||
this.child,
|
||||
this.onSave,
|
||||
this.onCancel,
|
||||
});
|
||||
|
||||
final EdgeInsets margin;
|
||||
final Widget child;
|
||||
final VoidCallback onSave;
|
||||
final VoidCallback onCancel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final TextTheme textTheme = theme.textTheme;
|
||||
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
margin: const EdgeInsets.only(
|
||||
left: 24.0,
|
||||
right: 24.0,
|
||||
bottom: 24.0,
|
||||
) - margin,
|
||||
child: Center(
|
||||
child: DefaultTextStyle(
|
||||
style: textTheme.caption.copyWith(fontSize: 15.0),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(height: 1.0),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 8.0),
|
||||
child: FlatButton(
|
||||
onPressed: onCancel,
|
||||
child: const Text('CANCEL', style: TextStyle(
|
||||
color: Colors.black54,
|
||||
fontSize: 15.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
)),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 8.0),
|
||||
child: FlatButton(
|
||||
onPressed: onSave,
|
||||
textTheme: ButtonTextTheme.accent,
|
||||
child: const Text('SAVE'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DemoItem<T> {
|
||||
DemoItem({
|
||||
this.name,
|
||||
this.value,
|
||||
this.hint,
|
||||
this.builder,
|
||||
this.valueToString,
|
||||
}) : textController = TextEditingController(text: valueToString(value));
|
||||
|
||||
final String name;
|
||||
final String hint;
|
||||
final TextEditingController textController;
|
||||
final DemoItemBodyBuilder<T> builder;
|
||||
final ValueToString<T> valueToString;
|
||||
T value;
|
||||
bool isExpanded = false;
|
||||
|
||||
ExpansionPanelHeaderBuilder get headerBuilder {
|
||||
return (BuildContext context, bool isExpanded) {
|
||||
return DualHeaderWithHint(
|
||||
name: name,
|
||||
value: valueToString(value),
|
||||
hint: hint,
|
||||
showHint: isExpanded,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
Widget build() => builder(this);
|
||||
}
|
||||
|
||||
class ExpansionPanelsDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/expansion_panels';
|
||||
|
||||
@override
|
||||
_ExpansionPanelsDemoState createState() => _ExpansionPanelsDemoState();
|
||||
}
|
||||
|
||||
class _ExpansionPanelsDemoState extends State<ExpansionPanelsDemo> {
|
||||
List<DemoItem<dynamic>> _demoItems;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_demoItems = <DemoItem<dynamic>>[
|
||||
DemoItem<String>(
|
||||
name: 'Trip',
|
||||
value: 'Caribbean cruise',
|
||||
hint: 'Change trip name',
|
||||
valueToString: (String value) => value,
|
||||
builder: (DemoItem<String> item) {
|
||||
void close() {
|
||||
setState(() {
|
||||
item.isExpanded = false;
|
||||
});
|
||||
}
|
||||
|
||||
return Form(
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return CollapsibleBody(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
onSave: () { Form.of(context).save(); close(); },
|
||||
onCancel: () { Form.of(context).reset(); close(); },
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: TextFormField(
|
||||
controller: item.textController,
|
||||
decoration: InputDecoration(
|
||||
hintText: item.hint,
|
||||
labelText: item.name,
|
||||
),
|
||||
onSaved: (String value) { item.value = value; },
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
DemoItem<Location>(
|
||||
name: 'Location',
|
||||
value: Location.Bahamas,
|
||||
hint: 'Select location',
|
||||
valueToString: (Location location) => location.toString().split('.')[1],
|
||||
builder: (DemoItem<Location> item) {
|
||||
void close() {
|
||||
setState(() {
|
||||
item.isExpanded = false;
|
||||
});
|
||||
}
|
||||
return Form(
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return CollapsibleBody(
|
||||
onSave: () { Form.of(context).save(); close(); },
|
||||
onCancel: () { Form.of(context).reset(); close(); },
|
||||
child: FormField<Location>(
|
||||
initialValue: item.value,
|
||||
onSaved: (Location result) { item.value = result; },
|
||||
builder: (FormFieldState<Location> field) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
RadioListTile<Location>(
|
||||
value: Location.Bahamas,
|
||||
title: const Text('Bahamas'),
|
||||
groupValue: field.value,
|
||||
onChanged: field.didChange,
|
||||
),
|
||||
RadioListTile<Location>(
|
||||
value: Location.Barbados,
|
||||
title: const Text('Barbados'),
|
||||
groupValue: field.value,
|
||||
onChanged: field.didChange,
|
||||
),
|
||||
RadioListTile<Location>(
|
||||
value: Location.Bermuda,
|
||||
title: const Text('Bermuda'),
|
||||
groupValue: field.value,
|
||||
onChanged: field.didChange,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
DemoItem<double>(
|
||||
name: 'Sun',
|
||||
value: 80.0,
|
||||
hint: 'Select sun level',
|
||||
valueToString: (double amount) => '${amount.round()}',
|
||||
builder: (DemoItem<double> item) {
|
||||
void close() {
|
||||
setState(() {
|
||||
item.isExpanded = false;
|
||||
});
|
||||
}
|
||||
|
||||
return Form(
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return CollapsibleBody(
|
||||
onSave: () { Form.of(context).save(); close(); },
|
||||
onCancel: () { Form.of(context).reset(); close(); },
|
||||
child: FormField<double>(
|
||||
initialValue: item.value,
|
||||
onSaved: (double value) { item.value = value; },
|
||||
builder: (FormFieldState<double> field) {
|
||||
return Container(
|
||||
// Allow room for the value indicator.
|
||||
padding: const EdgeInsets.only(top: 44.0),
|
||||
child: Slider(
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
divisions: 5,
|
||||
activeColor: Colors.orange[100 + (field.value * 5.0).round()],
|
||||
label: '${field.value.round()}',
|
||||
value: field.value,
|
||||
onChanged: field.didChange,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Expansion panels'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(ExpansionPanelsDemo.routeName),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(24.0),
|
||||
child: ExpansionPanelList(
|
||||
expansionCallback: (int index, bool isExpanded) {
|
||||
setState(() {
|
||||
_demoItems[index].isExpanded = !isExpanded;
|
||||
});
|
||||
},
|
||||
children: _demoItems.map<ExpansionPanel>((DemoItem<dynamic> item) {
|
||||
return ExpansionPanel(
|
||||
isExpanded: item.isExpanded,
|
||||
headerBuilder: item.headerBuilder,
|
||||
body: item.build(),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class ExpansionTileListDemo extends StatelessWidget {
|
||||
static const String routeName = '/material/expansion-tile-list';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Expand/collapse list control'),
|
||||
actions: <Widget>[MaterialDemoDocumentationButton(routeName)],
|
||||
),
|
||||
body: Scrollbar(
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
const ListTile(title: Text('Top')),
|
||||
ExpansionTile(
|
||||
title: const Text('Sublist'),
|
||||
backgroundColor: Theme.of(context).accentColor.withOpacity(0.025),
|
||||
children: const <Widget>[
|
||||
ListTile(title: Text('One')),
|
||||
ListTile(title: Text('Two')),
|
||||
// https://en.wikipedia.org/wiki/Free_Four
|
||||
ListTile(title: Text('Free')),
|
||||
ListTile(title: Text('Four')),
|
||||
],
|
||||
),
|
||||
const ListTile(title: Text('Bottom')),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,268 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
// This demo is based on
|
||||
// https://material.io/design/components/dialogs.html#full-screen-dialog
|
||||
|
||||
enum DismissDialogAction {
|
||||
cancel,
|
||||
discard,
|
||||
save,
|
||||
}
|
||||
|
||||
class DateTimeItem extends StatelessWidget {
|
||||
DateTimeItem({ Key key, DateTime dateTime, @required this.onChanged })
|
||||
: assert(onChanged != null),
|
||||
date = DateTime(dateTime.year, dateTime.month, dateTime.day),
|
||||
time = TimeOfDay(hour: dateTime.hour, minute: dateTime.minute),
|
||||
super(key: key);
|
||||
|
||||
final DateTime date;
|
||||
final TimeOfDay time;
|
||||
final ValueChanged<DateTime> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
|
||||
return DefaultTextStyle(
|
||||
style: theme.textTheme.subhead,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: theme.dividerColor))
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
showDatePicker(
|
||||
context: context,
|
||||
initialDate: date,
|
||||
firstDate: date.subtract(const Duration(days: 30)),
|
||||
lastDate: date.add(const Duration(days: 30)),
|
||||
)
|
||||
.then<void>((DateTime value) {
|
||||
if (value != null)
|
||||
onChanged(DateTime(value.year, value.month, value.day, time.hour, time.minute));
|
||||
});
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(DateFormat('EEE, MMM d yyyy').format(date)),
|
||||
const Icon(Icons.arrow_drop_down, color: Colors.black54),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 8.0),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: theme.dividerColor))
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
showTimePicker(
|
||||
context: context,
|
||||
initialTime: time,
|
||||
)
|
||||
.then<void>((TimeOfDay value) {
|
||||
if (value != null)
|
||||
onChanged(DateTime(date.year, date.month, date.day, value.hour, value.minute));
|
||||
});
|
||||
},
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text('${time.format(context)}'),
|
||||
const Icon(Icons.arrow_drop_down, color: Colors.black54),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FullScreenDialogDemo extends StatefulWidget {
|
||||
@override
|
||||
FullScreenDialogDemoState createState() => FullScreenDialogDemoState();
|
||||
}
|
||||
|
||||
class FullScreenDialogDemoState extends State<FullScreenDialogDemo> {
|
||||
DateTime _fromDateTime = DateTime.now();
|
||||
DateTime _toDateTime = DateTime.now();
|
||||
bool _allDayValue = false;
|
||||
bool _saveNeeded = false;
|
||||
bool _hasLocation = false;
|
||||
bool _hasName = false;
|
||||
String _eventName;
|
||||
|
||||
Future<bool> _onWillPop() async {
|
||||
_saveNeeded = _hasLocation || _hasName || _saveNeeded;
|
||||
if (!_saveNeeded)
|
||||
return true;
|
||||
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final TextStyle dialogTextStyle = theme.textTheme.subhead.copyWith(color: theme.textTheme.caption.color);
|
||||
|
||||
return await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
content: Text(
|
||||
'Discard new event?',
|
||||
style: dialogTextStyle,
|
||||
),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
child: const Text('CANCEL'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false); // Pops the confirmation dialog but not the page.
|
||||
},
|
||||
),
|
||||
FlatButton(
|
||||
child: const Text('DISCARD'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true); // Returning true to _onWillPop will pop again.
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
) ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_hasName ? _eventName : 'Event Name TBD'),
|
||||
actions: <Widget> [
|
||||
FlatButton(
|
||||
child: Text('SAVE', style: theme.textTheme.body1.copyWith(color: Colors.white)),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, DismissDialogAction.save);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Form(
|
||||
onWillPop: _onWillPop,
|
||||
child: Scrollbar(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Event name',
|
||||
filled: true,
|
||||
),
|
||||
style: theme.textTheme.headline,
|
||||
onChanged: (String value) {
|
||||
setState(() {
|
||||
_hasName = value.isNotEmpty;
|
||||
if (_hasName) {
|
||||
_eventName = value;
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Location',
|
||||
hintText: 'Where is the event?',
|
||||
filled: true,
|
||||
),
|
||||
onChanged: (String value) {
|
||||
setState(() {
|
||||
_hasLocation = value.isNotEmpty;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text('From', style: theme.textTheme.caption),
|
||||
DateTimeItem(
|
||||
dateTime: _fromDateTime,
|
||||
onChanged: (DateTime value) {
|
||||
setState(() {
|
||||
_fromDateTime = value;
|
||||
_saveNeeded = true;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text('To', style: theme.textTheme.caption),
|
||||
DateTimeItem(
|
||||
dateTime: _toDateTime,
|
||||
onChanged: (DateTime value) {
|
||||
setState(() {
|
||||
_toDateTime = value;
|
||||
_saveNeeded = true;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Text('All-day'),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: theme.dividerColor))
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget> [
|
||||
Checkbox(
|
||||
value: _allDayValue,
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
_allDayValue = value;
|
||||
_saveNeeded = true;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Text('All-day'),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
.map<Widget>((Widget child) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
height: 96.0,
|
||||
child: child,
|
||||
);
|
||||
})
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,395 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
enum GridDemoTileStyle {
|
||||
imageOnly,
|
||||
oneLine,
|
||||
twoLine
|
||||
}
|
||||
|
||||
typedef BannerTapCallback = void Function(Photo photo);
|
||||
|
||||
const double _kMinFlingVelocity = 800.0;
|
||||
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
|
||||
|
||||
class Photo {
|
||||
Photo({
|
||||
this.assetName,
|
||||
this.assetPackage,
|
||||
this.title,
|
||||
this.caption,
|
||||
this.isFavorite = false,
|
||||
});
|
||||
|
||||
final String assetName;
|
||||
final String assetPackage;
|
||||
final String title;
|
||||
final String caption;
|
||||
|
||||
bool isFavorite;
|
||||
String get tag => assetName; // Assuming that all asset names are unique.
|
||||
|
||||
bool get isValid => assetName != null && title != null && caption != null && isFavorite != null;
|
||||
}
|
||||
|
||||
class GridPhotoViewer extends StatefulWidget {
|
||||
const GridPhotoViewer({ Key key, this.photo }) : super(key: key);
|
||||
|
||||
final Photo photo;
|
||||
|
||||
@override
|
||||
_GridPhotoViewerState createState() => _GridPhotoViewerState();
|
||||
}
|
||||
|
||||
class _GridTitleText extends StatelessWidget {
|
||||
const _GridTitleText(this.text);
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(text),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GridPhotoViewerState extends State<GridPhotoViewer> with SingleTickerProviderStateMixin {
|
||||
AnimationController _controller;
|
||||
Animation<Offset> _flingAnimation;
|
||||
Offset _offset = Offset.zero;
|
||||
double _scale = 1.0;
|
||||
Offset _normalizedOffset;
|
||||
double _previousScale;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(vsync: this)
|
||||
..addListener(_handleFlingAnimation);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// The maximum offset value is 0,0. If the size of this renderer's box is w,h
|
||||
// then the minimum offset value is w - _scale * w, h - _scale * h.
|
||||
Offset _clampOffset(Offset offset) {
|
||||
final Size size = context.size;
|
||||
final Offset minOffset = Offset(size.width, size.height) * (1.0 - _scale);
|
||||
return Offset(offset.dx.clamp(minOffset.dx, 0.0), offset.dy.clamp(minOffset.dy, 0.0));
|
||||
}
|
||||
|
||||
void _handleFlingAnimation() {
|
||||
setState(() {
|
||||
_offset = _flingAnimation.value;
|
||||
});
|
||||
}
|
||||
|
||||
void _handleOnScaleStart(ScaleStartDetails details) {
|
||||
setState(() {
|
||||
_previousScale = _scale;
|
||||
_normalizedOffset = (details.focalPoint - _offset) / _scale;
|
||||
// The fling animation stops if an input gesture starts.
|
||||
_controller.stop();
|
||||
});
|
||||
}
|
||||
|
||||
void _handleOnScaleUpdate(ScaleUpdateDetails details) {
|
||||
setState(() {
|
||||
_scale = (_previousScale * details.scale).clamp(1.0, 4.0);
|
||||
// Ensure that image location under the focal point stays in the same place despite scaling.
|
||||
_offset = _clampOffset(details.focalPoint - _normalizedOffset * _scale);
|
||||
});
|
||||
}
|
||||
|
||||
void _handleOnScaleEnd(ScaleEndDetails details) {
|
||||
final double magnitude = details.velocity.pixelsPerSecond.distance;
|
||||
if (magnitude < _kMinFlingVelocity)
|
||||
return;
|
||||
final Offset direction = details.velocity.pixelsPerSecond / magnitude;
|
||||
final double distance = (Offset.zero & context.size).shortestSide;
|
||||
_flingAnimation = _controller.drive(Tween<Offset>(
|
||||
begin: _offset,
|
||||
end: _clampOffset(_offset + direction * distance),
|
||||
));
|
||||
_controller
|
||||
..value = 0.0
|
||||
..fling(velocity: magnitude / 1000.0);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onScaleStart: _handleOnScaleStart,
|
||||
onScaleUpdate: _handleOnScaleUpdate,
|
||||
onScaleEnd: _handleOnScaleEnd,
|
||||
child: ClipRect(
|
||||
child: Transform(
|
||||
transform: Matrix4.identity()
|
||||
..translate(_offset.dx, _offset.dy)
|
||||
..scale(_scale),
|
||||
child: Image.asset(
|
||||
widget.photo.assetName,
|
||||
package: widget.photo.assetPackage,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GridDemoPhotoItem extends StatelessWidget {
|
||||
GridDemoPhotoItem({
|
||||
Key key,
|
||||
@required this.photo,
|
||||
@required this.tileStyle,
|
||||
@required this.onBannerTap,
|
||||
}) : assert(photo != null && photo.isValid),
|
||||
assert(tileStyle != null),
|
||||
assert(onBannerTap != null),
|
||||
super(key: key);
|
||||
|
||||
final Photo photo;
|
||||
final GridDemoTileStyle tileStyle;
|
||||
final BannerTapCallback onBannerTap; // User taps on the photo's header or footer.
|
||||
|
||||
void showPhoto(BuildContext context) {
|
||||
Navigator.push(context, MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(photo.title),
|
||||
),
|
||||
body: SizedBox.expand(
|
||||
child: Hero(
|
||||
tag: photo.tag,
|
||||
child: GridPhotoViewer(photo: photo),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Widget image = GestureDetector(
|
||||
onTap: () { showPhoto(context); },
|
||||
child: Hero(
|
||||
key: Key(photo.assetName),
|
||||
tag: photo.tag,
|
||||
child: Image.asset(
|
||||
photo.assetName,
|
||||
package: photo.assetPackage,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final IconData icon = photo.isFavorite ? Icons.star : Icons.star_border;
|
||||
|
||||
switch (tileStyle) {
|
||||
case GridDemoTileStyle.imageOnly:
|
||||
return image;
|
||||
|
||||
case GridDemoTileStyle.oneLine:
|
||||
return GridTile(
|
||||
header: GestureDetector(
|
||||
onTap: () { onBannerTap(photo); },
|
||||
child: GridTileBar(
|
||||
title: _GridTitleText(photo.title),
|
||||
backgroundColor: Colors.black45,
|
||||
leading: Icon(
|
||||
icon,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: image,
|
||||
);
|
||||
|
||||
case GridDemoTileStyle.twoLine:
|
||||
return GridTile(
|
||||
footer: GestureDetector(
|
||||
onTap: () { onBannerTap(photo); },
|
||||
child: GridTileBar(
|
||||
backgroundColor: Colors.black45,
|
||||
title: _GridTitleText(photo.title),
|
||||
subtitle: _GridTitleText(photo.caption),
|
||||
trailing: Icon(
|
||||
icon,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: image,
|
||||
);
|
||||
}
|
||||
assert(tileStyle != null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class GridListDemo extends StatefulWidget {
|
||||
const GridListDemo({ Key key }) : super(key: key);
|
||||
|
||||
static const String routeName = '/material/grid-list';
|
||||
|
||||
@override
|
||||
GridListDemoState createState() => GridListDemoState();
|
||||
}
|
||||
|
||||
class GridListDemoState extends State<GridListDemo> {
|
||||
GridDemoTileStyle _tileStyle = GridDemoTileStyle.twoLine;
|
||||
|
||||
List<Photo> photos = <Photo>[
|
||||
Photo(
|
||||
assetName: 'places/india_chennai_flower_market.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Chennai',
|
||||
caption: 'Flower Market',
|
||||
),
|
||||
Photo(
|
||||
assetName: 'places/india_tanjore_bronze_works.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Tanjore',
|
||||
caption: 'Bronze Works',
|
||||
),
|
||||
Photo(
|
||||
assetName: 'places/india_tanjore_market_merchant.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Tanjore',
|
||||
caption: 'Market',
|
||||
),
|
||||
Photo(
|
||||
assetName: 'places/india_tanjore_thanjavur_temple.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Tanjore',
|
||||
caption: 'Thanjavur Temple',
|
||||
),
|
||||
Photo(
|
||||
assetName: 'places/india_tanjore_thanjavur_temple_carvings.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Tanjore',
|
||||
caption: 'Thanjavur Temple',
|
||||
),
|
||||
Photo(
|
||||
assetName: 'places/india_pondicherry_salt_farm.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Pondicherry',
|
||||
caption: 'Salt Farm',
|
||||
),
|
||||
Photo(
|
||||
assetName: 'places/india_chennai_highway.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Chennai',
|
||||
caption: 'Scooters',
|
||||
),
|
||||
Photo(
|
||||
assetName: 'places/india_chettinad_silk_maker.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Chettinad',
|
||||
caption: 'Silk Maker',
|
||||
),
|
||||
Photo(
|
||||
assetName: 'places/india_chettinad_produce.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Chettinad',
|
||||
caption: 'Lunch Prep',
|
||||
),
|
||||
Photo(
|
||||
assetName: 'places/india_tanjore_market_technology.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Tanjore',
|
||||
caption: 'Market',
|
||||
),
|
||||
Photo(
|
||||
assetName: 'places/india_pondicherry_beach.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Pondicherry',
|
||||
caption: 'Beach',
|
||||
),
|
||||
Photo(
|
||||
assetName: 'places/india_pondicherry_fisherman.png',
|
||||
assetPackage: _kGalleryAssetsPackage,
|
||||
title: 'Pondicherry',
|
||||
caption: 'Fisherman',
|
||||
),
|
||||
];
|
||||
|
||||
void changeTileStyle(GridDemoTileStyle value) {
|
||||
setState(() {
|
||||
_tileStyle = value;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Orientation orientation = MediaQuery.of(context).orientation;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Grid list'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(GridListDemo.routeName),
|
||||
PopupMenuButton<GridDemoTileStyle>(
|
||||
onSelected: changeTileStyle,
|
||||
itemBuilder: (BuildContext context) => <PopupMenuItem<GridDemoTileStyle>>[
|
||||
const PopupMenuItem<GridDemoTileStyle>(
|
||||
value: GridDemoTileStyle.imageOnly,
|
||||
child: Text('Image only'),
|
||||
),
|
||||
const PopupMenuItem<GridDemoTileStyle>(
|
||||
value: GridDemoTileStyle.oneLine,
|
||||
child: Text('One line'),
|
||||
),
|
||||
const PopupMenuItem<GridDemoTileStyle>(
|
||||
value: GridDemoTileStyle.twoLine,
|
||||
child: Text('Two line'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: GridView.count(
|
||||
crossAxisCount: (orientation == Orientation.portrait) ? 2 : 3,
|
||||
mainAxisSpacing: 4.0,
|
||||
crossAxisSpacing: 4.0,
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
childAspectRatio: (orientation == Orientation.portrait) ? 1.0 : 1.3,
|
||||
children: photos.map<Widget>((Photo photo) {
|
||||
return GridDemoPhotoItem(
|
||||
photo: photo,
|
||||
tileStyle: _tileStyle,
|
||||
onBannerTap: (Photo photo) {
|
||||
setState(() {
|
||||
photo.isFavorite = !photo.isFavorite;
|
||||
});
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,138 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class IconsDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/icons';
|
||||
|
||||
@override
|
||||
IconsDemoState createState() => IconsDemoState();
|
||||
}
|
||||
|
||||
class IconsDemoState extends State<IconsDemo> {
|
||||
static final List<MaterialColor> iconColors = <MaterialColor>[
|
||||
Colors.red,
|
||||
Colors.pink,
|
||||
Colors.purple,
|
||||
Colors.deepPurple,
|
||||
Colors.indigo,
|
||||
Colors.blue,
|
||||
Colors.lightBlue,
|
||||
Colors.cyan,
|
||||
Colors.teal,
|
||||
Colors.green,
|
||||
Colors.lightGreen,
|
||||
Colors.lime,
|
||||
Colors.yellow,
|
||||
Colors.amber,
|
||||
Colors.orange,
|
||||
Colors.deepOrange,
|
||||
Colors.brown,
|
||||
Colors.grey,
|
||||
Colors.blueGrey,
|
||||
];
|
||||
|
||||
int iconColorIndex = 8; // teal
|
||||
|
||||
Color get iconColor => iconColors[iconColorIndex];
|
||||
|
||||
void handleIconButtonPress() {
|
||||
setState(() {
|
||||
iconColorIndex = (iconColorIndex + 1) % iconColors.length;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Icons'),
|
||||
actions: <Widget>[MaterialDemoDocumentationButton(IconsDemo.routeName)],
|
||||
),
|
||||
body: IconTheme(
|
||||
data: IconThemeData(color: iconColor),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Scrollbar(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
children: <Widget>[
|
||||
_IconsDemoCard(handleIconButtonPress, Icons.face), // direction-agnostic icon
|
||||
const SizedBox(height: 24.0),
|
||||
_IconsDemoCard(handleIconButtonPress, Icons.battery_unknown), // direction-aware icon
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _IconsDemoCard extends StatelessWidget {
|
||||
const _IconsDemoCard(this.handleIconButtonPress, this.icon);
|
||||
|
||||
final VoidCallback handleIconButtonPress;
|
||||
final IconData icon;
|
||||
|
||||
Widget _buildIconButton(double iconSize, IconData icon, bool enabled) {
|
||||
return IconButton(
|
||||
icon: Icon(icon),
|
||||
iconSize: iconSize,
|
||||
tooltip: "${enabled ? 'Enabled' : 'Disabled'} icon button",
|
||||
onPressed: enabled ? handleIconButtonPress : null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _centeredText(String label) =>
|
||||
Padding(
|
||||
// Match the default padding of IconButton.
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(label, textAlign: TextAlign.center),
|
||||
);
|
||||
|
||||
TableRow _buildIconRow(double size) {
|
||||
return TableRow(
|
||||
children: <Widget> [
|
||||
_centeredText(size.floor().toString()),
|
||||
_buildIconButton(size, icon, true),
|
||||
_buildIconButton(size, icon, false),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final TextStyle textStyle = theme.textTheme.subhead.copyWith(color: theme.textTheme.caption.color);
|
||||
return Card(
|
||||
child: DefaultTextStyle(
|
||||
style: textStyle,
|
||||
child: Semantics(
|
||||
explicitChildNodes: true,
|
||||
child: Table(
|
||||
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
||||
children: <TableRow> [
|
||||
TableRow(
|
||||
children: <Widget> [
|
||||
_centeredText('Size'),
|
||||
_centeredText('Enabled'),
|
||||
_centeredText('Disabled'),
|
||||
]
|
||||
),
|
||||
_buildIconRow(18.0),
|
||||
_buildIconRow(24.0),
|
||||
_buildIconRow(36.0),
|
||||
_buildIconRow(48.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,302 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:collection/collection.dart' show lowerBound;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/semantics.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
enum LeaveBehindDemoAction {
|
||||
reset,
|
||||
horizontalSwipe,
|
||||
leftSwipe,
|
||||
rightSwipe,
|
||||
confirmDismiss,
|
||||
}
|
||||
|
||||
class LeaveBehindItem implements Comparable<LeaveBehindItem> {
|
||||
LeaveBehindItem({ this.index, this.name, this.subject, this.body });
|
||||
|
||||
LeaveBehindItem.from(LeaveBehindItem item)
|
||||
: index = item.index, name = item.name, subject = item.subject, body = item.body;
|
||||
|
||||
final int index;
|
||||
final String name;
|
||||
final String subject;
|
||||
final String body;
|
||||
|
||||
@override
|
||||
int compareTo(LeaveBehindItem other) => index.compareTo(other.index);
|
||||
}
|
||||
|
||||
class LeaveBehindDemo extends StatefulWidget {
|
||||
const LeaveBehindDemo({ Key key }) : super(key: key);
|
||||
|
||||
static const String routeName = '/material/leave-behind';
|
||||
|
||||
@override
|
||||
LeaveBehindDemoState createState() => LeaveBehindDemoState();
|
||||
}
|
||||
|
||||
class LeaveBehindDemoState extends State<LeaveBehindDemo> {
|
||||
static final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
DismissDirection _dismissDirection = DismissDirection.horizontal;
|
||||
bool _confirmDismiss = true;
|
||||
List<LeaveBehindItem> leaveBehindItems;
|
||||
|
||||
void initListItems() {
|
||||
leaveBehindItems = List<LeaveBehindItem>.generate(16, (int index) {
|
||||
return LeaveBehindItem(
|
||||
index: index,
|
||||
name: 'Item $index Sender',
|
||||
subject: 'Subject: $index',
|
||||
body: "[$index] first line of the message's body...",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initListItems();
|
||||
}
|
||||
|
||||
void handleDemoAction(LeaveBehindDemoAction action) {
|
||||
setState(() {
|
||||
switch (action) {
|
||||
case LeaveBehindDemoAction.reset:
|
||||
initListItems();
|
||||
break;
|
||||
case LeaveBehindDemoAction.horizontalSwipe:
|
||||
_dismissDirection = DismissDirection.horizontal;
|
||||
break;
|
||||
case LeaveBehindDemoAction.leftSwipe:
|
||||
_dismissDirection = DismissDirection.endToStart;
|
||||
break;
|
||||
case LeaveBehindDemoAction.rightSwipe:
|
||||
_dismissDirection = DismissDirection.startToEnd;
|
||||
break;
|
||||
case LeaveBehindDemoAction.confirmDismiss:
|
||||
_confirmDismiss = !_confirmDismiss;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void handleUndo(LeaveBehindItem item) {
|
||||
final int insertionIndex = lowerBound(leaveBehindItems, item);
|
||||
setState(() {
|
||||
leaveBehindItems.insert(insertionIndex, item);
|
||||
});
|
||||
}
|
||||
|
||||
void _handleArchive(LeaveBehindItem item) {
|
||||
setState(() {
|
||||
leaveBehindItems.remove(item);
|
||||
});
|
||||
_scaffoldKey.currentState.showSnackBar(SnackBar(
|
||||
content: Text('You archived item ${item.index}'),
|
||||
action: SnackBarAction(
|
||||
label: 'UNDO',
|
||||
onPressed: () { handleUndo(item); },
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
void _handleDelete(LeaveBehindItem item) {
|
||||
setState(() {
|
||||
leaveBehindItems.remove(item);
|
||||
});
|
||||
_scaffoldKey.currentState.showSnackBar(SnackBar(
|
||||
content: Text('You deleted item ${item.index}'),
|
||||
action: SnackBarAction(
|
||||
label: 'UNDO',
|
||||
onPressed: () { handleUndo(item); },
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget body;
|
||||
if (leaveBehindItems.isEmpty) {
|
||||
body = Center(
|
||||
child: RaisedButton(
|
||||
onPressed: () => handleDemoAction(LeaveBehindDemoAction.reset),
|
||||
child: const Text('Reset the list'),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
body = Scrollbar(
|
||||
child: ListView(
|
||||
children: leaveBehindItems.map<Widget>((LeaveBehindItem item) {
|
||||
return _LeaveBehindListItem(
|
||||
confirmDismiss: _confirmDismiss,
|
||||
item: item,
|
||||
onArchive: _handleArchive,
|
||||
onDelete: _handleDelete,
|
||||
dismissDirection: _dismissDirection,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: const Text('Swipe to dismiss'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(LeaveBehindDemo.routeName),
|
||||
PopupMenuButton<LeaveBehindDemoAction>(
|
||||
onSelected: handleDemoAction,
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry<LeaveBehindDemoAction>>[
|
||||
const PopupMenuItem<LeaveBehindDemoAction>(
|
||||
value: LeaveBehindDemoAction.reset,
|
||||
child: Text('Reset the list'),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
CheckedPopupMenuItem<LeaveBehindDemoAction>(
|
||||
value: LeaveBehindDemoAction.horizontalSwipe,
|
||||
checked: _dismissDirection == DismissDirection.horizontal,
|
||||
child: const Text('Horizontal swipe'),
|
||||
),
|
||||
CheckedPopupMenuItem<LeaveBehindDemoAction>(
|
||||
value: LeaveBehindDemoAction.leftSwipe,
|
||||
checked: _dismissDirection == DismissDirection.endToStart,
|
||||
child: const Text('Only swipe left'),
|
||||
),
|
||||
CheckedPopupMenuItem<LeaveBehindDemoAction>(
|
||||
value: LeaveBehindDemoAction.rightSwipe,
|
||||
checked: _dismissDirection == DismissDirection.startToEnd,
|
||||
child: const Text('Only swipe right'),
|
||||
),
|
||||
CheckedPopupMenuItem<LeaveBehindDemoAction>(
|
||||
value: LeaveBehindDemoAction.confirmDismiss,
|
||||
checked: _confirmDismiss,
|
||||
child: const Text('Confirm dismiss'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: body,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LeaveBehindListItem extends StatelessWidget {
|
||||
const _LeaveBehindListItem({
|
||||
Key key,
|
||||
@required this.item,
|
||||
@required this.onArchive,
|
||||
@required this.onDelete,
|
||||
@required this.dismissDirection,
|
||||
@required this.confirmDismiss,
|
||||
}) : super(key: key);
|
||||
|
||||
final LeaveBehindItem item;
|
||||
final DismissDirection dismissDirection;
|
||||
final void Function(LeaveBehindItem) onArchive;
|
||||
final void Function(LeaveBehindItem) onDelete;
|
||||
final bool confirmDismiss;
|
||||
|
||||
void _handleArchive() {
|
||||
onArchive(item);
|
||||
}
|
||||
|
||||
void _handleDelete() {
|
||||
onDelete(item);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return Semantics(
|
||||
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
|
||||
const CustomSemanticsAction(label: 'Archive'): _handleArchive,
|
||||
const CustomSemanticsAction(label: 'Delete'): _handleDelete,
|
||||
},
|
||||
child: Dismissible(
|
||||
key: ObjectKey(item),
|
||||
direction: dismissDirection,
|
||||
onDismissed: (DismissDirection direction) {
|
||||
if (direction == DismissDirection.endToStart)
|
||||
_handleArchive();
|
||||
else
|
||||
_handleDelete();
|
||||
},
|
||||
confirmDismiss: !confirmDismiss ? null : (DismissDirection dismissDirection) async {
|
||||
switch(dismissDirection) {
|
||||
case DismissDirection.endToStart:
|
||||
return await _showConfirmationDialog(context, 'archive') == true;
|
||||
case DismissDirection.startToEnd:
|
||||
return await _showConfirmationDialog(context, 'delete') == true;
|
||||
case DismissDirection.horizontal:
|
||||
case DismissDirection.vertical:
|
||||
case DismissDirection.up:
|
||||
case DismissDirection.down:
|
||||
assert(false);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
background: Container(
|
||||
color: theme.primaryColor,
|
||||
child: const Center(
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.delete, color: Colors.white, size: 36.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
secondaryBackground: Container(
|
||||
color: theme.primaryColor,
|
||||
child: const Center(
|
||||
child: ListTile(
|
||||
trailing: Icon(Icons.archive, color: Colors.white, size: 36.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.canvasColor,
|
||||
border: Border(bottom: BorderSide(color: theme.dividerColor)),
|
||||
),
|
||||
child: ListTile(
|
||||
title: Text(item.name),
|
||||
subtitle: Text('${item.subject}\n${item.body}'),
|
||||
isThreeLine: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _showConfirmationDialog(BuildContext context, String action) {
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('Do you want to $action this item?'),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
child: const Text('Yes'),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, true); // showDialog() returns true
|
||||
},
|
||||
),
|
||||
FlatButton(
|
||||
child: const Text('No'),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, false); // showDialog() returns false
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,251 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
enum _MaterialListType {
|
||||
/// A list tile that contains a single line of text.
|
||||
oneLine,
|
||||
|
||||
/// A list tile that contains a [CircleAvatar] followed by a single line of text.
|
||||
oneLineWithAvatar,
|
||||
|
||||
/// A list tile that contains two lines of text.
|
||||
twoLine,
|
||||
|
||||
/// A list tile that contains three lines of text.
|
||||
threeLine,
|
||||
}
|
||||
|
||||
class ListDemo extends StatefulWidget {
|
||||
const ListDemo({ Key key }) : super(key: key);
|
||||
|
||||
static const String routeName = '/material/list';
|
||||
|
||||
@override
|
||||
_ListDemoState createState() => _ListDemoState();
|
||||
}
|
||||
|
||||
class _ListDemoState extends State<ListDemo> {
|
||||
static final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
PersistentBottomSheetController<void> _bottomSheet;
|
||||
_MaterialListType _itemType = _MaterialListType.threeLine;
|
||||
bool _dense = false;
|
||||
bool _showAvatars = true;
|
||||
bool _showIcons = false;
|
||||
bool _showDividers = false;
|
||||
bool _reverseSort = false;
|
||||
List<String> items = <String>[
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
|
||||
];
|
||||
|
||||
void changeItemType(_MaterialListType type) {
|
||||
setState(() {
|
||||
_itemType = type;
|
||||
});
|
||||
_bottomSheet?.setState(() { });
|
||||
}
|
||||
|
||||
void _showConfigurationSheet() {
|
||||
final PersistentBottomSheetController<void> bottomSheet = scaffoldKey.currentState.showBottomSheet<void>((BuildContext bottomSheetContext) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(top: BorderSide(color: Colors.black26)),
|
||||
),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
primary: false,
|
||||
children: <Widget>[
|
||||
MergeSemantics(
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: const Text('One-line'),
|
||||
trailing: Radio<_MaterialListType>(
|
||||
value: _showAvatars ? _MaterialListType.oneLineWithAvatar : _MaterialListType.oneLine,
|
||||
groupValue: _itemType,
|
||||
onChanged: changeItemType,
|
||||
),
|
||||
),
|
||||
),
|
||||
MergeSemantics(
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: const Text('Two-line'),
|
||||
trailing: Radio<_MaterialListType>(
|
||||
value: _MaterialListType.twoLine,
|
||||
groupValue: _itemType,
|
||||
onChanged: changeItemType,
|
||||
),
|
||||
),
|
||||
),
|
||||
MergeSemantics(
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: const Text('Three-line'),
|
||||
trailing: Radio<_MaterialListType>(
|
||||
value: _MaterialListType.threeLine,
|
||||
groupValue: _itemType,
|
||||
onChanged: changeItemType,
|
||||
),
|
||||
),
|
||||
),
|
||||
MergeSemantics(
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: const Text('Show avatar'),
|
||||
trailing: Checkbox(
|
||||
value: _showAvatars,
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
_showAvatars = value;
|
||||
});
|
||||
_bottomSheet?.setState(() { });
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
MergeSemantics(
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: const Text('Show icon'),
|
||||
trailing: Checkbox(
|
||||
value: _showIcons,
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
_showIcons = value;
|
||||
});
|
||||
_bottomSheet?.setState(() { });
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
MergeSemantics(
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: const Text('Show dividers'),
|
||||
trailing: Checkbox(
|
||||
value: _showDividers,
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
_showDividers = value;
|
||||
});
|
||||
_bottomSheet?.setState(() { });
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
MergeSemantics(
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: const Text('Dense layout'),
|
||||
trailing: Checkbox(
|
||||
value: _dense,
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
_dense = value;
|
||||
});
|
||||
_bottomSheet?.setState(() { });
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
setState(() {
|
||||
_bottomSheet = bottomSheet;
|
||||
});
|
||||
|
||||
_bottomSheet.closed.whenComplete(() {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_bottomSheet = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Widget buildListTile(BuildContext context, String item) {
|
||||
Widget secondary;
|
||||
if (_itemType == _MaterialListType.twoLine) {
|
||||
secondary = const Text('Additional item information.');
|
||||
} else if (_itemType == _MaterialListType.threeLine) {
|
||||
secondary = const Text(
|
||||
'Even more additional list item information appears on line three.',
|
||||
);
|
||||
}
|
||||
return MergeSemantics(
|
||||
child: ListTile(
|
||||
isThreeLine: _itemType == _MaterialListType.threeLine,
|
||||
dense: _dense,
|
||||
leading: _showAvatars ? ExcludeSemantics(child: CircleAvatar(child: Text(item))) : null,
|
||||
title: Text('This item represents $item.'),
|
||||
subtitle: secondary,
|
||||
trailing: _showIcons ? Icon(Icons.info, color: Theme.of(context).disabledColor) : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String layoutText = _dense ? ' \u2013 Dense' : '';
|
||||
String itemTypeText;
|
||||
switch (_itemType) {
|
||||
case _MaterialListType.oneLine:
|
||||
case _MaterialListType.oneLineWithAvatar:
|
||||
itemTypeText = 'Single-line';
|
||||
break;
|
||||
case _MaterialListType.twoLine:
|
||||
itemTypeText = 'Two-line';
|
||||
break;
|
||||
case _MaterialListType.threeLine:
|
||||
itemTypeText = 'Three-line';
|
||||
break;
|
||||
}
|
||||
|
||||
Iterable<Widget> listTiles = items.map<Widget>((String item) => buildListTile(context, item));
|
||||
if (_showDividers)
|
||||
listTiles = ListTile.divideTiles(context: context, tiles: listTiles);
|
||||
|
||||
return Scaffold(
|
||||
key: scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: Text('Scrolling list\n$itemTypeText$layoutText'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(ListDemo.routeName),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.sort_by_alpha),
|
||||
tooltip: 'Sort',
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_reverseSort = !_reverseSort;
|
||||
items.sort((String a, String b) => _reverseSort ? b.compareTo(a) : a.compareTo(b));
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Theme.of(context).platform == TargetPlatform.iOS
|
||||
? Icons.more_horiz
|
||||
: Icons.more_vert,
|
||||
),
|
||||
tooltip: 'Show menu',
|
||||
onPressed: _bottomSheet == null ? _showConfigurationSheet : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Scrollbar(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.symmetric(vertical: _dense ? 4.0 : 8.0),
|
||||
children: listTiles.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
export 'backdrop_demo.dart';
|
||||
export 'banner_demo.dart';
|
||||
export 'bottom_app_bar_demo.dart';
|
||||
export 'bottom_navigation_demo.dart';
|
||||
export 'buttons_demo.dart';
|
||||
export 'cards_demo.dart';
|
||||
export 'chip_demo.dart';
|
||||
export 'data_table_demo.dart';
|
||||
export 'date_and_time_picker_demo.dart';
|
||||
export 'dialog_demo.dart';
|
||||
export 'drawer_demo.dart';
|
||||
export 'elevation_demo.dart';
|
||||
export 'expansion_panels_demo.dart';
|
||||
export 'expansion_tile_list_demo.dart';
|
||||
export 'grid_list_demo.dart';
|
||||
export 'icons_demo.dart';
|
||||
export 'leave_behind_demo.dart';
|
||||
export 'list_demo.dart';
|
||||
export 'menu_demo.dart';
|
||||
export 'modal_bottom_sheet_demo.dart';
|
||||
export 'overscroll_demo.dart';
|
||||
export 'page_selector_demo.dart';
|
||||
export 'persistent_bottom_sheet_demo.dart';
|
||||
export 'progress_indicator_demo.dart';
|
||||
export 'reorderable_list_demo.dart';
|
||||
export 'scrollable_tabs_demo.dart';
|
||||
export 'search_demo.dart';
|
||||
export 'selection_controls_demo.dart';
|
||||
export 'slider_demo.dart';
|
||||
export 'snack_bar_demo.dart';
|
||||
export 'tabs_demo.dart';
|
||||
export 'tabs_fab_demo.dart';
|
||||
export 'text_form_field_demo.dart';
|
||||
export 'tooltip_demo.dart';
|
||||
@ -1,219 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class MenuDemo extends StatefulWidget {
|
||||
const MenuDemo({ Key key }) : super(key: key);
|
||||
|
||||
static const String routeName = '/material/menu';
|
||||
|
||||
@override
|
||||
MenuDemoState createState() => MenuDemoState();
|
||||
}
|
||||
|
||||
class MenuDemoState extends State<MenuDemo> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
final String _simpleValue1 = 'Menu item value one';
|
||||
final String _simpleValue2 = 'Menu item value two';
|
||||
final String _simpleValue3 = 'Menu item value three';
|
||||
String _simpleValue;
|
||||
|
||||
final String _checkedValue1 = 'One';
|
||||
final String _checkedValue2 = 'Two';
|
||||
final String _checkedValue3 = 'Free';
|
||||
final String _checkedValue4 = 'Four';
|
||||
List<String> _checkedValues;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_simpleValue = _simpleValue2;
|
||||
_checkedValues = <String>[_checkedValue3];
|
||||
}
|
||||
|
||||
void showInSnackBar(String value) {
|
||||
_scaffoldKey.currentState.showSnackBar(SnackBar(
|
||||
content: Text(value),
|
||||
));
|
||||
}
|
||||
|
||||
void showMenuSelection(String value) {
|
||||
if (<String>[_simpleValue1, _simpleValue2, _simpleValue3].contains(value))
|
||||
_simpleValue = value;
|
||||
showInSnackBar('You selected: $value');
|
||||
}
|
||||
|
||||
void showCheckedMenuSelections(String value) {
|
||||
if (_checkedValues.contains(value))
|
||||
_checkedValues.remove(value);
|
||||
else
|
||||
_checkedValues.add(value);
|
||||
|
||||
showInSnackBar('Checked $_checkedValues');
|
||||
}
|
||||
|
||||
bool isChecked(String value) => _checkedValues.contains(value);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: const Text('Menus'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(MenuDemo.routeName),
|
||||
PopupMenuButton<String>(
|
||||
onSelected: showMenuSelection,
|
||||
itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
|
||||
const PopupMenuItem<String>(
|
||||
value: 'Toolbar menu',
|
||||
child: Text('Toolbar menu'),
|
||||
),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'Right here',
|
||||
child: Text('Right here'),
|
||||
),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'Hooray!',
|
||||
child: Text('Hooray!'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
padding: kMaterialListPadding,
|
||||
children: <Widget>[
|
||||
// Pressing the PopupMenuButton on the right of this item shows
|
||||
// a simple menu with one disabled item. Typically the contents
|
||||
// of this "contextual menu" would reflect the app's state.
|
||||
ListTile(
|
||||
title: const Text('An item with a context menu button'),
|
||||
trailing: PopupMenuButton<String>(
|
||||
padding: EdgeInsets.zero,
|
||||
onSelected: showMenuSelection,
|
||||
itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
|
||||
PopupMenuItem<String>(
|
||||
value: _simpleValue1,
|
||||
child: const Text('Context menu item one'),
|
||||
),
|
||||
const PopupMenuItem<String>(
|
||||
enabled: false,
|
||||
child: Text('A disabled menu item'),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
value: _simpleValue3,
|
||||
child: const Text('Context menu item three'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Pressing the PopupMenuButton on the right of this item shows
|
||||
// a menu whose items have text labels and icons and a divider
|
||||
// That separates the first three items from the last one.
|
||||
ListTile(
|
||||
title: const Text('An item with a sectioned menu'),
|
||||
trailing: PopupMenuButton<String>(
|
||||
padding: EdgeInsets.zero,
|
||||
onSelected: showMenuSelection,
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
|
||||
const PopupMenuItem<String>(
|
||||
value: 'Preview',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.visibility),
|
||||
title: Text('Preview'),
|
||||
),
|
||||
),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'Share',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.person_add),
|
||||
title: Text('Share'),
|
||||
),
|
||||
),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'Get Link',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.link),
|
||||
title: Text('Get link'),
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'Remove',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.delete),
|
||||
title: Text('Remove'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// This entire list item is a PopupMenuButton. Tapping anywhere shows
|
||||
// a menu whose current value is highlighted and aligned over the
|
||||
// list item's center line.
|
||||
PopupMenuButton<String>(
|
||||
padding: EdgeInsets.zero,
|
||||
initialValue: _simpleValue,
|
||||
onSelected: showMenuSelection,
|
||||
child: ListTile(
|
||||
title: const Text('An item with a simple menu'),
|
||||
subtitle: Text(_simpleValue),
|
||||
),
|
||||
itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
|
||||
PopupMenuItem<String>(
|
||||
value: _simpleValue1,
|
||||
child: Text(_simpleValue1),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
value: _simpleValue2,
|
||||
child: Text(_simpleValue2),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
value: _simpleValue3,
|
||||
child: Text(_simpleValue3),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Pressing the PopupMenuButton on the right of this item shows a menu
|
||||
// whose items have checked icons that reflect this app's state.
|
||||
ListTile(
|
||||
title: const Text('An item with a checklist menu'),
|
||||
trailing: PopupMenuButton<String>(
|
||||
padding: EdgeInsets.zero,
|
||||
onSelected: showCheckedMenuSelections,
|
||||
itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
|
||||
CheckedPopupMenuItem<String>(
|
||||
value: _checkedValue1,
|
||||
checked: isChecked(_checkedValue1),
|
||||
child: Text(_checkedValue1),
|
||||
),
|
||||
CheckedPopupMenuItem<String>(
|
||||
value: _checkedValue2,
|
||||
enabled: false,
|
||||
checked: isChecked(_checkedValue2),
|
||||
child: Text(_checkedValue2),
|
||||
),
|
||||
CheckedPopupMenuItem<String>(
|
||||
value: _checkedValue3,
|
||||
checked: isChecked(_checkedValue3),
|
||||
child: Text(_checkedValue3),
|
||||
),
|
||||
CheckedPopupMenuItem<String>(
|
||||
value: _checkedValue4,
|
||||
checked: isChecked(_checkedValue4),
|
||||
child: Text(_checkedValue4),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class ModalBottomSheetDemo extends StatelessWidget {
|
||||
static const String routeName = '/material/modal-bottom-sheet';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Modal bottom sheet'),
|
||||
actions: <Widget>[MaterialDemoDocumentationButton(routeName)],
|
||||
),
|
||||
body: Center(
|
||||
child: RaisedButton(
|
||||
child: const Text('SHOW BOTTOM SHEET'),
|
||||
onPressed: () {
|
||||
showModalBottomSheet<void>(context: context, builder: (BuildContext context) {
|
||||
return Container(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Text('This is the modal bottom sheet. Slide down to dismiss.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontSize: 24.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
enum IndicatorType { overscroll, refresh }
|
||||
|
||||
class OverscrollDemo extends StatefulWidget {
|
||||
const OverscrollDemo({ Key key }) : super(key: key);
|
||||
|
||||
static const String routeName = '/material/overscroll';
|
||||
|
||||
@override
|
||||
OverscrollDemoState createState() => OverscrollDemoState();
|
||||
}
|
||||
|
||||
class OverscrollDemoState extends State<OverscrollDemo> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
|
||||
static final List<String> _items = <String>[
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
|
||||
];
|
||||
|
||||
Future<void> _handleRefresh() {
|
||||
final Completer<void> completer = Completer<void>();
|
||||
Timer(const Duration(seconds: 3), () { completer.complete(); });
|
||||
return completer.future.then<void>((_) {
|
||||
_scaffoldKey.currentState?.showSnackBar(SnackBar(
|
||||
content: const Text('Refresh complete'),
|
||||
action: SnackBarAction(
|
||||
label: 'RETRY',
|
||||
onPressed: () {
|
||||
_refreshIndicatorKey.currentState.show();
|
||||
},
|
||||
),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: const Text('Pull to refresh'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(OverscrollDemo.routeName),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
tooltip: 'Refresh',
|
||||
onPressed: () {
|
||||
_refreshIndicatorKey.currentState.show();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
key: _refreshIndicatorKey,
|
||||
onRefresh: _handleRefresh,
|
||||
child: Scrollbar(
|
||||
child: ListView.builder(
|
||||
padding: kMaterialListPadding,
|
||||
itemCount: _items.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final String item = _items[index];
|
||||
return ListTile(
|
||||
isThreeLine: true,
|
||||
leading: CircleAvatar(child: Text(item)),
|
||||
title: Text('This item represents $item.'),
|
||||
subtitle: const Text('Even more additional list item information appears on line three.'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,100 +0,0 @@
|
||||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class _PageSelector extends StatelessWidget {
|
||||
const _PageSelector({ this.icons });
|
||||
|
||||
final List<Icon> icons;
|
||||
|
||||
void _handleArrowButtonPress(BuildContext context, int delta) {
|
||||
final TabController controller = DefaultTabController.of(context);
|
||||
if (!controller.indexIsChanging)
|
||||
controller.animateTo((controller.index + delta).clamp(0, icons.length - 1));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TabController controller = DefaultTabController.of(context);
|
||||
final Color color = Theme.of(context).accentColor;
|
||||
return SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 16.0),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
color: color,
|
||||
onPressed: () { _handleArrowButtonPress(context, -1); },
|
||||
tooltip: 'Page back',
|
||||
),
|
||||
TabPageSelector(controller: controller),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
color: color,
|
||||
onPressed: () { _handleArrowButtonPress(context, 1); },
|
||||
tooltip: 'Page forward',
|
||||
),
|
||||
],
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: IconTheme(
|
||||
data: IconThemeData(
|
||||
size: 128.0,
|
||||
color: color,
|
||||
),
|
||||
child: TabBarView(
|
||||
children: icons.map<Widget>((Icon icon) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Card(
|
||||
child: Center(
|
||||
child: icon,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PageSelectorDemo extends StatelessWidget {
|
||||
static const String routeName = '/material/page-selector';
|
||||
static final List<Icon> icons = <Icon>[
|
||||
const Icon(Icons.event, semanticLabel: 'Event'),
|
||||
const Icon(Icons.home, semanticLabel: 'Home'),
|
||||
const Icon(Icons.android, semanticLabel: 'Android'),
|
||||
const Icon(Icons.alarm, semanticLabel: 'Alarm'),
|
||||
const Icon(Icons.face, semanticLabel: 'Face'),
|
||||
const Icon(Icons.language, semanticLabel: 'Language'),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Page selector'),
|
||||
actions: <Widget>[MaterialDemoDocumentationButton(routeName)],
|
||||
),
|
||||
body: DefaultTabController(
|
||||
length: icons.length,
|
||||
child: _PageSelector(icons: icons),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,103 +0,0 @@
|
||||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class PersistentBottomSheetDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/persistent-bottom-sheet';
|
||||
|
||||
@override
|
||||
_PersistentBottomSheetDemoState createState() => _PersistentBottomSheetDemoState();
|
||||
}
|
||||
|
||||
class _PersistentBottomSheetDemoState extends State<PersistentBottomSheetDemo> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
VoidCallback _showBottomSheetCallback;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_showBottomSheetCallback = _showBottomSheet;
|
||||
}
|
||||
|
||||
void _showBottomSheet() {
|
||||
setState(() { // disable the button
|
||||
_showBottomSheetCallback = null;
|
||||
});
|
||||
_scaffoldKey.currentState.showBottomSheet<void>((BuildContext context) {
|
||||
final ThemeData themeData = Theme.of(context);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(top: BorderSide(color: themeData.disabledColor))
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Text('This is a Material persistent bottom sheet. Drag downwards to dismiss it.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: themeData.accentColor,
|
||||
fontSize: 24.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
})
|
||||
.closed.whenComplete(() {
|
||||
if (mounted) {
|
||||
setState(() { // re-enable the button
|
||||
_showBottomSheetCallback = _showBottomSheet;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _showMessage() {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
content: const Text('You tapped the floating action button.'),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: const Text('Persistent bottom sheet'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(PersistentBottomSheetDemo.routeName),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _showMessage,
|
||||
backgroundColor: Colors.redAccent,
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
semanticLabel: 'Add',
|
||||
),
|
||||
),
|
||||
body: Center(
|
||||
child: RaisedButton(
|
||||
onPressed: _showBottomSheetCallback,
|
||||
child: const Text('SHOW BOTTOM SHEET'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,133 +0,0 @@
|
||||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class ProgressIndicatorDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/progress-indicator';
|
||||
|
||||
@override
|
||||
_ProgressIndicatorDemoState createState() => _ProgressIndicatorDemoState();
|
||||
}
|
||||
|
||||
class _ProgressIndicatorDemoState extends State<ProgressIndicatorDemo> with SingleTickerProviderStateMixin {
|
||||
AnimationController _controller;
|
||||
Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
vsync: this,
|
||||
animationBehavior: AnimationBehavior.preserve,
|
||||
)..forward();
|
||||
|
||||
_animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.0, 0.9, curve: Curves.fastOutSlowIn),
|
||||
reverseCurve: Curves.fastOutSlowIn,
|
||||
)..addStatusListener((AnimationStatus status) {
|
||||
if (status == AnimationStatus.dismissed)
|
||||
_controller.forward();
|
||||
else if (status == AnimationStatus.completed)
|
||||
_controller.reverse();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.stop();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleTap() {
|
||||
setState(() {
|
||||
// valueAnimation.isAnimating is part of our build state
|
||||
if (_controller.isAnimating) {
|
||||
_controller.stop();
|
||||
} else {
|
||||
switch (_controller.status) {
|
||||
case AnimationStatus.dismissed:
|
||||
case AnimationStatus.forward:
|
||||
_controller.forward();
|
||||
break;
|
||||
case AnimationStatus.reverse:
|
||||
case AnimationStatus.completed:
|
||||
_controller.reverse();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildIndicators(BuildContext context, Widget child) {
|
||||
final List<Widget> indicators = <Widget>[
|
||||
const SizedBox(
|
||||
width: 200.0,
|
||||
child: LinearProgressIndicator(),
|
||||
),
|
||||
const LinearProgressIndicator(),
|
||||
const LinearProgressIndicator(),
|
||||
LinearProgressIndicator(value: _animation.value),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
const CircularProgressIndicator(),
|
||||
SizedBox(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: CircularProgressIndicator(value: _animation.value),
|
||||
),
|
||||
SizedBox(
|
||||
width: 100.0,
|
||||
height: 20.0,
|
||||
child: Text('${(_animation.value * 100.0).toStringAsFixed(1)}%',
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
return Column(
|
||||
children: indicators
|
||||
.map<Widget>((Widget c) => Container(child: c, margin: const EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0)))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Progress indicators'),
|
||||
actions: <Widget>[MaterialDemoDocumentationButton(ProgressIndicatorDemo.routeName)],
|
||||
),
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
child: DefaultTextStyle(
|
||||
style: Theme.of(context).textTheme.title,
|
||||
child: GestureDetector(
|
||||
onTap: _handleTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 8.0),
|
||||
child: AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: _buildIndicators,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,221 +0,0 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
enum _ReorderableListType {
|
||||
/// A list tile that contains a [CircleAvatar].
|
||||
horizontalAvatar,
|
||||
|
||||
/// A list tile that contains a [CircleAvatar].
|
||||
verticalAvatar,
|
||||
|
||||
/// A list tile that contains three lines of text and a checkbox.
|
||||
threeLine,
|
||||
}
|
||||
|
||||
class ReorderableListDemo extends StatefulWidget {
|
||||
const ReorderableListDemo({ Key key }) : super(key: key);
|
||||
|
||||
static const String routeName = '/material/reorderable-list';
|
||||
|
||||
@override
|
||||
_ListDemoState createState() => _ListDemoState();
|
||||
}
|
||||
|
||||
class _ListItem {
|
||||
_ListItem(this.value, this.checkState);
|
||||
|
||||
final String value;
|
||||
|
||||
bool checkState;
|
||||
}
|
||||
|
||||
class _ListDemoState extends State<ReorderableListDemo> {
|
||||
static final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
PersistentBottomSheetController<void> _bottomSheet;
|
||||
_ReorderableListType _itemType = _ReorderableListType.threeLine;
|
||||
bool _reverse = false;
|
||||
bool _reverseSort = false;
|
||||
final List<_ListItem> _items = <String>[
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
|
||||
].map<_ListItem>((String item) => _ListItem(item, false)).toList();
|
||||
|
||||
void changeItemType(_ReorderableListType type) {
|
||||
setState(() {
|
||||
_itemType = type;
|
||||
});
|
||||
// Rebuild the bottom sheet to reflect the selected list view.
|
||||
_bottomSheet?.setState(() {
|
||||
// Trigger a rebuild.
|
||||
});
|
||||
// Close the bottom sheet to give the user a clear view of the list.
|
||||
_bottomSheet?.close();
|
||||
}
|
||||
|
||||
void changeReverse(bool newValue) {
|
||||
setState(() {
|
||||
_reverse = newValue;
|
||||
});
|
||||
// Rebuild the bottom sheet to reflect the selected list view.
|
||||
_bottomSheet?.setState(() {
|
||||
// Trigger a rebuild.
|
||||
});
|
||||
// Close the bottom sheet to give the user a clear view of the list.
|
||||
_bottomSheet?.close();
|
||||
}
|
||||
|
||||
void _showConfigurationSheet() {
|
||||
setState(() {
|
||||
_bottomSheet = scaffoldKey.currentState.showBottomSheet<void>((BuildContext bottomSheetContext) {
|
||||
return DecoratedBox(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(top: BorderSide(color: Colors.black26)),
|
||||
),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
primary: false,
|
||||
children: <Widget>[
|
||||
CheckboxListTile(
|
||||
dense: true,
|
||||
title: const Text('Reverse'),
|
||||
value: _reverse,
|
||||
onChanged: changeReverse,
|
||||
),
|
||||
RadioListTile<_ReorderableListType>(
|
||||
dense: true,
|
||||
title: const Text('Horizontal Avatars'),
|
||||
value: _ReorderableListType.horizontalAvatar,
|
||||
groupValue: _itemType,
|
||||
onChanged: changeItemType,
|
||||
),
|
||||
RadioListTile<_ReorderableListType>(
|
||||
dense: true,
|
||||
title: const Text('Vertical Avatars'),
|
||||
value: _ReorderableListType.verticalAvatar,
|
||||
groupValue: _itemType,
|
||||
onChanged: changeItemType,
|
||||
),
|
||||
RadioListTile<_ReorderableListType>(
|
||||
dense: true,
|
||||
title: const Text('Three-line'),
|
||||
value: _ReorderableListType.threeLine,
|
||||
groupValue: _itemType,
|
||||
onChanged: changeItemType,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// Garbage collect the bottom sheet when it closes.
|
||||
_bottomSheet.closed.whenComplete(() {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_bottomSheet = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Widget buildListTile(_ListItem item) {
|
||||
const Widget secondary = Text(
|
||||
'Even more additional list item information appears on line three.',
|
||||
);
|
||||
Widget listTile;
|
||||
switch (_itemType) {
|
||||
case _ReorderableListType.threeLine:
|
||||
listTile = CheckboxListTile(
|
||||
key: Key(item.value),
|
||||
isThreeLine: true,
|
||||
value: item.checkState ?? false,
|
||||
onChanged: (bool newValue) {
|
||||
setState(() {
|
||||
item.checkState = newValue;
|
||||
});
|
||||
},
|
||||
title: Text('This item represents ${item.value}.'),
|
||||
subtitle: secondary,
|
||||
secondary: const Icon(Icons.drag_handle),
|
||||
);
|
||||
break;
|
||||
case _ReorderableListType.horizontalAvatar:
|
||||
case _ReorderableListType.verticalAvatar:
|
||||
listTile = Container(
|
||||
key: Key(item.value),
|
||||
height: 100.0,
|
||||
width: 100.0,
|
||||
child: CircleAvatar(child: Text(item.value),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return listTile;
|
||||
}
|
||||
|
||||
void _onReorder(int oldIndex, int newIndex) {
|
||||
setState(() {
|
||||
if (newIndex > oldIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final _ListItem item = _items.removeAt(oldIndex);
|
||||
_items.insert(newIndex, item);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
key: scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: const Text('Reorderable list'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(ReorderableListDemo.routeName),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.sort_by_alpha),
|
||||
tooltip: 'Sort',
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_reverseSort = !_reverseSort;
|
||||
_items.sort((_ListItem a, _ListItem b) => _reverseSort ? b.value.compareTo(a.value) : a.value.compareTo(b.value));
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Theme.of(context).platform == TargetPlatform.iOS
|
||||
? Icons.more_horiz
|
||||
: Icons.more_vert,
|
||||
),
|
||||
tooltip: 'Show menu',
|
||||
onPressed: _bottomSheet == null ? _showConfigurationSheet : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Scrollbar(
|
||||
child: ReorderableListView(
|
||||
header: _itemType != _ReorderableListType.threeLine
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text('Header of the list', style: Theme.of(context).textTheme.headline))
|
||||
: null,
|
||||
onReorder: _onReorder,
|
||||
reverse: _reverse,
|
||||
scrollDirection: _itemType == _ReorderableListType.horizontalAvatar ? Axis.horizontal : Axis.vertical,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
children: _items.map<Widget>(buildListTile).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,200 +0,0 @@
|
||||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
enum TabsDemoStyle {
|
||||
iconsAndText,
|
||||
iconsOnly,
|
||||
textOnly
|
||||
}
|
||||
|
||||
class _Page {
|
||||
const _Page({ this.icon, this.text });
|
||||
final IconData icon;
|
||||
final String text;
|
||||
}
|
||||
|
||||
const List<_Page> _allPages = <_Page>[
|
||||
_Page(icon: Icons.grade, text: 'TRIUMPH'),
|
||||
_Page(icon: Icons.playlist_add, text: 'NOTE'),
|
||||
_Page(icon: Icons.check_circle, text: 'SUCCESS'),
|
||||
_Page(icon: Icons.question_answer, text: 'OVERSTATE'),
|
||||
_Page(icon: Icons.sentiment_very_satisfied, text: 'SATISFACTION'),
|
||||
_Page(icon: Icons.camera, text: 'APERTURE'),
|
||||
_Page(icon: Icons.assignment_late, text: 'WE MUST'),
|
||||
_Page(icon: Icons.assignment_turned_in, text: 'WE CAN'),
|
||||
_Page(icon: Icons.group, text: 'ALL'),
|
||||
_Page(icon: Icons.block, text: 'EXCEPT'),
|
||||
_Page(icon: Icons.sentiment_very_dissatisfied, text: 'CRYING'),
|
||||
_Page(icon: Icons.error, text: 'MISTAKE'),
|
||||
_Page(icon: Icons.loop, text: 'TRYING'),
|
||||
_Page(icon: Icons.cake, text: 'CAKE'),
|
||||
];
|
||||
|
||||
class ScrollableTabsDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/scrollable-tabs';
|
||||
|
||||
@override
|
||||
ScrollableTabsDemoState createState() => ScrollableTabsDemoState();
|
||||
}
|
||||
|
||||
class ScrollableTabsDemoState extends State<ScrollableTabsDemo> with SingleTickerProviderStateMixin {
|
||||
TabController _controller;
|
||||
TabsDemoStyle _demoStyle = TabsDemoStyle.iconsAndText;
|
||||
bool _customIndicator = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TabController(vsync: this, length: _allPages.length);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void changeDemoStyle(TabsDemoStyle style) {
|
||||
setState(() {
|
||||
_demoStyle = style;
|
||||
});
|
||||
}
|
||||
|
||||
Decoration getIndicator() {
|
||||
if (!_customIndicator)
|
||||
return const UnderlineTabIndicator();
|
||||
|
||||
switch(_demoStyle) {
|
||||
case TabsDemoStyle.iconsAndText:
|
||||
return ShapeDecoration(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(4.0)),
|
||||
side: BorderSide(
|
||||
color: Colors.white24,
|
||||
width: 2.0,
|
||||
),
|
||||
) + const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(4.0)),
|
||||
side: BorderSide(
|
||||
color: Colors.transparent,
|
||||
width: 4.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
case TabsDemoStyle.iconsOnly:
|
||||
return ShapeDecoration(
|
||||
shape: const CircleBorder(
|
||||
side: BorderSide(
|
||||
color: Colors.white24,
|
||||
width: 4.0,
|
||||
),
|
||||
) + const CircleBorder(
|
||||
side: BorderSide(
|
||||
color: Colors.transparent,
|
||||
width: 4.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
case TabsDemoStyle.textOnly:
|
||||
return ShapeDecoration(
|
||||
shape: const StadiumBorder(
|
||||
side: BorderSide(
|
||||
color: Colors.white24,
|
||||
width: 2.0,
|
||||
),
|
||||
) + const StadiumBorder(
|
||||
side: BorderSide(
|
||||
color: Colors.transparent,
|
||||
width: 4.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color iconColor = Theme.of(context).accentColor;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Scrollable tabs'),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(ScrollableTabsDemo.routeName),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.sentiment_very_satisfied),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_customIndicator = !_customIndicator;
|
||||
});
|
||||
},
|
||||
),
|
||||
PopupMenuButton<TabsDemoStyle>(
|
||||
onSelected: changeDemoStyle,
|
||||
itemBuilder: (BuildContext context) => <PopupMenuItem<TabsDemoStyle>>[
|
||||
const PopupMenuItem<TabsDemoStyle>(
|
||||
value: TabsDemoStyle.iconsAndText,
|
||||
child: Text('Icons and text'),
|
||||
),
|
||||
const PopupMenuItem<TabsDemoStyle>(
|
||||
value: TabsDemoStyle.iconsOnly,
|
||||
child: Text('Icons only'),
|
||||
),
|
||||
const PopupMenuItem<TabsDemoStyle>(
|
||||
value: TabsDemoStyle.textOnly,
|
||||
child: Text('Text only'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
bottom: TabBar(
|
||||
controller: _controller,
|
||||
isScrollable: true,
|
||||
indicator: getIndicator(),
|
||||
tabs: _allPages.map<Tab>((_Page page) {
|
||||
assert(_demoStyle != null);
|
||||
switch (_demoStyle) {
|
||||
case TabsDemoStyle.iconsAndText:
|
||||
return Tab(text: page.text, icon: Icon(page.icon));
|
||||
case TabsDemoStyle.iconsOnly:
|
||||
return Tab(icon: Icon(page.icon));
|
||||
case TabsDemoStyle.textOnly:
|
||||
return Tab(text: page.text);
|
||||
}
|
||||
return null;
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
controller: _controller,
|
||||
children: _allPages.map<Widget>((_Page page) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Container(
|
||||
key: ObjectKey(page.icon),
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Card(
|
||||
child: Center(
|
||||
child: Icon(
|
||||
page.icon,
|
||||
color: iconColor,
|
||||
size: 128.0,
|
||||
semanticLabel: 'Placeholder for ${page.text} tab',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,295 +0,0 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class SearchDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/search';
|
||||
|
||||
@override
|
||||
_SearchDemoState createState() => _SearchDemoState();
|
||||
}
|
||||
|
||||
class _SearchDemoState extends State<SearchDemo> {
|
||||
final _SearchDemoSearchDelegate _delegate = _SearchDemoSearchDelegate();
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
int _lastIntegerSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
tooltip: 'Navigation menu',
|
||||
icon: AnimatedIcon(
|
||||
icon: AnimatedIcons.menu_arrow,
|
||||
color: Colors.white,
|
||||
progress: _delegate.transitionAnimation,
|
||||
),
|
||||
onPressed: () {
|
||||
_scaffoldKey.currentState.openDrawer();
|
||||
},
|
||||
),
|
||||
title: const Text('Numbers'),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
tooltip: 'Search',
|
||||
icon: const Icon(Icons.search),
|
||||
onPressed: () async {
|
||||
final int selected = await showSearch<int>(
|
||||
context: context,
|
||||
delegate: _delegate,
|
||||
);
|
||||
if (selected != null && selected != _lastIntegerSelected) {
|
||||
setState(() {
|
||||
_lastIntegerSelected = selected;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
MaterialDemoDocumentationButton(SearchDemo.routeName),
|
||||
IconButton(
|
||||
tooltip: 'More (not implemented)',
|
||||
icon: Icon(
|
||||
Theme.of(context).platform == TargetPlatform.iOS
|
||||
? Icons.more_horiz
|
||||
: Icons.more_vert,
|
||||
),
|
||||
onPressed: () { },
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
MergeSemantics(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const <Widget>[
|
||||
Text('Press the '),
|
||||
Tooltip(
|
||||
message: 'search',
|
||||
child: Icon(
|
||||
Icons.search,
|
||||
size: 18.0,
|
||||
),
|
||||
),
|
||||
Text(' icon in the AppBar'),
|
||||
],
|
||||
),
|
||||
const Text('and search for an integer between 0 and 100,000.'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 64.0),
|
||||
Text('Last selected integer: ${_lastIntegerSelected ?? 'NONE' }.'),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
tooltip: 'Back', // Tests depend on this label to exit the demo.
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
label: const Text('Close demo'),
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
drawer: Drawer(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
const UserAccountsDrawerHeader(
|
||||
accountName: Text('Peter Widget'),
|
||||
accountEmail: Text('peter.widget@example.com'),
|
||||
currentAccountPicture: CircleAvatar(
|
||||
backgroundImage: AssetImage(
|
||||
'people/square/peter.png',
|
||||
package: 'flutter_gallery_assets',
|
||||
),
|
||||
),
|
||||
margin: EdgeInsets.zero,
|
||||
),
|
||||
MediaQuery.removePadding(
|
||||
context: context,
|
||||
// DrawerHeader consumes top MediaQuery padding.
|
||||
removeTop: true,
|
||||
child: const ListTile(
|
||||
leading: Icon(Icons.payment),
|
||||
title: Text('Placeholder'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SearchDemoSearchDelegate extends SearchDelegate<int> {
|
||||
final List<int> _data = List<int>.generate(100001, (int i) => i).reversed.toList();
|
||||
final List<int> _history = <int>[42607, 85604, 66374, 44, 174];
|
||||
|
||||
@override
|
||||
Widget buildLeading(BuildContext context) {
|
||||
return IconButton(
|
||||
tooltip: 'Back',
|
||||
icon: AnimatedIcon(
|
||||
icon: AnimatedIcons.menu_arrow,
|
||||
progress: transitionAnimation,
|
||||
),
|
||||
onPressed: () {
|
||||
close(context, null);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildSuggestions(BuildContext context) {
|
||||
|
||||
final Iterable<int> suggestions = query.isEmpty
|
||||
? _history
|
||||
: _data.where((int i) => '$i'.startsWith(query));
|
||||
|
||||
return _SuggestionList(
|
||||
query: query,
|
||||
suggestions: suggestions.map<String>((int i) => '$i').toList(),
|
||||
onSelected: (String suggestion) {
|
||||
query = suggestion;
|
||||
showResults(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildResults(BuildContext context) {
|
||||
final int searched = int.tryParse(query);
|
||||
if (searched == null || !_data.contains(searched)) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'"$query"\n is not a valid integer between 0 and 100,000.\nTry again.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView(
|
||||
children: <Widget>[
|
||||
_ResultCard(
|
||||
title: 'This integer',
|
||||
integer: searched,
|
||||
searchDelegate: this,
|
||||
),
|
||||
_ResultCard(
|
||||
title: 'Next integer',
|
||||
integer: searched + 1,
|
||||
searchDelegate: this,
|
||||
),
|
||||
_ResultCard(
|
||||
title: 'Previous integer',
|
||||
integer: searched - 1,
|
||||
searchDelegate: this,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Widget> buildActions(BuildContext context) {
|
||||
return <Widget>[
|
||||
if (query.isEmpty)
|
||||
IconButton(
|
||||
tooltip: 'Voice Search',
|
||||
icon: const Icon(Icons.mic),
|
||||
onPressed: () {
|
||||
query = 'TODO: implement voice input';
|
||||
},
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
tooltip: 'Clear',
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
query = '';
|
||||
showSuggestions(context);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class _ResultCard extends StatelessWidget {
|
||||
const _ResultCard({this.integer, this.title, this.searchDelegate});
|
||||
|
||||
final int integer;
|
||||
final String title;
|
||||
final SearchDelegate<int> searchDelegate;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
searchDelegate.close(context, integer);
|
||||
},
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Text(title),
|
||||
Text(
|
||||
'$integer',
|
||||
style: theme.textTheme.headline.copyWith(fontSize: 72.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SuggestionList extends StatelessWidget {
|
||||
const _SuggestionList({this.suggestions, this.query, this.onSelected});
|
||||
|
||||
final List<String> suggestions;
|
||||
final String query;
|
||||
final ValueChanged<String> onSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return ListView.builder(
|
||||
itemCount: suggestions.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
final String suggestion = suggestions[i];
|
||||
return ListTile(
|
||||
leading: query.isEmpty ? const Icon(Icons.history) : const Icon(null),
|
||||
title: RichText(
|
||||
text: TextSpan(
|
||||
text: suggestion.substring(0, query.length),
|
||||
style: theme.textTheme.subhead.copyWith(fontWeight: FontWeight.bold),
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: suggestion.substring(query.length),
|
||||
style: theme.textTheme.subhead,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
onSelected(suggestion);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,441 +0,0 @@
|
||||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class SliderDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/slider';
|
||||
|
||||
@override
|
||||
_SliderDemoState createState() => _SliderDemoState();
|
||||
}
|
||||
|
||||
Path _downTriangle(double size, Offset thumbCenter, { bool invert = false }) {
|
||||
final Path thumbPath = Path();
|
||||
final double height = math.sqrt(3.0) / 2.0;
|
||||
final double centerHeight = size * height / 3.0;
|
||||
final double halfSize = size / 2.0;
|
||||
final double sign = invert ? -1.0 : 1.0;
|
||||
thumbPath.moveTo(thumbCenter.dx - halfSize, thumbCenter.dy + sign * centerHeight);
|
||||
thumbPath.lineTo(thumbCenter.dx, thumbCenter.dy - 2.0 * sign * centerHeight);
|
||||
thumbPath.lineTo(thumbCenter.dx + halfSize, thumbCenter.dy + sign * centerHeight);
|
||||
thumbPath.close();
|
||||
return thumbPath;
|
||||
}
|
||||
|
||||
Path _rightTriangle(double size, Offset thumbCenter, { bool invert = false }) {
|
||||
final Path thumbPath = Path();
|
||||
final double halfSize = size / 2.0;
|
||||
final double sign = invert ? -1.0 : 1.0;
|
||||
thumbPath.moveTo(thumbCenter.dx + halfSize * sign, thumbCenter.dy);
|
||||
thumbPath.lineTo(thumbCenter.dx - halfSize * sign, thumbCenter.dy - size);
|
||||
thumbPath.lineTo(thumbCenter.dx - halfSize * sign, thumbCenter.dy + size);
|
||||
thumbPath.close();
|
||||
return thumbPath;
|
||||
}
|
||||
|
||||
Path _upTriangle(double size, Offset thumbCenter) => _downTriangle(size, thumbCenter, invert: true);
|
||||
|
||||
Path _leftTriangle(double size, Offset thumbCenter) => _rightTriangle(size, thumbCenter, invert: true);
|
||||
|
||||
class _CustomRangeThumbShape extends RangeSliderThumbShape {
|
||||
static const double _thumbSize = 4.0;
|
||||
static const double _disabledThumbSize = 3.0;
|
||||
|
||||
@override
|
||||
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
|
||||
return isEnabled ? const Size.fromRadius(_thumbSize) : const Size.fromRadius(_disabledThumbSize);
|
||||
}
|
||||
|
||||
static final Animatable<double> sizeTween = Tween<double>(
|
||||
begin: _disabledThumbSize,
|
||||
end: _thumbSize,
|
||||
);
|
||||
|
||||
@override
|
||||
void paint(
|
||||
PaintingContext context,
|
||||
Offset center, {
|
||||
@required Animation<double> activationAnimation,
|
||||
@required Animation<double> enableAnimation,
|
||||
bool isDiscrete = false,
|
||||
bool isEnabled = false,
|
||||
bool isOnTop,
|
||||
@required SliderThemeData sliderTheme,
|
||||
TextDirection textDirection,
|
||||
Thumb thumb,
|
||||
}) {
|
||||
final Canvas canvas = context.canvas;
|
||||
final ColorTween colorTween = ColorTween(
|
||||
begin: sliderTheme.disabledThumbColor,
|
||||
end: sliderTheme.thumbColor,
|
||||
);
|
||||
|
||||
final double size = _thumbSize * sizeTween.evaluate(enableAnimation);
|
||||
Path thumbPath;
|
||||
switch (textDirection) {
|
||||
case TextDirection.rtl:
|
||||
switch (thumb) {
|
||||
case Thumb.start:
|
||||
thumbPath = _rightTriangle(size, center);
|
||||
break;
|
||||
case Thumb.end:
|
||||
thumbPath = _leftTriangle(size, center);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case TextDirection.ltr:
|
||||
switch (thumb) {
|
||||
case Thumb.start:
|
||||
thumbPath = _leftTriangle(size, center);
|
||||
break;
|
||||
case Thumb.end:
|
||||
thumbPath = _rightTriangle(size, center);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
canvas.drawPath(thumbPath, Paint()..color = colorTween.evaluate(enableAnimation));
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomThumbShape extends SliderComponentShape {
|
||||
static const double _thumbSize = 4.0;
|
||||
static const double _disabledThumbSize = 3.0;
|
||||
|
||||
@override
|
||||
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
|
||||
return isEnabled ? const Size.fromRadius(_thumbSize) : const Size.fromRadius(_disabledThumbSize);
|
||||
}
|
||||
|
||||
static final Animatable<double> sizeTween = Tween<double>(
|
||||
begin: _disabledThumbSize,
|
||||
end: _thumbSize,
|
||||
);
|
||||
|
||||
@override
|
||||
void paint(
|
||||
PaintingContext context,
|
||||
Offset thumbCenter, {
|
||||
Animation<double> activationAnimation,
|
||||
Animation<double> enableAnimation,
|
||||
bool isDiscrete,
|
||||
TextPainter labelPainter,
|
||||
RenderBox parentBox,
|
||||
SliderThemeData sliderTheme,
|
||||
TextDirection textDirection,
|
||||
double value,
|
||||
}) {
|
||||
final Canvas canvas = context.canvas;
|
||||
final ColorTween colorTween = ColorTween(
|
||||
begin: sliderTheme.disabledThumbColor,
|
||||
end: sliderTheme.thumbColor,
|
||||
);
|
||||
final double size = _thumbSize * sizeTween.evaluate(enableAnimation);
|
||||
final Path thumbPath = _downTriangle(size, thumbCenter);
|
||||
canvas.drawPath(thumbPath, Paint()..color = colorTween.evaluate(enableAnimation));
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomValueIndicatorShape extends SliderComponentShape {
|
||||
static const double _indicatorSize = 4.0;
|
||||
static const double _disabledIndicatorSize = 3.0;
|
||||
static const double _slideUpHeight = 40.0;
|
||||
|
||||
@override
|
||||
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
|
||||
return Size.fromRadius(isEnabled ? _indicatorSize : _disabledIndicatorSize);
|
||||
}
|
||||
|
||||
static final Animatable<double> sizeTween = Tween<double>(
|
||||
begin: _disabledIndicatorSize,
|
||||
end: _indicatorSize,
|
||||
);
|
||||
|
||||
@override
|
||||
void paint(
|
||||
PaintingContext context,
|
||||
Offset thumbCenter, {
|
||||
Animation<double> activationAnimation,
|
||||
Animation<double> enableAnimation,
|
||||
bool isDiscrete,
|
||||
TextPainter labelPainter,
|
||||
RenderBox parentBox,
|
||||
SliderThemeData sliderTheme,
|
||||
TextDirection textDirection,
|
||||
double value,
|
||||
}) {
|
||||
final Canvas canvas = context.canvas;
|
||||
final ColorTween enableColor = ColorTween(
|
||||
begin: sliderTheme.disabledThumbColor,
|
||||
end: sliderTheme.valueIndicatorColor,
|
||||
);
|
||||
final Tween<double> slideUpTween = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: _slideUpHeight,
|
||||
);
|
||||
final double size = _indicatorSize * sizeTween.evaluate(enableAnimation);
|
||||
final Offset slideUpOffset = Offset(0.0, -slideUpTween.evaluate(activationAnimation));
|
||||
final Path thumbPath = _upTriangle(size, thumbCenter + slideUpOffset);
|
||||
final Color paintColor = enableColor.evaluate(enableAnimation).withAlpha((255.0 * activationAnimation.value).round());
|
||||
canvas.drawPath(
|
||||
thumbPath,
|
||||
Paint()..color = paintColor,
|
||||
);
|
||||
canvas.drawLine(
|
||||
thumbCenter,
|
||||
thumbCenter + slideUpOffset,
|
||||
Paint()
|
||||
..color = paintColor
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2.0);
|
||||
labelPainter.paint(canvas, thumbCenter + slideUpOffset + Offset(-labelPainter.width / 2.0, -labelPainter.height - 4.0));
|
||||
}
|
||||
}
|
||||
|
||||
class _SliderDemoState extends State<SliderDemo> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<ComponentDemoTabData> demos = <ComponentDemoTabData>[
|
||||
ComponentDemoTabData(
|
||||
tabName: 'SINGLE',
|
||||
description: 'Sliders containing 1 thumb',
|
||||
demoWidget: _Sliders(),
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/Slider-class.html',
|
||||
),
|
||||
ComponentDemoTabData(
|
||||
tabName: 'RANGE',
|
||||
description: 'Sliders containing 2 thumbs',
|
||||
demoWidget: _RangeSliders(),
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/RangeSlider-class.html',
|
||||
),
|
||||
];
|
||||
|
||||
return TabbedComponentDemoScaffold(
|
||||
title: 'Sliders',
|
||||
demos: demos,
|
||||
isScrollable: false,
|
||||
showExampleCodeAction: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Sliders extends StatefulWidget {
|
||||
@override
|
||||
_SlidersState createState() => _SlidersState();
|
||||
}
|
||||
|
||||
class _SlidersState extends State<_Sliders> {
|
||||
double _continuousValue = 25.0;
|
||||
double _discreteValue = 20.0;
|
||||
double _discreteCustomValue = 25.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Semantics(
|
||||
label: 'Editable numerical value',
|
||||
child: SizedBox(
|
||||
width: 64,
|
||||
height: 48,
|
||||
child: TextField(
|
||||
textAlign: TextAlign.center,
|
||||
onSubmitted: (String value) {
|
||||
final double newValue = double.tryParse(value);
|
||||
if (newValue != null && newValue != _continuousValue) {
|
||||
setState(() {
|
||||
_continuousValue = newValue.clamp(0, 100);
|
||||
});
|
||||
}
|
||||
},
|
||||
keyboardType: TextInputType.number,
|
||||
controller: TextEditingController(
|
||||
text: _continuousValue.toStringAsFixed(0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Slider.adaptive(
|
||||
value: _continuousValue,
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
_continuousValue = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Text('Continuous with Editable Numerical Value'),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const <Widget>[
|
||||
Slider.adaptive(value: 0.25, onChanged: null),
|
||||
Text('Disabled'),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Slider.adaptive(
|
||||
value: _discreteValue,
|
||||
min: 0.0,
|
||||
max: 200.0,
|
||||
divisions: 5,
|
||||
label: '${_discreteValue.round()}',
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
_discreteValue = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Text('Discrete'),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
SliderTheme(
|
||||
data: theme.sliderTheme.copyWith(
|
||||
activeTrackColor: Colors.deepPurple,
|
||||
inactiveTrackColor: theme.colorScheme.onSurface.withOpacity(0.5),
|
||||
activeTickMarkColor: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||
inactiveTickMarkColor: theme.colorScheme.surface.withOpacity(0.7),
|
||||
overlayColor: theme.colorScheme.onSurface.withOpacity(0.12),
|
||||
thumbColor: Colors.deepPurple,
|
||||
valueIndicatorColor: Colors.deepPurpleAccent,
|
||||
thumbShape: _CustomThumbShape(),
|
||||
valueIndicatorShape: _CustomValueIndicatorShape(),
|
||||
valueIndicatorTextStyle: theme.accentTextTheme.body2.copyWith(color: theme.colorScheme.onSurface),
|
||||
),
|
||||
child: Slider(
|
||||
value: _discreteCustomValue,
|
||||
min: 0.0,
|
||||
max: 200.0,
|
||||
divisions: 5,
|
||||
semanticFormatterCallback: (double value) => value.round().toString(),
|
||||
label: '${_discreteCustomValue.round()}',
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
_discreteCustomValue = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const Text('Discrete with Custom Theme'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RangeSliders extends StatefulWidget {
|
||||
@override
|
||||
_RangeSlidersState createState() => _RangeSlidersState();
|
||||
}
|
||||
|
||||
class _RangeSlidersState extends State<_RangeSliders> {
|
||||
RangeValues _continuousValues = const RangeValues(25.0, 75.0);
|
||||
RangeValues _discreteValues = const RangeValues(40.0, 120.0);
|
||||
RangeValues _discreteCustomValues = const RangeValues(40.0, 160.0);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
RangeSlider(
|
||||
values: _continuousValues,
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
onChanged: (RangeValues values) {
|
||||
setState(() {
|
||||
_continuousValues = values;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Text('Continuous'),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
RangeSlider(values: const RangeValues(0.25, 0.75), onChanged: null),
|
||||
const Text('Disabled'),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
RangeSlider(
|
||||
values: _discreteValues,
|
||||
min: 0.0,
|
||||
max: 200.0,
|
||||
divisions: 5,
|
||||
labels: RangeLabels('${_discreteValues.start.round()}', '${_discreteValues.end.round()}'),
|
||||
onChanged: (RangeValues values) {
|
||||
setState(() {
|
||||
_discreteValues = values;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Text('Discrete'),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
SliderTheme(
|
||||
data: SliderThemeData(
|
||||
activeTrackColor: Colors.deepPurple,
|
||||
inactiveTrackColor: Colors.black26,
|
||||
activeTickMarkColor: Colors.white70,
|
||||
inactiveTickMarkColor: Colors.black,
|
||||
overlayColor: Colors.black12,
|
||||
thumbColor: Colors.deepPurple,
|
||||
rangeThumbShape: _CustomRangeThumbShape(),
|
||||
showValueIndicator: ShowValueIndicator.never,
|
||||
),
|
||||
child: RangeSlider(
|
||||
values: _discreteCustomValues,
|
||||
min: 0.0,
|
||||
max: 200.0,
|
||||
divisions: 5,
|
||||
labels: RangeLabels('${_discreteCustomValues.start.round()}', '${_discreteCustomValues.end.round()}'),
|
||||
onChanged: (RangeValues values) {
|
||||
setState(() {
|
||||
_discreteCustomValues = values;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const Text('Discrete with Custom Theme'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,95 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
const String _text1 =
|
||||
'Snackbars provide lightweight feedback about an operation by '
|
||||
'showing a brief message at the bottom of the screen. Snackbars '
|
||||
'can contain an action.';
|
||||
|
||||
const String _text2 =
|
||||
'Snackbars should contain a single line of text directly related '
|
||||
'to the operation performed. They cannot contain icons.';
|
||||
|
||||
const String _text3 =
|
||||
'By default snackbars automatically disappear after a few seconds ';
|
||||
|
||||
class SnackBarDemo extends StatefulWidget {
|
||||
const SnackBarDemo({ Key key }) : super(key: key);
|
||||
|
||||
static const String routeName = '/material/snack-bar';
|
||||
|
||||
@override
|
||||
_SnackBarDemoState createState() => _SnackBarDemoState();
|
||||
}
|
||||
|
||||
class _SnackBarDemoState extends State<SnackBarDemo> {
|
||||
int _snackBarIndex = 1;
|
||||
|
||||
Widget buildBody(BuildContext context) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
children: <Widget>[
|
||||
const Text(_text1),
|
||||
const Text(_text2),
|
||||
Center(
|
||||
child: RaisedButton(
|
||||
child: const Text('SHOW A SNACKBAR'),
|
||||
onPressed: () {
|
||||
final int thisSnackBarIndex = _snackBarIndex++;
|
||||
Scaffold.of(context).showSnackBar(SnackBar(
|
||||
content: Text('This is snackbar #$thisSnackBarIndex.'),
|
||||
action: SnackBarAction(
|
||||
label: 'ACTION',
|
||||
onPressed: () {
|
||||
Scaffold.of(context).showSnackBar(SnackBar(
|
||||
content: Text('You pressed snackbar $thisSnackBarIndex\'s action.'),
|
||||
));
|
||||
},
|
||||
),
|
||||
));
|
||||
},
|
||||
),
|
||||
),
|
||||
const Text(_text3),
|
||||
]
|
||||
.map<Widget>((Widget child) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: child,
|
||||
);
|
||||
})
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Snackbar'),
|
||||
actions: <Widget>[MaterialDemoDocumentationButton(SnackBarDemo.routeName)],
|
||||
),
|
||||
body: Builder(
|
||||
// Create an inner BuildContext so that the snackBar onPressed methods
|
||||
// can refer to the Scaffold with Scaffold.of().
|
||||
builder: buildBody
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.add),
|
||||
tooltip: 'Create',
|
||||
onPressed: () {
|
||||
print('Floating Action Button was pressed');
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,216 +0,0 @@
|
||||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
// Each TabBarView contains a _Page and for each _Page there is a list
|
||||
// of _CardData objects. Each _CardData object is displayed by a _CardItem.
|
||||
|
||||
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
|
||||
|
||||
class _Page {
|
||||
_Page({ this.label });
|
||||
final String label;
|
||||
String get id => label[0];
|
||||
@override
|
||||
String toString() => '$runtimeType("$label")';
|
||||
}
|
||||
|
||||
class _CardData {
|
||||
const _CardData({ this.title, this.imageAsset, this.imageAssetPackage });
|
||||
final String title;
|
||||
final String imageAsset;
|
||||
final String imageAssetPackage;
|
||||
}
|
||||
|
||||
final Map<_Page, List<_CardData>> _allPages = <_Page, List<_CardData>>{
|
||||
_Page(label: 'HOME'): <_CardData>[
|
||||
const _CardData(
|
||||
title: 'Flatwear',
|
||||
imageAsset: 'products/flatwear.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
),
|
||||
const _CardData(
|
||||
title: 'Pine Table',
|
||||
imageAsset: 'products/table.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
),
|
||||
const _CardData(
|
||||
title: 'Blue Cup',
|
||||
imageAsset: 'products/cup.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
),
|
||||
const _CardData(
|
||||
title: 'Tea Set',
|
||||
imageAsset: 'products/teaset.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
),
|
||||
const _CardData(
|
||||
title: 'Desk Set',
|
||||
imageAsset: 'products/deskset.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
),
|
||||
const _CardData(
|
||||
title: 'Blue Linen Napkins',
|
||||
imageAsset: 'products/napkins.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
),
|
||||
const _CardData(
|
||||
title: 'Planters',
|
||||
imageAsset: 'products/planters.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
),
|
||||
const _CardData(
|
||||
title: 'Kitchen Quattro',
|
||||
imageAsset: 'products/kitchen_quattro.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
),
|
||||
const _CardData(
|
||||
title: 'Platter',
|
||||
imageAsset: 'products/platter.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
),
|
||||
],
|
||||
_Page(label: 'APPAREL'): <_CardData>[
|
||||
const _CardData(
|
||||
title: 'Cloud-White Dress',
|
||||
imageAsset: 'products/dress.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
),
|
||||
const _CardData(
|
||||
title: 'Ginger Scarf',
|
||||
imageAsset: 'products/scarf.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
),
|
||||
const _CardData(
|
||||
title: 'Blush Sweats',
|
||||
imageAsset: 'products/sweats.png',
|
||||
imageAssetPackage: _kGalleryAssetsPackage,
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
class _CardDataItem extends StatelessWidget {
|
||||
const _CardDataItem({ this.page, this.data });
|
||||
|
||||
static const double height = 272.0;
|
||||
final _Page page;
|
||||
final _CardData data;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Align(
|
||||
alignment: page.id == 'H'
|
||||
? Alignment.centerLeft
|
||||
: Alignment.centerRight,
|
||||
child: CircleAvatar(child: Text('${page.id}')),
|
||||
),
|
||||
SizedBox(
|
||||
width: 144.0,
|
||||
height: 144.0,
|
||||
child: Image.asset(
|
||||
data.imageAsset,
|
||||
package: data.imageAssetPackage,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
data.title,
|
||||
style: Theme.of(context).textTheme.title,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TabsDemo extends StatelessWidget {
|
||||
static const String routeName = '/material/tabs';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: _allPages.length,
|
||||
child: Scaffold(
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
return <Widget>[
|
||||
SliverOverlapAbsorber(
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
child: SliverAppBar(
|
||||
title: const Text('Tabs and scrolling'),
|
||||
actions: <Widget>[MaterialDemoDocumentationButton(routeName)],
|
||||
pinned: true,
|
||||
expandedHeight: 150.0,
|
||||
forceElevated: innerBoxIsScrolled,
|
||||
bottom: TabBar(
|
||||
tabs: _allPages.keys.map<Widget>(
|
||||
(_Page page) => Tab(text: page.label),
|
||||
).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
body: TabBarView(
|
||||
children: _allPages.keys.map<Widget>((_Page page) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return CustomScrollView(
|
||||
key: PageStorageKey<_Page>(page),
|
||||
slivers: <Widget>[
|
||||
SliverOverlapInjector(
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
horizontal: 16.0,
|
||||
),
|
||||
sliver: SliverFixedExtentList(
|
||||
itemExtent: _CardDataItem.height,
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
final _CardData data = _allPages[page][index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: _CardDataItem(
|
||||
page: page,
|
||||
data: data,
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: _allPages[page].length,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,160 +0,0 @@
|
||||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
const String _explanatoryText =
|
||||
"When the Scaffold's floating action button changes, the new button fades and "
|
||||
'turns into view. In this demo, changing tabs can cause the app to be rebuilt '
|
||||
'with a FloatingActionButton that the Scaffold distinguishes from the others '
|
||||
'by its key.';
|
||||
|
||||
class _Page {
|
||||
_Page({ this.label, this.colors, this.icon });
|
||||
|
||||
final String label;
|
||||
final MaterialColor colors;
|
||||
final IconData icon;
|
||||
|
||||
Color get labelColor => colors != null ? colors.shade300 : Colors.grey.shade300;
|
||||
bool get fabDefined => colors != null && icon != null;
|
||||
Color get fabColor => colors.shade400;
|
||||
Icon get fabIcon => Icon(icon);
|
||||
Key get fabKey => ValueKey<Color>(fabColor);
|
||||
}
|
||||
|
||||
final List<_Page> _allPages = <_Page>[
|
||||
_Page(label: 'Blue', colors: Colors.indigo, icon: Icons.add),
|
||||
_Page(label: 'Eco', colors: Colors.green, icon: Icons.create),
|
||||
_Page(label: 'No'),
|
||||
_Page(label: 'Teal', colors: Colors.teal, icon: Icons.add),
|
||||
_Page(label: 'Red', colors: Colors.red, icon: Icons.create),
|
||||
];
|
||||
|
||||
class TabsFabDemo extends StatefulWidget {
|
||||
static const String routeName = '/material/tabs-fab';
|
||||
|
||||
@override
|
||||
_TabsFabDemoState createState() => _TabsFabDemoState();
|
||||
}
|
||||
|
||||
class _TabsFabDemoState extends State<TabsFabDemo> with SingleTickerProviderStateMixin {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
TabController _controller;
|
||||
_Page _selectedPage;
|
||||
bool _extendedButtons = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TabController(vsync: this, length: _allPages.length);
|
||||
_controller.addListener(_handleTabSelection);
|
||||
_selectedPage = _allPages[0];
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleTabSelection() {
|
||||
setState(() {
|
||||
_selectedPage = _allPages[_controller.index];
|
||||
});
|
||||
}
|
||||
|
||||
void _showExplanatoryText() {
|
||||
_scaffoldKey.currentState.showBottomSheet<void>((BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(top: BorderSide(color: Theme.of(context).dividerColor))
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Text(_explanatoryText, style: Theme.of(context).textTheme.subhead),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget buildTabView(_Page page) {
|
||||
return Builder(
|
||||
builder: (BuildContext context) {
|
||||
return Container(
|
||||
key: ValueKey<String>(page.label),
|
||||
padding: const EdgeInsets.fromLTRB(48.0, 48.0, 48.0, 96.0),
|
||||
child: Card(
|
||||
child: Center(
|
||||
child: Text(page.label,
|
||||
style: TextStyle(
|
||||
color: page.labelColor,
|
||||
fontSize: 32.0,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildFloatingActionButton(_Page page) {
|
||||
if (!page.fabDefined)
|
||||
return null;
|
||||
|
||||
if (_extendedButtons) {
|
||||
return FloatingActionButton.extended(
|
||||
key: ValueKey<Key>(page.fabKey),
|
||||
tooltip: 'Show explanation',
|
||||
backgroundColor: page.fabColor,
|
||||
icon: page.fabIcon,
|
||||
label: Text(page.label.toUpperCase()),
|
||||
onPressed: _showExplanatoryText,
|
||||
);
|
||||
}
|
||||
|
||||
return FloatingActionButton(
|
||||
key: page.fabKey,
|
||||
tooltip: 'Show explanation',
|
||||
backgroundColor: page.fabColor,
|
||||
child: page.fabIcon,
|
||||
onPressed: _showExplanatoryText,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: const Text('FAB per tab'),
|
||||
bottom: TabBar(
|
||||
controller: _controller,
|
||||
tabs: _allPages.map<Widget>((_Page page) => Tab(text: page.label.toUpperCase())).toList(),
|
||||
),
|
||||
actions: <Widget>[
|
||||
MaterialDemoDocumentationButton(TabsFabDemo.routeName),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.sentiment_very_satisfied, semanticLabel: 'Toggle extended buttons'),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_extendedButtons = !_extendedButtons;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: buildFloatingActionButton(_selectedPage),
|
||||
body: TabBarView(
|
||||
controller: _controller,
|
||||
children: _allPages.map<Widget>(buildTabView).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,344 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
class TextFormFieldDemo extends StatefulWidget {
|
||||
const TextFormFieldDemo({ Key key }) : super(key: key);
|
||||
|
||||
static const String routeName = '/material/text-form-field';
|
||||
|
||||
@override
|
||||
TextFormFieldDemoState createState() => TextFormFieldDemoState();
|
||||
}
|
||||
|
||||
class PersonData {
|
||||
String name = '';
|
||||
String phoneNumber = '';
|
||||
String email = '';
|
||||
String password = '';
|
||||
}
|
||||
|
||||
class PasswordField extends StatefulWidget {
|
||||
const PasswordField({
|
||||
this.fieldKey,
|
||||
this.hintText,
|
||||
this.labelText,
|
||||
this.helperText,
|
||||
this.onSaved,
|
||||
this.validator,
|
||||
this.onFieldSubmitted,
|
||||
});
|
||||
|
||||
final Key fieldKey;
|
||||
final String hintText;
|
||||
final String labelText;
|
||||
final String helperText;
|
||||
final FormFieldSetter<String> onSaved;
|
||||
final FormFieldValidator<String> validator;
|
||||
final ValueChanged<String> onFieldSubmitted;
|
||||
|
||||
@override
|
||||
_PasswordFieldState createState() => _PasswordFieldState();
|
||||
}
|
||||
|
||||
class _PasswordFieldState extends State<PasswordField> {
|
||||
bool _obscureText = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
key: widget.fieldKey,
|
||||
obscureText: _obscureText,
|
||||
maxLength: 8,
|
||||
onSaved: widget.onSaved,
|
||||
validator: widget.validator,
|
||||
onFieldSubmitted: widget.onFieldSubmitted,
|
||||
decoration: InputDecoration(
|
||||
border: const UnderlineInputBorder(),
|
||||
filled: true,
|
||||
hintText: widget.hintText,
|
||||
labelText: widget.labelText,
|
||||
helperText: widget.helperText,
|
||||
suffixIcon: GestureDetector(
|
||||
dragStartBehavior: DragStartBehavior.down,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_obscureText = !_obscureText;
|
||||
});
|
||||
},
|
||||
child: Icon(
|
||||
_obscureText ? Icons.visibility : Icons.visibility_off,
|
||||
semanticLabel: _obscureText ? 'show password' : 'hide password',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TextFormFieldDemoState extends State<TextFormFieldDemo> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
PersonData person = PersonData();
|
||||
|
||||
void showInSnackBar(String value) {
|
||||
_scaffoldKey.currentState.showSnackBar(SnackBar(
|
||||
content: Text(value),
|
||||
));
|
||||
}
|
||||
|
||||
bool _autovalidate = false;
|
||||
bool _formWasEdited = false;
|
||||
|
||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||
final GlobalKey<FormFieldState<String>> _passwordFieldKey = GlobalKey<FormFieldState<String>>();
|
||||
final _UsNumberTextInputFormatter _phoneNumberFormatter = _UsNumberTextInputFormatter();
|
||||
void _handleSubmitted() {
|
||||
final FormState form = _formKey.currentState;
|
||||
if (!form.validate()) {
|
||||
_autovalidate = true; // Start validating on every change.
|
||||
showInSnackBar('Please fix the errors in red before submitting.');
|
||||
} else {
|
||||
form.save();
|
||||
showInSnackBar('${person.name}\'s phone number is ${person.phoneNumber}');
|
||||
}
|
||||
}
|
||||
|
||||
String _validateName(String value) {
|
||||
_formWasEdited = true;
|
||||
if (value.isEmpty)
|
||||
return 'Name is required.';
|
||||
final RegExp nameExp = RegExp(r'^[A-Za-z ]+$');
|
||||
if (!nameExp.hasMatch(value))
|
||||
return 'Please enter only alphabetical characters.';
|
||||
return null;
|
||||
}
|
||||
|
||||
String _validatePhoneNumber(String value) {
|
||||
_formWasEdited = true;
|
||||
final RegExp phoneExp = RegExp(r'^\(\d\d\d\) \d\d\d\-\d\d\d\d$');
|
||||
if (!phoneExp.hasMatch(value))
|
||||
return '(###) ###-#### - Enter a US phone number.';
|
||||
return null;
|
||||
}
|
||||
|
||||
String _validatePassword(String value) {
|
||||
_formWasEdited = true;
|
||||
final FormFieldState<String> passwordField = _passwordFieldKey.currentState;
|
||||
if (passwordField.value == null || passwordField.value.isEmpty)
|
||||
return 'Please enter a password.';
|
||||
if (passwordField.value != value)
|
||||
return 'The passwords don\'t match';
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<bool> _warnUserAboutInvalidData() async {
|
||||
final FormState form = _formKey.currentState;
|
||||
if (form == null || !_formWasEdited || form.validate())
|
||||
return true;
|
||||
|
||||
return await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('This form has errors'),
|
||||
content: const Text('Really leave this form?'),
|
||||
actions: <Widget> [
|
||||
FlatButton(
|
||||
child: const Text('YES'),
|
||||
onPressed: () { Navigator.of(context).pop(true); },
|
||||
),
|
||||
FlatButton(
|
||||
child: const Text('NO'),
|
||||
onPressed: () { Navigator.of(context).pop(false); },
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
) ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
drawerDragStartBehavior: DragStartBehavior.down,
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: const Text('Text fields'),
|
||||
actions: <Widget>[MaterialDemoDocumentationButton(TextFormFieldDemo.routeName)],
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
autovalidate: _autovalidate,
|
||||
onWillPop: _warnUserAboutInvalidData,
|
||||
child: Scrollbar(
|
||||
child: SingleChildScrollView(
|
||||
dragStartBehavior: DragStartBehavior.down,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
const SizedBox(height: 24.0),
|
||||
TextFormField(
|
||||
textCapitalization: TextCapitalization.words,
|
||||
decoration: const InputDecoration(
|
||||
border: UnderlineInputBorder(),
|
||||
filled: true,
|
||||
icon: Icon(Icons.person),
|
||||
hintText: 'What do people call you?',
|
||||
labelText: 'Name *',
|
||||
),
|
||||
onSaved: (String value) { person.name = value; },
|
||||
validator: _validateName,
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
border: UnderlineInputBorder(),
|
||||
filled: true,
|
||||
icon: Icon(Icons.phone),
|
||||
hintText: 'Where can we reach you?',
|
||||
labelText: 'Phone Number *',
|
||||
prefixText: '+1',
|
||||
),
|
||||
keyboardType: TextInputType.phone,
|
||||
onSaved: (String value) { person.phoneNumber = value; },
|
||||
validator: _validatePhoneNumber,
|
||||
// TextInputFormatters are applied in sequence.
|
||||
inputFormatters: <TextInputFormatter> [
|
||||
WhitelistingTextInputFormatter.digitsOnly,
|
||||
// Fit the validating format.
|
||||
_phoneNumberFormatter,
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
border: UnderlineInputBorder(),
|
||||
filled: true,
|
||||
icon: Icon(Icons.email),
|
||||
hintText: 'Your email address',
|
||||
labelText: 'E-mail',
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
onSaved: (String value) { person.email = value; },
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
hintText: 'Tell us about yourself (e.g., write down what you do or what hobbies you have)',
|
||||
helperText: 'Keep it short, this is just a demo.',
|
||||
labelText: 'Life story',
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
TextFormField(
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Salary',
|
||||
prefixText: '\$',
|
||||
suffixText: 'USD',
|
||||
suffixStyle: TextStyle(color: Colors.green),
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
PasswordField(
|
||||
fieldKey: _passwordFieldKey,
|
||||
helperText: 'No more than 8 characters.',
|
||||
labelText: 'Password *',
|
||||
onFieldSubmitted: (String value) {
|
||||
setState(() {
|
||||
person.password = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
TextFormField(
|
||||
enabled: person.password != null && person.password.isNotEmpty,
|
||||
decoration: const InputDecoration(
|
||||
border: UnderlineInputBorder(),
|
||||
filled: true,
|
||||
labelText: 'Re-type password',
|
||||
),
|
||||
maxLength: 8,
|
||||
obscureText: true,
|
||||
validator: _validatePassword,
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
Center(
|
||||
child: RaisedButton(
|
||||
child: const Text('SUBMIT'),
|
||||
onPressed: _handleSubmitted,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
Text(
|
||||
'* indicates required field',
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Format incoming numeric text to fit the format of (###) ###-#### ##...
|
||||
class _UsNumberTextInputFormatter extends TextInputFormatter {
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue,
|
||||
TextEditingValue newValue,
|
||||
) {
|
||||
final int newTextLength = newValue.text.length;
|
||||
int selectionIndex = newValue.selection.end;
|
||||
int usedSubstringIndex = 0;
|
||||
final StringBuffer newText = StringBuffer();
|
||||
if (newTextLength >= 1) {
|
||||
newText.write('(');
|
||||
if (newValue.selection.end >= 1)
|
||||
selectionIndex++;
|
||||
}
|
||||
if (newTextLength >= 4) {
|
||||
newText.write(newValue.text.substring(0, usedSubstringIndex = 3) + ') ');
|
||||
if (newValue.selection.end >= 3)
|
||||
selectionIndex += 2;
|
||||
}
|
||||
if (newTextLength >= 7) {
|
||||
newText.write(newValue.text.substring(3, usedSubstringIndex = 6) + '-');
|
||||
if (newValue.selection.end >= 6)
|
||||
selectionIndex++;
|
||||
}
|
||||
if (newTextLength >= 11) {
|
||||
newText.write(newValue.text.substring(6, usedSubstringIndex = 10) + ' ');
|
||||
if (newValue.selection.end >= 10)
|
||||
selectionIndex++;
|
||||
}
|
||||
// Dump the rest.
|
||||
if (newTextLength >= usedSubstringIndex)
|
||||
newText.write(newValue.text.substring(usedSubstringIndex));
|
||||
return TextEditingValue(
|
||||
text: newText.toString(),
|
||||
selection: TextSelection.collapsed(offset: selectionIndex),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
|
||||
const String _introText =
|
||||
'Tooltips are short identifying messages that briefly appear in response to '
|
||||
'a long press. Tooltip messages are also used by services that make Flutter '
|
||||
'apps accessible, like screen readers.';
|
||||
|
||||
class TooltipDemo extends StatelessWidget {
|
||||
|
||||
static const String routeName = '/material/tooltips';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Tooltips'),
|
||||
actions: <Widget>[MaterialDemoDocumentationButton(routeName)],
|
||||
),
|
||||
body: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
Text(_introText, style: theme.textTheme.subhead),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text('Long press the ', style: theme.textTheme.subhead),
|
||||
Tooltip(
|
||||
message: 'call icon',
|
||||
child: Icon(
|
||||
Icons.call,
|
||||
size: 18.0,
|
||||
color: theme.iconTheme.color,
|
||||
),
|
||||
),
|
||||
Text(' icon.', style: theme.textTheme.subhead),
|
||||
],
|
||||
),
|
||||
Center(
|
||||
child: IconButton(
|
||||
iconSize: 48.0,
|
||||
icon: const Icon(Icons.call),
|
||||
color: theme.iconTheme.color,
|
||||
tooltip: 'Place a phone call',
|
||||
onPressed: () {
|
||||
Scaffold.of(context).showSnackBar(const SnackBar(
|
||||
content: Text('That was an ordinary tap.'),
|
||||
));
|
||||
},
|
||||
),
|
||||
),
|
||||
]
|
||||
.map<Widget>((Widget widget) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0, left: 16.0, right: 16.0),
|
||||
child: widget,
|
||||
);
|
||||
})
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,715 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class PestoDemo extends StatelessWidget {
|
||||
const PestoDemo({ Key key }) : super(key: key);
|
||||
|
||||
static const String routeName = '/pesto';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => PestoHome();
|
||||
}
|
||||
|
||||
|
||||
const String _kSmallLogoImage = 'logos/pesto/logo_small.png';
|
||||
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
|
||||
const double _kAppBarHeight = 128.0;
|
||||
const double _kFabHalfSize = 28.0; // TODO(mpcomplete): needs to adapt to screen size
|
||||
const double _kRecipePageMaxWidth = 500.0;
|
||||
|
||||
final Set<Recipe> _favoriteRecipes = <Recipe>{};
|
||||
|
||||
final ThemeData _kTheme = ThemeData(
|
||||
brightness: Brightness.light,
|
||||
primarySwatch: Colors.teal,
|
||||
accentColor: Colors.redAccent,
|
||||
);
|
||||
|
||||
class PestoHome extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const RecipeGridPage(recipes: kPestoRecipes);
|
||||
}
|
||||
}
|
||||
|
||||
class PestoFavorites extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RecipeGridPage(recipes: _favoriteRecipes.toList());
|
||||
}
|
||||
}
|
||||
|
||||
class PestoStyle extends TextStyle {
|
||||
const PestoStyle({
|
||||
double fontSize = 12.0,
|
||||
FontWeight fontWeight,
|
||||
Color color = Colors.black87,
|
||||
double letterSpacing,
|
||||
double height,
|
||||
}) : super(
|
||||
inherit: false,
|
||||
color: color,
|
||||
fontFamily: 'Raleway',
|
||||
fontSize: fontSize,
|
||||
fontWeight: fontWeight,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
letterSpacing: letterSpacing,
|
||||
height: height,
|
||||
);
|
||||
}
|
||||
|
||||
// Displays a grid of recipe cards.
|
||||
class RecipeGridPage extends StatefulWidget {
|
||||
const RecipeGridPage({ Key key, this.recipes }) : super(key: key);
|
||||
|
||||
final List<Recipe> recipes;
|
||||
|
||||
@override
|
||||
_RecipeGridPageState createState() => _RecipeGridPageState();
|
||||
}
|
||||
|
||||
class _RecipeGridPageState extends State<RecipeGridPage> {
|
||||
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double statusBarHeight = MediaQuery.of(context).padding.top;
|
||||
return Theme(
|
||||
data: _kTheme.copyWith(platform: Theme.of(context).platform),
|
||||
child: Scaffold(
|
||||
key: scaffoldKey,
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.edit),
|
||||
onPressed: () {
|
||||
scaffoldKey.currentState.showSnackBar(const SnackBar(
|
||||
content: Text('Not supported.'),
|
||||
));
|
||||
},
|
||||
),
|
||||
body: CustomScrollView(
|
||||
semanticChildCount: widget.recipes.length,
|
||||
slivers: <Widget>[
|
||||
_buildAppBar(context, statusBarHeight),
|
||||
_buildBody(context, statusBarHeight),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAppBar(BuildContext context, double statusBarHeight) {
|
||||
return SliverAppBar(
|
||||
pinned: true,
|
||||
expandedHeight: _kAppBarHeight,
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search),
|
||||
tooltip: 'Search',
|
||||
onPressed: () {
|
||||
scaffoldKey.currentState.showSnackBar(const SnackBar(
|
||||
content: Text('Not supported.'),
|
||||
));
|
||||
},
|
||||
),
|
||||
],
|
||||
flexibleSpace: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
final Size size = constraints.biggest;
|
||||
final double appBarHeight = size.height - statusBarHeight;
|
||||
final double t = (appBarHeight - kToolbarHeight) / (_kAppBarHeight - kToolbarHeight);
|
||||
final double extraPadding = Tween<double>(begin: 10.0, end: 24.0).transform(t);
|
||||
final double logoHeight = appBarHeight - 1.5 * extraPadding;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: statusBarHeight + 0.5 * extraPadding,
|
||||
bottom: extraPadding,
|
||||
),
|
||||
child: Center(
|
||||
child: PestoLogo(height: logoHeight, t: t.clamp(0.0, 1.0)),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(BuildContext context, double statusBarHeight) {
|
||||
final EdgeInsets mediaPadding = MediaQuery.of(context).padding;
|
||||
final EdgeInsets padding = EdgeInsets.only(
|
||||
top: 8.0,
|
||||
left: 8.0 + mediaPadding.left,
|
||||
right: 8.0 + mediaPadding.right,
|
||||
bottom: 8.0,
|
||||
);
|
||||
return SliverPadding(
|
||||
padding: padding,
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: _kRecipePageMaxWidth,
|
||||
crossAxisSpacing: 8.0,
|
||||
mainAxisSpacing: 8.0,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
final Recipe recipe = widget.recipes[index];
|
||||
return RecipeCard(
|
||||
recipe: recipe,
|
||||
onTap: () { showRecipePage(context, recipe); },
|
||||
);
|
||||
},
|
||||
childCount: widget.recipes.length,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void showFavoritesPage(BuildContext context) {
|
||||
Navigator.push(context, MaterialPageRoute<void>(
|
||||
settings: const RouteSettings(name: '/pesto/favorites'),
|
||||
builder: (BuildContext context) => PestoFavorites(),
|
||||
));
|
||||
}
|
||||
|
||||
void showRecipePage(BuildContext context, Recipe recipe) {
|
||||
Navigator.push(context, MaterialPageRoute<void>(
|
||||
settings: const RouteSettings(name: '/pesto/recipe'),
|
||||
builder: (BuildContext context) {
|
||||
return Theme(
|
||||
data: _kTheme.copyWith(platform: Theme.of(context).platform),
|
||||
child: RecipePage(recipe: recipe),
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class PestoLogo extends StatefulWidget {
|
||||
const PestoLogo({this.height, this.t});
|
||||
|
||||
final double height;
|
||||
final double t;
|
||||
|
||||
@override
|
||||
_PestoLogoState createState() => _PestoLogoState();
|
||||
}
|
||||
|
||||
class _PestoLogoState extends State<PestoLogo> {
|
||||
// Native sizes for logo and its image/text components.
|
||||
static const double kLogoHeight = 162.0;
|
||||
static const double kLogoWidth = 220.0;
|
||||
static const double kImageHeight = 108.0;
|
||||
static const double kTextHeight = 48.0;
|
||||
final TextStyle titleStyle = const PestoStyle(fontSize: kTextHeight, fontWeight: FontWeight.w900, color: Colors.white, letterSpacing: 3.0);
|
||||
final RectTween _textRectTween = RectTween(
|
||||
begin: const Rect.fromLTWH(0.0, kLogoHeight, kLogoWidth, kTextHeight),
|
||||
end: const Rect.fromLTWH(0.0, kImageHeight, kLogoWidth, kTextHeight),
|
||||
);
|
||||
final Curve _textOpacity = const Interval(0.4, 1.0, curve: Curves.easeInOut);
|
||||
final RectTween _imageRectTween = RectTween(
|
||||
begin: const Rect.fromLTWH(0.0, 0.0, kLogoWidth, kLogoHeight),
|
||||
end: const Rect.fromLTWH(0.0, 0.0, kLogoWidth, kImageHeight),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Semantics(
|
||||
namesRoute: true,
|
||||
child: Transform(
|
||||
transform: Matrix4.identity()..scale(widget.height / kLogoHeight),
|
||||
alignment: Alignment.topCenter,
|
||||
child: SizedBox(
|
||||
width: kLogoWidth,
|
||||
child: Stack(
|
||||
overflow: Overflow.visible,
|
||||
children: <Widget>[
|
||||
Positioned.fromRect(
|
||||
rect: _imageRectTween.lerp(widget.t),
|
||||
child: Image.asset(
|
||||
_kSmallLogoImage,
|
||||
package: _kGalleryAssetsPackage,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
Positioned.fromRect(
|
||||
rect: _textRectTween.lerp(widget.t),
|
||||
child: Opacity(
|
||||
opacity: _textOpacity.transform(widget.t),
|
||||
child: Text('PESTO', style: titleStyle, textAlign: TextAlign.center),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// A card with the recipe's image, author, and title.
|
||||
class RecipeCard extends StatelessWidget {
|
||||
const RecipeCard({ Key key, this.recipe, this.onTap }) : super(key: key);
|
||||
|
||||
final Recipe recipe;
|
||||
final VoidCallback onTap;
|
||||
|
||||
TextStyle get titleStyle => const PestoStyle(fontSize: 24.0, fontWeight: FontWeight.w600);
|
||||
TextStyle get authorStyle => const PestoStyle(fontWeight: FontWeight.w500, color: Colors.black54);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Hero(
|
||||
tag: 'packages/$_kGalleryAssetsPackage/${recipe.imagePath}',
|
||||
child: AspectRatio(
|
||||
aspectRatio: 4.0 / 3.0,
|
||||
child: Image.asset(
|
||||
recipe.imagePath,
|
||||
package: recipe.imagePackage,
|
||||
fit: BoxFit.cover,
|
||||
semanticLabel: recipe.name,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Image.asset(
|
||||
recipe.ingredientsImagePath,
|
||||
package: recipe.ingredientsImagePackage,
|
||||
width: 48.0,
|
||||
height: 48.0,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(recipe.name, style: titleStyle, softWrap: false, overflow: TextOverflow.ellipsis),
|
||||
Text(recipe.author, style: authorStyle),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Displays one recipe. Includes the recipe sheet with a background image.
|
||||
class RecipePage extends StatefulWidget {
|
||||
const RecipePage({ Key key, this.recipe }) : super(key: key);
|
||||
|
||||
final Recipe recipe;
|
||||
|
||||
@override
|
||||
_RecipePageState createState() => _RecipePageState();
|
||||
}
|
||||
|
||||
class _RecipePageState extends State<RecipePage> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
final TextStyle menuItemStyle = const PestoStyle(fontSize: 15.0, color: Colors.black54, height: 24.0/15.0);
|
||||
|
||||
double _getAppBarHeight(BuildContext context) => MediaQuery.of(context).size.height * 0.3;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// The full page content with the recipe's image behind it. This
|
||||
// adjusts based on the size of the screen. If the recipe sheet touches
|
||||
// the edge of the screen, use a slightly different layout.
|
||||
final double appBarHeight = _getAppBarHeight(context);
|
||||
final Size screenSize = MediaQuery.of(context).size;
|
||||
final bool fullWidth = screenSize.width < _kRecipePageMaxWidth;
|
||||
final bool isFavorite = _favoriteRecipes.contains(widget.recipe);
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
body: Stack(
|
||||
children: <Widget>[
|
||||
Positioned(
|
||||
top: 0.0,
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
height: appBarHeight + _kFabHalfSize,
|
||||
child: Hero(
|
||||
tag: 'packages/$_kGalleryAssetsPackage/${widget.recipe.imagePath}',
|
||||
child: Image.asset(
|
||||
widget.recipe.imagePath,
|
||||
package: widget.recipe.imagePackage,
|
||||
fit: fullWidth ? BoxFit.fitWidth : BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
SliverAppBar(
|
||||
expandedHeight: appBarHeight - _kFabHalfSize,
|
||||
backgroundColor: Colors.transparent,
|
||||
actions: <Widget>[
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (String item) { },
|
||||
itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
|
||||
_buildMenuItem(Icons.share, 'Tweet recipe'),
|
||||
_buildMenuItem(Icons.email, 'Email recipe'),
|
||||
_buildMenuItem(Icons.message, 'Message recipe'),
|
||||
_buildMenuItem(Icons.people, 'Share on Facebook'),
|
||||
],
|
||||
),
|
||||
],
|
||||
flexibleSpace: const FlexibleSpaceBar(
|
||||
background: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment(0.0, -1.0),
|
||||
end: Alignment(0.0, -0.2),
|
||||
colors: <Color>[Color(0x60000000), Color(0x00000000)],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: _kFabHalfSize),
|
||||
width: fullWidth ? null : _kRecipePageMaxWidth,
|
||||
child: RecipeSheet(recipe: widget.recipe),
|
||||
),
|
||||
Positioned(
|
||||
right: 16.0,
|
||||
child: FloatingActionButton(
|
||||
child: Icon(isFavorite ? Icons.favorite : Icons.favorite_border),
|
||||
onPressed: _toggleFavorite,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
PopupMenuItem<String> _buildMenuItem(IconData icon, String label) {
|
||||
return PopupMenuItem<String>(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 24.0),
|
||||
child: Icon(icon, color: Colors.black54),
|
||||
),
|
||||
Text(label, style: menuItemStyle),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _toggleFavorite() {
|
||||
setState(() {
|
||||
if (_favoriteRecipes.contains(widget.recipe))
|
||||
_favoriteRecipes.remove(widget.recipe);
|
||||
else
|
||||
_favoriteRecipes.add(widget.recipe);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays the recipe's name and instructions.
|
||||
class RecipeSheet extends StatelessWidget {
|
||||
RecipeSheet({ Key key, this.recipe }) : super(key: key);
|
||||
|
||||
final TextStyle titleStyle = const PestoStyle(fontSize: 34.0);
|
||||
final TextStyle descriptionStyle = const PestoStyle(fontSize: 15.0, color: Colors.black54, height: 24.0/15.0);
|
||||
final TextStyle itemStyle = const PestoStyle(fontSize: 15.0, height: 24.0/15.0);
|
||||
final TextStyle itemAmountStyle = PestoStyle(fontSize: 15.0, color: _kTheme.primaryColor, height: 24.0/15.0);
|
||||
final TextStyle headingStyle = const PestoStyle(fontSize: 16.0, fontWeight: FontWeight.bold, height: 24.0/15.0);
|
||||
|
||||
final Recipe recipe;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 40.0),
|
||||
child: Table(
|
||||
columnWidths: const <int, TableColumnWidth>{
|
||||
0: FixedColumnWidth(64.0),
|
||||
},
|
||||
children: <TableRow>[
|
||||
TableRow(
|
||||
children: <Widget>[
|
||||
TableCell(
|
||||
verticalAlignment: TableCellVerticalAlignment.middle,
|
||||
child: Image.asset(
|
||||
recipe.ingredientsImagePath,
|
||||
package: recipe.ingredientsImagePackage,
|
||||
width: 32.0,
|
||||
height: 32.0,
|
||||
alignment: Alignment.centerLeft,
|
||||
fit: BoxFit.scaleDown,
|
||||
),
|
||||
),
|
||||
TableCell(
|
||||
verticalAlignment: TableCellVerticalAlignment.middle,
|
||||
child: Text(recipe.name, style: titleStyle),
|
||||
),
|
||||
]
|
||||
),
|
||||
TableRow(
|
||||
children: <Widget>[
|
||||
const SizedBox(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0, bottom: 4.0),
|
||||
child: Text(recipe.description, style: descriptionStyle),
|
||||
),
|
||||
]
|
||||
),
|
||||
TableRow(
|
||||
children: <Widget>[
|
||||
const SizedBox(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 24.0, bottom: 4.0),
|
||||
child: Text('Ingredients', style: headingStyle),
|
||||
),
|
||||
]
|
||||
),
|
||||
...recipe.ingredients.map<TableRow>((RecipeIngredient ingredient) {
|
||||
return _buildItemRow(ingredient.amount, ingredient.description);
|
||||
}),
|
||||
TableRow(
|
||||
children: <Widget>[
|
||||
const SizedBox(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 24.0, bottom: 4.0),
|
||||
child: Text('Steps', style: headingStyle),
|
||||
),
|
||||
]
|
||||
),
|
||||
...recipe.steps.map<TableRow>((RecipeStep step) {
|
||||
return _buildItemRow(step.duration ?? '', step.description);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TableRow _buildItemRow(String left, String right) {
|
||||
return TableRow(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Text(left, style: itemAmountStyle),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Text(right, style: itemStyle),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Recipe {
|
||||
const Recipe({
|
||||
this.name,
|
||||
this.author,
|
||||
this.description,
|
||||
this.imagePath,
|
||||
this.imagePackage,
|
||||
this.ingredientsImagePath,
|
||||
this.ingredientsImagePackage,
|
||||
this.ingredients,
|
||||
this.steps,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String author;
|
||||
final String description;
|
||||
final String imagePath;
|
||||
final String imagePackage;
|
||||
final String ingredientsImagePath;
|
||||
final String ingredientsImagePackage;
|
||||
final List<RecipeIngredient> ingredients;
|
||||
final List<RecipeStep> steps;
|
||||
}
|
||||
|
||||
class RecipeIngredient {
|
||||
const RecipeIngredient({this.amount, this.description});
|
||||
|
||||
final String amount;
|
||||
final String description;
|
||||
}
|
||||
|
||||
class RecipeStep {
|
||||
const RecipeStep({this.duration, this.description});
|
||||
|
||||
final String duration;
|
||||
final String description;
|
||||
}
|
||||
|
||||
const List<Recipe> kPestoRecipes = <Recipe>[
|
||||
Recipe(
|
||||
name: 'Roasted Chicken',
|
||||
author: 'Peter Carlsson',
|
||||
ingredientsImagePath: 'food/icons/main.png',
|
||||
ingredientsImagePackage: _kGalleryAssetsPackage,
|
||||
description: 'The perfect dish to welcome your family and friends with on a crisp autumn night. Pair with roasted veggies to truly impress them.',
|
||||
imagePath: 'food/roasted_chicken.png',
|
||||
imagePackage: _kGalleryAssetsPackage,
|
||||
ingredients: <RecipeIngredient>[
|
||||
RecipeIngredient(amount: '1 whole', description: 'Chicken'),
|
||||
RecipeIngredient(amount: '1/2 cup', description: 'Butter'),
|
||||
RecipeIngredient(amount: '1 tbsp', description: 'Onion powder'),
|
||||
RecipeIngredient(amount: '1 tbsp', description: 'Freshly ground pepper'),
|
||||
RecipeIngredient(amount: '1 tsp', description: 'Salt'),
|
||||
],
|
||||
steps: <RecipeStep>[
|
||||
RecipeStep(duration: '1 min', description: 'Put in oven'),
|
||||
RecipeStep(duration: '1hr 45 min', description: 'Cook'),
|
||||
],
|
||||
),
|
||||
Recipe(
|
||||
name: 'Chopped Beet Leaves',
|
||||
author: 'Trevor Hansen',
|
||||
ingredientsImagePath: 'food/icons/veggie.png',
|
||||
ingredientsImagePackage: _kGalleryAssetsPackage,
|
||||
description: 'This vegetable has more to offer than just its root. Beet greens can be tossed into a salad to add some variety or sauteed on its own with some oil and garlic.',
|
||||
imagePath: 'food/chopped_beet_leaves.png',
|
||||
imagePackage: _kGalleryAssetsPackage,
|
||||
ingredients: <RecipeIngredient>[
|
||||
RecipeIngredient(amount: '3 cups', description: 'Beet greens'),
|
||||
],
|
||||
steps: <RecipeStep>[
|
||||
RecipeStep(duration: '5 min', description: 'Chop'),
|
||||
],
|
||||
),
|
||||
Recipe(
|
||||
name: 'Pesto Pasta',
|
||||
author: 'Ali Connors',
|
||||
ingredientsImagePath: 'food/icons/main.png',
|
||||
ingredientsImagePackage: _kGalleryAssetsPackage,
|
||||
description: 'With this pesto recipe, you can quickly whip up a meal to satisfy your savory needs. And if you\'re feeling festive, you can add bacon to taste.',
|
||||
imagePath: 'food/pesto_pasta.png',
|
||||
imagePackage: _kGalleryAssetsPackage,
|
||||
ingredients: <RecipeIngredient>[
|
||||
RecipeIngredient(amount: '1/4 cup ', description: 'Pasta'),
|
||||
RecipeIngredient(amount: '2 cups', description: 'Fresh basil leaves'),
|
||||
RecipeIngredient(amount: '1/2 cup', description: 'Parmesan cheese'),
|
||||
RecipeIngredient(amount: '1/2 cup', description: 'Extra virgin olive oil'),
|
||||
RecipeIngredient(amount: '1/3 cup', description: 'Pine nuts'),
|
||||
RecipeIngredient(amount: '1/4 cup', description: 'Lemon juice'),
|
||||
RecipeIngredient(amount: '3 cloves', description: 'Garlic'),
|
||||
RecipeIngredient(amount: '1/4 tsp', description: 'Salt'),
|
||||
RecipeIngredient(amount: '1/8 tsp', description: 'Pepper'),
|
||||
RecipeIngredient(amount: '3 lbs', description: 'Bacon'),
|
||||
],
|
||||
steps: <RecipeStep>[
|
||||
RecipeStep(duration: '15 min', description: 'Blend'),
|
||||
],
|
||||
),
|
||||
Recipe(
|
||||
name: 'Cherry Pie',
|
||||
author: 'Sandra Adams',
|
||||
ingredientsImagePath: 'food/icons/main.png',
|
||||
ingredientsImagePackage: _kGalleryAssetsPackage,
|
||||
description: 'Sometimes when you\'re craving some cheer in your life you can jumpstart your day with some cherry pie. Dessert for breakfast is perfectly acceptable.',
|
||||
imagePath: 'food/cherry_pie.png',
|
||||
imagePackage: _kGalleryAssetsPackage,
|
||||
ingredients: <RecipeIngredient>[
|
||||
RecipeIngredient(amount: '1', description: 'Pie crust'),
|
||||
RecipeIngredient(amount: '4 cups', description: 'Fresh or frozen cherries'),
|
||||
RecipeIngredient(amount: '1 cup', description: 'Granulated sugar'),
|
||||
RecipeIngredient(amount: '4 tbsp', description: 'Cornstarch'),
|
||||
RecipeIngredient(amount: '1½ tbsp', description: 'Butter'),
|
||||
],
|
||||
steps: <RecipeStep>[
|
||||
RecipeStep(duration: '15 min', description: 'Mix'),
|
||||
RecipeStep(duration: '1hr 30 min', description: 'Bake'),
|
||||
],
|
||||
),
|
||||
Recipe(
|
||||
name: 'Spinach Salad',
|
||||
author: 'Peter Carlsson',
|
||||
ingredientsImagePath: 'food/icons/spicy.png',
|
||||
ingredientsImagePackage: _kGalleryAssetsPackage,
|
||||
description: 'Everyone\'s favorite leafy green is back. Paired with fresh sliced onion, it\'s ready to tackle any dish, whether it be a salad or an egg scramble.',
|
||||
imagePath: 'food/spinach_onion_salad.png',
|
||||
imagePackage: _kGalleryAssetsPackage,
|
||||
ingredients: <RecipeIngredient>[
|
||||
RecipeIngredient(amount: '4 cups', description: 'Spinach'),
|
||||
RecipeIngredient(amount: '1 cup', description: 'Sliced onion'),
|
||||
],
|
||||
steps: <RecipeStep>[
|
||||
RecipeStep(duration: '5 min', description: 'Mix'),
|
||||
],
|
||||
),
|
||||
Recipe(
|
||||
name: 'Butternut Squash Soup',
|
||||
author: 'Ali Connors',
|
||||
ingredientsImagePath: 'food/icons/healthy.png',
|
||||
ingredientsImagePackage: _kGalleryAssetsPackage,
|
||||
description: 'This creamy butternut squash soup will warm you on the chilliest of winter nights and bring a delightful pop of orange to the dinner table.',
|
||||
imagePath: 'food/butternut_squash_soup.png',
|
||||
imagePackage: _kGalleryAssetsPackage,
|
||||
ingredients: <RecipeIngredient>[
|
||||
RecipeIngredient(amount: '1', description: 'Butternut squash'),
|
||||
RecipeIngredient(amount: '4 cups', description: 'Chicken stock'),
|
||||
RecipeIngredient(amount: '2', description: 'Potatoes'),
|
||||
RecipeIngredient(amount: '1', description: 'Onion'),
|
||||
RecipeIngredient(amount: '1', description: 'Carrot'),
|
||||
RecipeIngredient(amount: '1', description: 'Celery'),
|
||||
RecipeIngredient(amount: '1 tsp', description: 'Salt'),
|
||||
RecipeIngredient(amount: '1 tsp', description: 'Pepper'),
|
||||
],
|
||||
steps: <RecipeStep>[
|
||||
RecipeStep(duration: '10 min', description: 'Prep vegetables'),
|
||||
RecipeStep(duration: '5 min', description: 'Stir'),
|
||||
RecipeStep(duration: '1 hr 10 min', description: 'Cook'),
|
||||
],
|
||||
),
|
||||
Recipe(
|
||||
name: 'Spanakopita',
|
||||
author: 'Trevor Hansen',
|
||||
ingredientsImagePath: 'food/icons/quick.png',
|
||||
ingredientsImagePackage: _kGalleryAssetsPackage,
|
||||
description: 'You \'feta\' believe this is a crowd-pleaser! Flaky phyllo pastry surrounds a delicious mixture of spinach and cheeses to create the perfect appetizer.',
|
||||
imagePath: 'food/spanakopita.png',
|
||||
imagePackage: _kGalleryAssetsPackage,
|
||||
ingredients: <RecipeIngredient>[
|
||||
RecipeIngredient(amount: '1 lb', description: 'Spinach'),
|
||||
RecipeIngredient(amount: '½ cup', description: 'Feta cheese'),
|
||||
RecipeIngredient(amount: '½ cup', description: 'Cottage cheese'),
|
||||
RecipeIngredient(amount: '2', description: 'Eggs'),
|
||||
RecipeIngredient(amount: '1', description: 'Onion'),
|
||||
RecipeIngredient(amount: '½ lb', description: 'Phyllo dough'),
|
||||
],
|
||||
steps: <RecipeStep>[
|
||||
RecipeStep(duration: '5 min', description: 'Sauté vegetables'),
|
||||
RecipeStep(duration: '3 min', description: 'Stir vegetables and other filling ingredients'),
|
||||
RecipeStep(duration: '10 min', description: 'Fill phyllo squares half-full with filling and fold.'),
|
||||
RecipeStep(duration: '40 min', description: 'Bake'),
|
||||
],
|
||||
),
|
||||
];
|
||||
@ -1,138 +0,0 @@
|
||||
// Copyright 2018-present the Flutter authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gallery/demo/shrine/backdrop.dart';
|
||||
import 'package:flutter_gallery/demo/shrine/category_menu_page.dart';
|
||||
import 'package:flutter_gallery/demo/shrine/colors.dart';
|
||||
import 'package:flutter_gallery/demo/shrine/expanding_bottom_sheet.dart';
|
||||
import 'package:flutter_gallery/demo/shrine/home.dart';
|
||||
import 'package:flutter_gallery/demo/shrine/login.dart';
|
||||
import 'package:flutter_gallery/demo/shrine/supplemental/cut_corners_border.dart';
|
||||
|
||||
class ShrineApp extends StatefulWidget {
|
||||
@override
|
||||
_ShrineAppState createState() => _ShrineAppState();
|
||||
}
|
||||
|
||||
class _ShrineAppState extends State<ShrineApp> with SingleTickerProviderStateMixin {
|
||||
// Controller to coordinate both the opening/closing of backdrop and sliding
|
||||
// of expanding bottom sheet
|
||||
AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 450),
|
||||
value: 1.0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Shrine',
|
||||
home: HomePage(
|
||||
backdrop: Backdrop(
|
||||
frontLayer: const ProductPage(),
|
||||
backLayer: CategoryMenuPage(onCategoryTap: () => _controller.forward()),
|
||||
frontTitle: const Text('SHRINE'),
|
||||
backTitle: const Text('MENU'),
|
||||
controller: _controller,
|
||||
),
|
||||
expandingBottomSheet: ExpandingBottomSheet(hideController: _controller),
|
||||
),
|
||||
initialRoute: '/login',
|
||||
onGenerateRoute: _getRoute,
|
||||
// Copy the platform from the main theme in order to support platform
|
||||
// toggling from the Gallery options menu.
|
||||
theme: _kShrineTheme.copyWith(platform: Theme.of(context).platform),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Route<dynamic> _getRoute(RouteSettings settings) {
|
||||
if (settings.name != '/login') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return MaterialPageRoute<void>(
|
||||
settings: settings,
|
||||
builder: (BuildContext context) => LoginPage(),
|
||||
fullscreenDialog: true,
|
||||
);
|
||||
}
|
||||
|
||||
final ThemeData _kShrineTheme = _buildShrineTheme();
|
||||
|
||||
IconThemeData _customIconTheme(IconThemeData original) {
|
||||
return original.copyWith(color: kShrineBrown900);
|
||||
}
|
||||
|
||||
ThemeData _buildShrineTheme() {
|
||||
final ThemeData base = ThemeData.light();
|
||||
return base.copyWith(
|
||||
colorScheme: kShrineColorScheme,
|
||||
accentColor: kShrineBrown900,
|
||||
primaryColor: kShrinePink100,
|
||||
buttonColor: kShrinePink100,
|
||||
scaffoldBackgroundColor: kShrineBackgroundWhite,
|
||||
cardColor: kShrineBackgroundWhite,
|
||||
textSelectionColor: kShrinePink100,
|
||||
errorColor: kShrineErrorRed,
|
||||
buttonTheme: const ButtonThemeData(
|
||||
colorScheme: kShrineColorScheme,
|
||||
textTheme: ButtonTextTheme.normal,
|
||||
),
|
||||
primaryIconTheme: _customIconTheme(base.iconTheme),
|
||||
inputDecorationTheme: const InputDecorationTheme(border: CutCornersBorder()),
|
||||
textTheme: _buildShrineTextTheme(base.textTheme),
|
||||
primaryTextTheme: _buildShrineTextTheme(base.primaryTextTheme),
|
||||
accentTextTheme: _buildShrineTextTheme(base.accentTextTheme),
|
||||
iconTheme: _customIconTheme(base.iconTheme),
|
||||
);
|
||||
}
|
||||
|
||||
TextTheme _buildShrineTextTheme(TextTheme base) {
|
||||
return base.copyWith(
|
||||
headline: base.headline.copyWith(fontWeight: FontWeight.w500),
|
||||
title: base.title.copyWith(fontSize: 18.0),
|
||||
caption: base.caption.copyWith(fontWeight: FontWeight.w400, fontSize: 14.0),
|
||||
body2: base.body2.copyWith(fontWeight: FontWeight.w500, fontSize: 16.0),
|
||||
button: base.button.copyWith(fontWeight: FontWeight.w500, fontSize: 14.0),
|
||||
).apply(
|
||||
fontFamily: 'Raleway',
|
||||
displayColor: kShrineBrown900,
|
||||
bodyColor: kShrineBrown900,
|
||||
);
|
||||
}
|
||||
|
||||
const ColorScheme kShrineColorScheme = ColorScheme(
|
||||
primary: kShrinePink100,
|
||||
primaryVariant: kShrineBrown900,
|
||||
secondary: kShrinePink50,
|
||||
secondaryVariant: kShrineBrown900,
|
||||
surface: kShrineSurfaceWhite,
|
||||
background: kShrineBackgroundWhite,
|
||||
error: kShrineErrorRed,
|
||||
onPrimary: kShrineBrown900,
|
||||
onSecondary: kShrineBrown900,
|
||||
onSurface: kShrineBrown900,
|
||||
onBackground: kShrineBrown900,
|
||||
onError: kShrineSurfaceWhite,
|
||||
brightness: Brightness.light,
|
||||
);
|
||||
@ -1,397 +0,0 @@
|
||||
// Copyright 2018-present the Flutter authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'package:flutter_gallery/demo/shrine/login.dart';
|
||||
|
||||
const Cubic _kAccelerateCurve = Cubic(0.548, 0.0, 0.757, 0.464);
|
||||
const Cubic _kDecelerateCurve = Cubic(0.23, 0.94, 0.41, 1.0);
|
||||
const double _kPeakVelocityTime = 0.248210;
|
||||
const double _kPeakVelocityProgress = 0.379146;
|
||||
|
||||
class _TappableWhileStatusIs extends StatefulWidget {
|
||||
const _TappableWhileStatusIs(
|
||||
this.status, {
|
||||
Key key,
|
||||
this.controller,
|
||||
this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
final AnimationController controller;
|
||||
final AnimationStatus status;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
_TappableWhileStatusIsState createState() => _TappableWhileStatusIsState();
|
||||
}
|
||||
|
||||
class _TappableWhileStatusIsState extends State<_TappableWhileStatusIs> {
|
||||
bool _active;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.controller.addStatusListener(_handleStatusChange);
|
||||
_active = widget.controller.status == widget.status;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeStatusListener(_handleStatusChange);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleStatusChange(AnimationStatus status) {
|
||||
final bool value = widget.controller.status == widget.status;
|
||||
if (_active != value) {
|
||||
setState(() {
|
||||
_active = value;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = AbsorbPointer(
|
||||
absorbing: !_active,
|
||||
child: widget.child,
|
||||
);
|
||||
|
||||
if (!_active) {
|
||||
child = FocusScope(
|
||||
canRequestFocus: false,
|
||||
debugLabel: '$_TappableWhileStatusIs',
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
class _FrontLayer extends StatelessWidget {
|
||||
const _FrontLayer({
|
||||
Key key,
|
||||
this.onTap,
|
||||
this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
final VoidCallback onTap;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
elevation: 16.0,
|
||||
shape: const BeveledRectangleBorder(
|
||||
borderRadius: BorderRadius.only(topLeft: Radius.circular(46.0)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 40.0,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: child,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BackdropTitle extends AnimatedWidget {
|
||||
const _BackdropTitle({
|
||||
Key key,
|
||||
Listenable listenable,
|
||||
this.onPress,
|
||||
@required this.frontTitle,
|
||||
@required this.backTitle,
|
||||
}) : assert(frontTitle != null),
|
||||
assert(backTitle != null),
|
||||
super(key: key, listenable: listenable);
|
||||
|
||||
final Function onPress;
|
||||
final Widget frontTitle;
|
||||
final Widget backTitle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Animation<double> animation = CurvedAnimation(
|
||||
parent: listenable,
|
||||
curve: const Interval(0.0, 0.78),
|
||||
);
|
||||
|
||||
return DefaultTextStyle(
|
||||
style: Theme.of(context).primaryTextTheme.title,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: Row(children: <Widget>[
|
||||
// branded icon
|
||||
SizedBox(
|
||||
width: 72.0,
|
||||
child: IconButton(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
onPressed: onPress,
|
||||
icon: Stack(children: <Widget>[
|
||||
Opacity(
|
||||
opacity: animation.value,
|
||||
child: const ImageIcon(AssetImage('packages/shrine_images/slanted_menu.png')),
|
||||
),
|
||||
FractionalTranslation(
|
||||
translation: Tween<Offset>(
|
||||
begin: Offset.zero,
|
||||
end: const Offset(1.0, 0.0),
|
||||
).evaluate(animation),
|
||||
child: const ImageIcon(AssetImage('packages/shrine_images/diamond.png')),
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
// Here, we do a custom cross fade between backTitle and frontTitle.
|
||||
// This makes a smooth animation between the two texts.
|
||||
Stack(
|
||||
children: <Widget>[
|
||||
Opacity(
|
||||
opacity: CurvedAnimation(
|
||||
parent: ReverseAnimation(animation),
|
||||
curve: const Interval(0.5, 1.0),
|
||||
).value,
|
||||
child: FractionalTranslation(
|
||||
translation: Tween<Offset>(
|
||||
begin: Offset.zero,
|
||||
end: const Offset(0.5, 0.0),
|
||||
).evaluate(animation),
|
||||
child: backTitle,
|
||||
),
|
||||
),
|
||||
Opacity(
|
||||
opacity: CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: const Interval(0.5, 1.0),
|
||||
).value,
|
||||
child: FractionalTranslation(
|
||||
translation: Tween<Offset>(
|
||||
begin: const Offset(-0.25, 0.0),
|
||||
end: Offset.zero,
|
||||
).evaluate(animation),
|
||||
child: frontTitle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a Backdrop.
|
||||
///
|
||||
/// A Backdrop widget has two layers, front and back. The front layer is shown
|
||||
/// by default, and slides down to show the back layer, from which a user
|
||||
/// can make a selection. The user can also configure the titles for when the
|
||||
/// front or back layer is showing.
|
||||
class Backdrop extends StatefulWidget {
|
||||
const Backdrop({
|
||||
@required this.frontLayer,
|
||||
@required this.backLayer,
|
||||
@required this.frontTitle,
|
||||
@required this.backTitle,
|
||||
@required this.controller,
|
||||
}) : assert(frontLayer != null),
|
||||
assert(backLayer != null),
|
||||
assert(frontTitle != null),
|
||||
assert(backTitle != null),
|
||||
assert(controller != null);
|
||||
|
||||
final Widget frontLayer;
|
||||
final Widget backLayer;
|
||||
final Widget frontTitle;
|
||||
final Widget backTitle;
|
||||
final AnimationController controller;
|
||||
|
||||
@override
|
||||
_BackdropState createState() => _BackdropState();
|
||||
}
|
||||
|
||||
class _BackdropState extends State<Backdrop> with SingleTickerProviderStateMixin {
|
||||
final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop');
|
||||
AnimationController _controller;
|
||||
Animation<RelativeRect> _layerAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = widget.controller;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool get _frontLayerVisible {
|
||||
final AnimationStatus status = _controller.status;
|
||||
return status == AnimationStatus.completed || status == AnimationStatus.forward;
|
||||
}
|
||||
|
||||
void _toggleBackdropLayerVisibility() {
|
||||
// Call setState here to update layerAnimation if that's necessary
|
||||
setState(() {
|
||||
_frontLayerVisible ? _controller.reverse() : _controller.forward();
|
||||
});
|
||||
}
|
||||
|
||||
// _layerAnimation animates the front layer between open and close.
|
||||
// _getLayerAnimation adjusts the values in the TweenSequence so the
|
||||
// curve and timing are correct in both directions.
|
||||
Animation<RelativeRect> _getLayerAnimation(Size layerSize, double layerTop) {
|
||||
Curve firstCurve; // Curve for first TweenSequenceItem
|
||||
Curve secondCurve; // Curve for second TweenSequenceItem
|
||||
double firstWeight; // Weight of first TweenSequenceItem
|
||||
double secondWeight; // Weight of second TweenSequenceItem
|
||||
Animation<double> animation; // Animation on which TweenSequence runs
|
||||
|
||||
if (_frontLayerVisible) {
|
||||
firstCurve = _kAccelerateCurve;
|
||||
secondCurve = _kDecelerateCurve;
|
||||
firstWeight = _kPeakVelocityTime;
|
||||
secondWeight = 1.0 - _kPeakVelocityTime;
|
||||
animation = CurvedAnimation(
|
||||
parent: _controller.view,
|
||||
curve: const Interval(0.0, 0.78),
|
||||
);
|
||||
} else {
|
||||
// These values are only used when the controller runs from t=1.0 to t=0.0
|
||||
firstCurve = _kDecelerateCurve.flipped;
|
||||
secondCurve = _kAccelerateCurve.flipped;
|
||||
firstWeight = 1.0 - _kPeakVelocityTime;
|
||||
secondWeight = _kPeakVelocityTime;
|
||||
animation = _controller.view;
|
||||
}
|
||||
|
||||
return TweenSequence<RelativeRect>(
|
||||
<TweenSequenceItem<RelativeRect>>[
|
||||
TweenSequenceItem<RelativeRect>(
|
||||
tween: RelativeRectTween(
|
||||
begin: RelativeRect.fromLTRB(
|
||||
0.0,
|
||||
layerTop,
|
||||
0.0,
|
||||
layerTop - layerSize.height,
|
||||
),
|
||||
end: RelativeRect.fromLTRB(
|
||||
0.0,
|
||||
layerTop * _kPeakVelocityProgress,
|
||||
0.0,
|
||||
(layerTop - layerSize.height) * _kPeakVelocityProgress,
|
||||
),
|
||||
).chain(CurveTween(curve: firstCurve)),
|
||||
weight: firstWeight,
|
||||
),
|
||||
TweenSequenceItem<RelativeRect>(
|
||||
tween: RelativeRectTween(
|
||||
begin: RelativeRect.fromLTRB(
|
||||
0.0,
|
||||
layerTop * _kPeakVelocityProgress,
|
||||
0.0,
|
||||
(layerTop - layerSize.height) * _kPeakVelocityProgress,
|
||||
),
|
||||
end: RelativeRect.fill,
|
||||
).chain(CurveTween(curve: secondCurve)),
|
||||
weight: secondWeight,
|
||||
),
|
||||
],
|
||||
).animate(animation);
|
||||
}
|
||||
|
||||
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
|
||||
const double layerTitleHeight = 48.0;
|
||||
final Size layerSize = constraints.biggest;
|
||||
final double layerTop = layerSize.height - layerTitleHeight;
|
||||
|
||||
_layerAnimation = _getLayerAnimation(layerSize, layerTop);
|
||||
|
||||
return Stack(
|
||||
key: _backdropKey,
|
||||
children: <Widget>[
|
||||
_TappableWhileStatusIs(
|
||||
AnimationStatus.dismissed,
|
||||
controller: _controller,
|
||||
child: widget.backLayer,
|
||||
),
|
||||
PositionedTransition(
|
||||
rect: _layerAnimation,
|
||||
child: _FrontLayer(
|
||||
onTap: _toggleBackdropLayerVisibility,
|
||||
child: _TappableWhileStatusIs(
|
||||
AnimationStatus.completed,
|
||||
controller: _controller,
|
||||
child: widget.frontLayer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final AppBar appBar = AppBar(
|
||||
brightness: Brightness.light,
|
||||
elevation: 0.0,
|
||||
titleSpacing: 0.0,
|
||||
title: _BackdropTitle(
|
||||
listenable: _controller.view,
|
||||
onPress: _toggleBackdropLayerVisibility,
|
||||
frontTitle: widget.frontTitle,
|
||||
backTitle: widget.backTitle,
|
||||
),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search, semanticLabel: 'login'),
|
||||
onPressed: () {
|
||||
Navigator.push<void>(
|
||||
context,
|
||||
MaterialPageRoute<void>(builder: (BuildContext context) => LoginPage()),
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.tune, semanticLabel: 'login'),
|
||||
onPressed: () {
|
||||
Navigator.push<void>(
|
||||
context,
|
||||
MaterialPageRoute<void>(builder: (BuildContext context) => LoginPage()),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
return Scaffold(
|
||||
appBar: appBar,
|
||||
body: LayoutBuilder(
|
||||
builder: _buildStack,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,85 +0,0 @@
|
||||
// Copyright 2018-present the Flutter authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:scoped_model/scoped_model.dart';
|
||||
|
||||
import 'package:flutter_gallery/demo/shrine/colors.dart';
|
||||
import 'package:flutter_gallery/demo/shrine/model/app_state_model.dart';
|
||||
import 'package:flutter_gallery/demo/shrine/model/product.dart';
|
||||
|
||||
class CategoryMenuPage extends StatelessWidget {
|
||||
const CategoryMenuPage({
|
||||
Key key,
|
||||
this.onCategoryTap,
|
||||
}) : super(key: key);
|
||||
|
||||
final VoidCallback onCategoryTap;
|
||||
|
||||
Widget _buildCategory(Category category, BuildContext context) {
|
||||
final String categoryString = category.toString().replaceAll('Category.', '').toUpperCase();
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return ScopedModelDescendant<AppStateModel>(
|
||||
builder: (BuildContext context, Widget child, AppStateModel model) =>
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
model.setCategory(category);
|
||||
if (onCategoryTap != null) {
|
||||
onCategoryTap();
|
||||
}
|
||||
},
|
||||
child: model.selectedCategory == category
|
||||
? Column(
|
||||
children: <Widget>[
|
||||
const SizedBox(height: 16.0),
|
||||
Text(
|
||||
categoryString,
|
||||
style: theme.textTheme.body2,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 14.0),
|
||||
Container(
|
||||
width: 70.0,
|
||||
height: 2.0,
|
||||
color: kShrinePink400,
|
||||
),
|
||||
],
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: Text(
|
||||
categoryString,
|
||||
style: theme.textTheme.body2.copyWith(
|
||||
color: kShrineBrown900.withAlpha(153)
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(top: 40.0),
|
||||
color: kShrinePink100,
|
||||
child: ListView(
|
||||
children: Category.values.map((Category c) => _buildCategory(c, context)).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
// Copyright 2018-present the Flutter authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const Color kShrinePink50 = Color(0xFFFEEAE6);
|
||||
const Color kShrinePink100 = Color(0xFFFEDBD0);
|
||||
const Color kShrinePink300 = Color(0xFFFBB8AC);
|
||||
const Color kShrinePink400 = Color(0xFFEAA4A4);
|
||||
|
||||
const Color kShrineBrown900 = Color(0xFF442B2D);
|
||||
const Color kShrineBrown600 = Color(0xFF7D4F52);
|
||||
|
||||
const Color kShrineErrorRed = Color(0xFFC5032B);
|
||||
|
||||
const Color kShrineSurfaceWhite = Color(0xFFFFFBFA);
|
||||
const Color kShrineBackgroundWhite = Colors.white;
|
||||
@ -1,656 +0,0 @@
|
||||
// Copyright 2018-present the Flutter authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:scoped_model/scoped_model.dart';
|
||||
|
||||
import 'package:flutter_gallery/demo/shrine/colors.dart';
|
||||
import 'package:flutter_gallery/demo/shrine/model/app_state_model.dart';
|
||||
import 'package:flutter_gallery/demo/shrine/model/product.dart';
|
||||
import 'package:flutter_gallery/demo/shrine/shopping_cart.dart';
|
||||
|
||||
// These curves define the emphasized easing curve.
|
||||
const Cubic _kAccelerateCurve = Cubic(0.548, 0.0, 0.757, 0.464);
|
||||
const Cubic _kDecelerateCurve = Cubic(0.23, 0.94, 0.41, 1.0);
|
||||
// The time at which the accelerate and decelerate curves switch off
|
||||
const double _kPeakVelocityTime = 0.248210;
|
||||
// Percent (as a decimal) of animation that should be completed at _peakVelocityTime
|
||||
const double _kPeakVelocityProgress = 0.379146;
|
||||
const double _kCartHeight = 56.0;
|
||||
// Radius of the shape on the top left of the sheet.
|
||||
const double _kCornerRadius = 24.0;
|
||||
// Width for just the cart icon and no thumbnails.
|
||||
const double _kWidthForCartIcon = 64.0;
|
||||
|
||||
class ExpandingBottomSheet extends StatefulWidget {
|
||||
const ExpandingBottomSheet({Key key, @required this.hideController})
|
||||
: assert(hideController != null),
|
||||
super(key: key);
|
||||
|
||||
final AnimationController hideController;
|
||||
|
||||
@override
|
||||
_ExpandingBottomSheetState createState() => _ExpandingBottomSheetState();
|
||||
|
||||
static _ExpandingBottomSheetState of(BuildContext context, {bool isNullOk = false}) {
|
||||
assert(isNullOk != null);
|
||||
assert(context != null);
|
||||
final _ExpandingBottomSheetState result = context.ancestorStateOfType(
|
||||
const TypeMatcher<_ExpandingBottomSheetState>()
|
||||
);
|
||||
if (isNullOk || result != null) {
|
||||
return result;
|
||||
}
|
||||
throw FlutterError(
|
||||
'ExpandingBottomSheet.of() called with a context that does not contain a ExpandingBottomSheet.\n');
|
||||
}
|
||||
}
|
||||
|
||||
// Emphasized Easing is a motion curve that has an organic, exciting feeling.
|
||||
// It's very fast to begin with and then very slow to finish. Unlike standard
|
||||
// curves, like [Curves.fastOutSlowIn], it can't be expressed in a cubic bezier
|
||||
// curve formula. It's quintic, not cubic. But it _can_ be expressed as one
|
||||
// curve followed by another, which we do here.
|
||||
Animation<T> _getEmphasizedEasingAnimation<T>({
|
||||
@required T begin,
|
||||
@required T peak,
|
||||
@required T end,
|
||||
@required bool isForward,
|
||||
@required Animation<double> parent,
|
||||
}) {
|
||||
Curve firstCurve;
|
||||
Curve secondCurve;
|
||||
double firstWeight;
|
||||
double secondWeight;
|
||||
|
||||
if (isForward) {
|
||||
firstCurve = _kAccelerateCurve;
|
||||
secondCurve = _kDecelerateCurve;
|
||||
firstWeight = _kPeakVelocityTime;
|
||||
secondWeight = 1.0 - _kPeakVelocityTime;
|
||||
} else {
|
||||
firstCurve = _kDecelerateCurve.flipped;
|
||||
secondCurve = _kAccelerateCurve.flipped;
|
||||
firstWeight = 1.0 - _kPeakVelocityTime;
|
||||
secondWeight = _kPeakVelocityTime;
|
||||
}
|
||||
|
||||
return TweenSequence<T>(
|
||||
<TweenSequenceItem<T>>[
|
||||
TweenSequenceItem<T>(
|
||||
weight: firstWeight,
|
||||
tween: Tween<T>(
|
||||
begin: begin,
|
||||
end: peak,
|
||||
).chain(CurveTween(curve: firstCurve)),
|
||||
),
|
||||
TweenSequenceItem<T>(
|
||||
weight: secondWeight,
|
||||
tween: Tween<T>(
|
||||
begin: peak,
|
||||
end: end,
|
||||
).chain(CurveTween(curve: secondCurve)),
|
||||
),
|
||||
],
|
||||
).animate(parent);
|
||||
}
|
||||
|
||||
// Calculates the value where two double Animations should be joined. Used by
|
||||
// callers of _getEmphasisedEasing<double>().
|
||||
double _getPeakPoint({double begin, double end}) {
|
||||
return begin + (end - begin) * _kPeakVelocityProgress;
|
||||
}
|
||||
|
||||
class _ExpandingBottomSheetState extends State<ExpandingBottomSheet> with TickerProviderStateMixin {
|
||||
final GlobalKey _expandingBottomSheetKey = GlobalKey(debugLabel: 'Expanding bottom sheet');
|
||||
|
||||
// The width of the Material, calculated by _widthFor() & based on the number
|
||||
// of products in the cart. 64.0 is the width when there are 0 products
|
||||
// (_kWidthForZeroProducts)
|
||||
double _width = _kWidthForCartIcon;
|
||||
|
||||
// Controller for the opening and closing of the ExpandingBottomSheet
|
||||
AnimationController _controller;
|
||||
|
||||
// Animations for the opening and closing of the ExpandingBottomSheet
|
||||
Animation<double> _widthAnimation;
|
||||
Animation<double> _heightAnimation;
|
||||
Animation<double> _thumbnailOpacityAnimation;
|
||||
Animation<double> _cartOpacityAnimation;
|
||||
Animation<double> _shapeAnimation;
|
||||
Animation<Offset> _slideAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
vsync: this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Animation<double> _getWidthAnimation(double screenWidth) {
|
||||
if (_controller.status == AnimationStatus.forward) {
|
||||
// Opening animation
|
||||
return Tween<double>(begin: _width, end: screenWidth).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller.view,
|
||||
curve: const Interval(0.0, 0.3, curve: Curves.fastOutSlowIn),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Closing animation
|
||||
return _getEmphasizedEasingAnimation(
|
||||
begin: _width,
|
||||
peak: _getPeakPoint(begin: _width, end: screenWidth),
|
||||
end: screenWidth,
|
||||
isForward: false,
|
||||
parent: CurvedAnimation(parent: _controller.view, curve: const Interval(0.0, 0.87)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Animation<double> _getHeightAnimation(double screenHeight) {
|
||||
if (_controller.status == AnimationStatus.forward) {
|
||||
// Opening animation
|
||||
|
||||
return _getEmphasizedEasingAnimation(
|
||||
begin: _kCartHeight,
|
||||
peak: _kCartHeight + (screenHeight - _kCartHeight) * _kPeakVelocityProgress,
|
||||
end: screenHeight,
|
||||
isForward: true,
|
||||
parent: _controller.view,
|
||||
);
|
||||
} else {
|
||||
// Closing animation
|
||||
return Tween<double>(
|
||||
begin: _kCartHeight,
|
||||
end: screenHeight,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller.view,
|
||||
curve: const Interval(0.434, 1.0, curve: Curves.linear), // not used
|
||||
// only the reverseCurve will be used
|
||||
reverseCurve: Interval(0.434, 1.0, curve: Curves.fastOutSlowIn.flipped),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Animation of the cut corner. It's cut when closed and not cut when open.
|
||||
Animation<double> _getShapeAnimation() {
|
||||
if (_controller.status == AnimationStatus.forward) {
|
||||
return Tween<double>(begin: _kCornerRadius, end: 0.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller.view,
|
||||
curve: const Interval(0.0, 0.3, curve: Curves.fastOutSlowIn),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return _getEmphasizedEasingAnimation(
|
||||
begin: _kCornerRadius,
|
||||
peak: _getPeakPoint(begin: _kCornerRadius, end: 0.0),
|
||||
end: 0.0,
|
||||
isForward: false,
|
||||
parent: _controller.view,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Animation<double> _getThumbnailOpacityAnimation() {
|
||||
return Tween<double>(begin: 1.0, end: 0.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller.view,
|
||||
curve: _controller.status == AnimationStatus.forward
|
||||
? const Interval(0.0, 0.3)
|
||||
: const Interval(0.532, 0.766),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Animation<double> _getCartOpacityAnimation() {
|
||||
return CurvedAnimation(
|
||||
parent: _controller.view,
|
||||
curve: _controller.status == AnimationStatus.forward
|
||||
? const Interval(0.3, 0.6)
|
||||
: const Interval(0.766, 1.0),
|
||||
);
|
||||
}
|
||||
|
||||
// Returns the correct width of the ExpandingBottomSheet based on the number of
|
||||
// products in the cart.
|
||||
double _widthFor(int numProducts) {
|
||||
switch (numProducts) {
|
||||
case 0:
|
||||
return _kWidthForCartIcon;
|
||||
case 1:
|
||||
return 136.0;
|
||||
case 2:
|
||||
return 192.0;
|
||||
case 3:
|
||||
return 248.0;
|
||||
default:
|
||||
return 278.0;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if the cart is open or opening and false otherwise.
|
||||
bool get _isOpen {
|
||||
final AnimationStatus status = _controller.status;
|
||||
return status == AnimationStatus.completed || status == AnimationStatus.forward;
|
||||
}
|
||||
|
||||
// Opens the ExpandingBottomSheet if it's closed, otherwise does nothing.
|
||||
void open() {
|
||||
if (!_isOpen) {
|
||||
_controller.forward();
|
||||
}
|
||||
}
|
||||
|
||||
// Closes the ExpandingBottomSheet if it's open or opening, otherwise does nothing.
|
||||
void close() {
|
||||
if (_isOpen) {
|
||||
_controller.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
// Changes the padding between the start edge of the Material and the cart icon
|
||||
// based on the number of products in the cart (padding increases when > 0
|
||||
// products.)
|
||||
EdgeInsetsDirectional _cartPaddingFor(int numProducts) {
|
||||
return (numProducts == 0)
|
||||
? const EdgeInsetsDirectional.only(start: 20.0, end: 8.0)
|
||||
: const EdgeInsetsDirectional.only(start: 32.0, end: 8.0);
|
||||
}
|
||||
|
||||
bool get _cartIsVisible => _thumbnailOpacityAnimation.value == 0.0;
|
||||
|
||||
Widget _buildThumbnails(int numProducts) {
|
||||
return ExcludeSemantics(
|
||||
child: Opacity(
|
||||
opacity: _thumbnailOpacityAnimation.value,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
AnimatedPadding(
|
||||
padding: _cartPaddingFor(numProducts),
|
||||
child: const Icon(Icons.shopping_cart),
|
||||
duration: const Duration(milliseconds: 225),
|
||||
),
|
||||
Container(
|
||||
// Accounts for the overflow number
|
||||
width: numProducts > 3 ? _width - 94.0 : _width - 64.0,
|
||||
height: _kCartHeight,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: ProductThumbnailRow(),
|
||||
),
|
||||
ExtraProductsNumber(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildShoppingCartPage() {
|
||||
return Opacity(
|
||||
opacity: _cartOpacityAnimation.value,
|
||||
child: ShoppingCartPage(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCart(BuildContext context, Widget child) {
|
||||
// numProducts is the number of different products in the cart (does not
|
||||
// include multiples of the same product).
|
||||
final AppStateModel model = ScopedModel.of<AppStateModel>(context);
|
||||
final int numProducts = model.productsInCart.keys.length;
|
||||
final int totalCartQuantity = model.totalCartQuantity;
|
||||
final Size screenSize = MediaQuery.of(context).size;
|
||||
final double screenWidth = screenSize.width;
|
||||
final double screenHeight = screenSize.height;
|
||||
|
||||
_width = _widthFor(numProducts);
|
||||
_widthAnimation = _getWidthAnimation(screenWidth);
|
||||
_heightAnimation = _getHeightAnimation(screenHeight);
|
||||
_shapeAnimation = _getShapeAnimation();
|
||||
_thumbnailOpacityAnimation = _getThumbnailOpacityAnimation();
|
||||
_cartOpacityAnimation = _getCartOpacityAnimation();
|
||||
|
||||
return Semantics(
|
||||
button: true,
|
||||
value: 'Shopping cart, $totalCartQuantity items',
|
||||
child: Container(
|
||||
width: _widthAnimation.value,
|
||||
height: _heightAnimation.value,
|
||||
child: Material(
|
||||
animationDuration: const Duration(milliseconds: 0),
|
||||
shape: BeveledRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(_shapeAnimation.value),
|
||||
),
|
||||
),
|
||||
elevation: 4.0,
|
||||
color: kShrinePink50,
|
||||
child: _cartIsVisible
|
||||
? _buildShoppingCartPage()
|
||||
: _buildThumbnails(numProducts),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Builder for the hide and reveal animation when the backdrop opens and closes
|
||||
Widget _buildSlideAnimation(BuildContext context, Widget child) {
|
||||
_slideAnimation = _getEmphasizedEasingAnimation(
|
||||
begin: const Offset(1.0, 0.0),
|
||||
peak: const Offset(_kPeakVelocityProgress, 0.0),
|
||||
end: const Offset(0.0, 0.0),
|
||||
isForward: widget.hideController.status == AnimationStatus.forward,
|
||||
parent: widget.hideController,
|
||||
);
|
||||
|
||||
return SlideTransition(
|
||||
position: _slideAnimation,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
// Closes the cart if the cart is open, otherwise exits the app (this should
|
||||
// only be relevant for Android).
|
||||
Future<bool> _onWillPop() async {
|
||||
if (!_isOpen) {
|
||||
await SystemNavigator.pop();
|
||||
return true;
|
||||
}
|
||||
|
||||
close();
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedSize(
|
||||
key: _expandingBottomSheetKey,
|
||||
duration: const Duration(milliseconds: 225),
|
||||
curve: Curves.easeInOut,
|
||||
vsync: this,
|
||||
alignment: FractionalOffset.topLeft,
|
||||
child: WillPopScope(
|
||||
onWillPop: _onWillPop,
|
||||
child: AnimatedBuilder(
|
||||
animation: widget.hideController,
|
||||
builder: _buildSlideAnimation,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: open,
|
||||
child: ScopedModelDescendant<AppStateModel>(
|
||||
builder: (BuildContext context, Widget child, AppStateModel model) {
|
||||
return AnimatedBuilder(
|
||||
builder: _buildCart,
|
||||
animation: _controller,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ProductThumbnailRow extends StatefulWidget {
|
||||
@override
|
||||
_ProductThumbnailRowState createState() => _ProductThumbnailRowState();
|
||||
}
|
||||
|
||||
class _ProductThumbnailRowState extends State<ProductThumbnailRow> {
|
||||
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
|
||||
|
||||
// _list represents what's currently on screen. If _internalList updates,
|
||||
// it will need to be updated to match it.
|
||||
_ListModel _list;
|
||||
|
||||
// _internalList represents the list as it is updated by the AppStateModel.
|
||||
List<int> _internalList;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_list = _ListModel(
|
||||
listKey: _listKey,
|
||||
initialItems: ScopedModel.of<AppStateModel>(context).productsInCart.keys.toList(),
|
||||
removedItemBuilder: _buildRemovedThumbnail,
|
||||
);
|
||||
_internalList = List<int>.from(_list.list);
|
||||
}
|
||||
|
||||
Product _productWithId(int productId) {
|
||||
final AppStateModel model = ScopedModel.of<AppStateModel>(context);
|
||||
final Product product = model.getProductById(productId);
|
||||
assert(product != null);
|
||||
return product;
|
||||
}
|
||||
|
||||
Widget _buildRemovedThumbnail(int item, BuildContext context, Animation<double> animation) {
|
||||
return ProductThumbnail(animation, animation, _productWithId(item));
|
||||
}
|
||||
|
||||
Widget _buildThumbnail(BuildContext context, int index, Animation<double> animation) {
|
||||
final Animation<double> thumbnailSize = Tween<double>(begin: 0.8, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
curve: const Interval(0.33, 1.0, curve: Curves.easeIn),
|
||||
parent: animation,
|
||||
),
|
||||
);
|
||||
|
||||
final Animation<double> opacity = CurvedAnimation(
|
||||
curve: const Interval(0.33, 1.0, curve: Curves.linear),
|
||||
parent: animation,
|
||||
);
|
||||
|
||||
return ProductThumbnail(thumbnailSize, opacity, _productWithId(_list[index]));
|
||||
}
|
||||
|
||||
// If the lists are the same length, assume nothing has changed.
|
||||
// If the internalList is shorter than the ListModel, an item has been removed.
|
||||
// If the internalList is longer, then an item has been added.
|
||||
void _updateLists() {
|
||||
// Update _internalList based on the model
|
||||
_internalList = ScopedModel.of<AppStateModel>(context).productsInCart.keys.toList();
|
||||
final Set<int> internalSet = Set<int>.from(_internalList);
|
||||
final Set<int> listSet = Set<int>.from(_list.list);
|
||||
|
||||
final Set<int> difference = internalSet.difference(listSet);
|
||||
if (difference.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int product in difference) {
|
||||
if (_internalList.length < _list.length) {
|
||||
_list.remove(product);
|
||||
} else if (_internalList.length > _list.length) {
|
||||
_list.add(product);
|
||||
}
|
||||
}
|
||||
|
||||
while (_internalList.length != _list.length) {
|
||||
int index = 0;
|
||||
// Check bounds and that the list elements are the same
|
||||
while (_internalList.isNotEmpty &&
|
||||
_list.length > 0 &&
|
||||
index < _internalList.length &&
|
||||
index < _list.length &&
|
||||
_internalList[index] == _list[index]) {
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildAnimatedList() {
|
||||
return AnimatedList(
|
||||
key: _listKey,
|
||||
shrinkWrap: true,
|
||||
itemBuilder: _buildThumbnail,
|
||||
initialItemCount: _list.length,
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const NeverScrollableScrollPhysics(), // Cart shouldn't scroll
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_updateLists();
|
||||
return ScopedModelDescendant<AppStateModel>(
|
||||
builder: (BuildContext context, Widget child, AppStateModel model) => _buildAnimatedList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ExtraProductsNumber extends StatelessWidget {
|
||||
// Calculates the number to be displayed at the end of the row if there are
|
||||
// more than three products in the cart. This calculates overflow products,
|
||||
// including their duplicates (but not duplicates of products shown as
|
||||
// thumbnails).
|
||||
int _calculateOverflow(AppStateModel model) {
|
||||
final Map<int, int> productMap = model.productsInCart;
|
||||
// List created to be able to access products by index instead of ID.
|
||||
// Order is guaranteed because productsInCart returns a LinkedHashMap.
|
||||
final List<int> products = productMap.keys.toList();
|
||||
int overflow = 0;
|
||||
final int numProducts = products.length;
|
||||
if (numProducts > 3) {
|
||||
for (int i = 3; i < numProducts; i++) {
|
||||
overflow += productMap[products[i]];
|
||||
}
|
||||
}
|
||||
return overflow;
|
||||
}
|
||||
|
||||
Widget _buildOverflow(AppStateModel model, BuildContext context) {
|
||||
if (model.productsInCart.length <= 3)
|
||||
return Container();
|
||||
|
||||
final int numOverflowProducts = _calculateOverflow(model);
|
||||
// Maximum of 99 so padding doesn't get messy.
|
||||
final int displayedOverflowProducts = numOverflowProducts <= 99 ? numOverflowProducts : 99;
|
||||
return Container(
|
||||
child: Text(
|
||||
'+$displayedOverflowProducts',
|
||||
style: Theme.of(context).primaryTextTheme.button,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ScopedModelDescendant<AppStateModel>(
|
||||
builder: (BuildContext builder, Widget child, AppStateModel model) => _buildOverflow(model, context),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ProductThumbnail extends StatelessWidget {
|
||||
const ProductThumbnail(this.animation, this.opacityAnimation, this.product);
|
||||
|
||||
final Animation<double> animation;
|
||||
final Animation<double> opacityAnimation;
|
||||
final Product product;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FadeTransition(
|
||||
opacity: opacityAnimation,
|
||||
child: ScaleTransition(
|
||||
scale: animation,
|
||||
child: Container(
|
||||
width: 40.0,
|
||||
height: 40.0,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: ExactAssetImage(
|
||||
product.assetName, // asset name
|
||||
package: product.assetPackage, // asset package
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10.0)),
|
||||
),
|
||||
margin: const EdgeInsets.only(left: 16.0),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// _ListModel manipulates an internal list and an AnimatedList
|
||||
class _ListModel {
|
||||
_ListModel({
|
||||
@required this.listKey,
|
||||
@required this.removedItemBuilder,
|
||||
Iterable<int> initialItems,
|
||||
}) : assert(listKey != null),
|
||||
assert(removedItemBuilder != null),
|
||||
_items = initialItems?.toList() ?? <int>[];
|
||||
|
||||
final GlobalKey<AnimatedListState> listKey;
|
||||
final dynamic removedItemBuilder;
|
||||
final List<int> _items;
|
||||
|
||||
AnimatedListState get _animatedList => listKey.currentState;
|
||||
|
||||
void add(int product) {
|
||||
_insert(_items.length, product);
|
||||
}
|
||||
|
||||
void _insert(int index, int item) {
|
||||
_items.insert(index, item);
|
||||
_animatedList.insertItem(index, duration: const Duration(milliseconds: 225));
|
||||
}
|
||||
|
||||
void remove(int product) {
|
||||
final int index = _items.indexOf(product);
|
||||
if (index >= 0) {
|
||||
_removeAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
void _removeAt(int index) {
|
||||
final int removedItem = _items.removeAt(index);
|
||||
if (removedItem != null) {
|
||||
_animatedList.removeItem(index, (BuildContext context, Animation<double> animation) {
|
||||
return removedItemBuilder(removedItem, context, animation);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
int get length => _items.length;
|
||||
|
||||
int operator [](int index) => _items[index];
|
||||
|
||||
int indexOf(int item) => _items.indexOf(item);
|
||||
|
||||
List<int> get list => _items;
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
// Copyright 2018-present the Flutter authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:scoped_model/scoped_model.dart';
|
||||
|
||||
import 'package:flutter_gallery/demo/shrine/backdrop.dart';
|
||||
import 'package:flutter_gallery/demo/shrine/expanding_bottom_sheet.dart';
|
||||
import 'package:flutter_gallery/demo/shrine/model/app_state_model.dart';
|
||||
import 'package:flutter_gallery/demo/shrine/model/product.dart';
|
||||
import 'package:flutter_gallery/demo/shrine/supplemental/asymmetric_view.dart';
|
||||
|
||||
class ProductPage extends StatelessWidget {
|
||||
const ProductPage({this.category = Category.all});
|
||||
|
||||
final Category category;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ScopedModelDescendant<AppStateModel>(
|
||||
builder: (BuildContext context, Widget child, AppStateModel model) {
|
||||
return AsymmetricView(products: model.getProducts());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class HomePage extends StatelessWidget {
|
||||
const HomePage({
|
||||
this.expandingBottomSheet,
|
||||
this.backdrop,
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
|
||||
final ExpandingBottomSheet expandingBottomSheet;
|
||||
final Backdrop backdrop;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
backdrop,
|
||||
Align(child: expandingBottomSheet, alignment: Alignment.bottomRight),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,144 +0,0 @@
|
||||
// Copyright 2018-present the Flutter authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gallery/demo/shrine/colors.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
@override
|
||||
_LoginPageState createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
final TextEditingController _usernameController = TextEditingController();
|
||||
final TextEditingController _passwordController = TextEditingController();
|
||||
static const ShapeDecoration _decoration = ShapeDecoration(
|
||||
shape: BeveledRectangleBorder(
|
||||
side: BorderSide(color: kShrineBrown900, width: 0.5),
|
||||
borderRadius: BorderRadius.all(Radius.circular(7.0)),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: 0.0,
|
||||
backgroundColor: Colors.white,
|
||||
brightness: Brightness.light,
|
||||
leading: IconButton(
|
||||
icon: const BackButtonIcon(),
|
||||
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
||||
onPressed: () {
|
||||
// The login screen is immediately displayed on top of the Shrine
|
||||
// home screen using onGenerateRoute and so rootNavigator must be
|
||||
// set to true in order to get out of Shrine completely.
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
children: <Widget>[
|
||||
const SizedBox(height: 80.0),
|
||||
Column(
|
||||
children: <Widget>[
|
||||
Image.asset('packages/shrine_images/diamond.png'),
|
||||
const SizedBox(height: 16.0),
|
||||
Text(
|
||||
'SHRINE',
|
||||
style: Theme.of(context).textTheme.headline,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 120.0),
|
||||
PrimaryColorOverride(
|
||||
color: kShrineBrown900,
|
||||
child: Container(
|
||||
decoration: _decoration,
|
||||
child: TextField(
|
||||
controller: _usernameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Username',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12.0),
|
||||
PrimaryColorOverride(
|
||||
color: kShrineBrown900,
|
||||
child: Container(
|
||||
decoration: _decoration,
|
||||
child: TextField(
|
||||
controller: _passwordController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Password',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Wrap(
|
||||
children: <Widget>[
|
||||
ButtonBar(
|
||||
children: <Widget>[
|
||||
FlatButton(
|
||||
child: const Text('CANCEL'),
|
||||
shape: const BeveledRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(7.0)),
|
||||
),
|
||||
onPressed: () {
|
||||
// The login screen is immediately displayed on top of
|
||||
// the Shrine home screen using onGenerateRoute and so
|
||||
// rootNavigator must be set to true in order to get out
|
||||
// of Shrine completely.
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
},
|
||||
),
|
||||
RaisedButton(
|
||||
child: const Text('NEXT'),
|
||||
elevation: 8.0,
|
||||
shape: const BeveledRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(7.0)),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PrimaryColorOverride extends StatelessWidget {
|
||||
const PrimaryColorOverride({Key key, this.color, this.child}) : super(key: key);
|
||||
|
||||
final Color color;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
child: child,
|
||||
data: Theme.of(context).copyWith(primaryColor: color),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,123 +0,0 @@
|
||||
// Copyright 2018-present the Flutter authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:scoped_model/scoped_model.dart';
|
||||
|
||||
import 'package:flutter_gallery/demo/shrine/model/product.dart';
|
||||
import 'package:flutter_gallery/demo/shrine/model/products_repository.dart';
|
||||
|
||||
double _salesTaxRate = 0.06;
|
||||
double _shippingCostPerItem = 7.0;
|
||||
|
||||
class AppStateModel extends Model {
|
||||
// All the available products.
|
||||
List<Product> _availableProducts;
|
||||
|
||||
// The currently selected category of products.
|
||||
Category _selectedCategory = Category.all;
|
||||
|
||||
// The IDs and quantities of products currently in the cart.
|
||||
final Map<int, int> _productsInCart = <int, int>{};
|
||||
|
||||
Map<int, int> get productsInCart => Map<int, int>.from(_productsInCart);
|
||||
|
||||
// Total number of items in the cart.
|
||||
int get totalCartQuantity => _productsInCart.values.fold(0, (int v, int e) => v + e);
|
||||
|
||||
Category get selectedCategory => _selectedCategory;
|
||||
|
||||
// Totaled prices of the items in the cart.
|
||||
double get subtotalCost {
|
||||
return _productsInCart.keys
|
||||
.map((int id) => _availableProducts[id].price * _productsInCart[id])
|
||||
.fold(0.0, (double sum, int e) => sum + e);
|
||||
}
|
||||
|
||||
// Total shipping cost for the items in the cart.
|
||||
double get shippingCost {
|
||||
return _shippingCostPerItem * _productsInCart.values.fold(0.0, (num sum, int e) => sum + e);
|
||||
}
|
||||
|
||||
// Sales tax for the items in the cart
|
||||
double get tax => subtotalCost * _salesTaxRate;
|
||||
|
||||
// Total cost to order everything in the cart.
|
||||
double get totalCost => subtotalCost + shippingCost + tax;
|
||||
|
||||
// Returns a copy of the list of available products, filtered by category.
|
||||
List<Product> getProducts() {
|
||||
if (_availableProducts == null) {
|
||||
return <Product>[];
|
||||
}
|
||||
|
||||
if (_selectedCategory == Category.all) {
|
||||
return List<Product>.from(_availableProducts);
|
||||
} else {
|
||||
return _availableProducts
|
||||
.where((Product p) => p.category == _selectedCategory)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a product to the cart.
|
||||
void addProductToCart(int productId) {
|
||||
if (!_productsInCart.containsKey(productId)) {
|
||||
_productsInCart[productId] = 1;
|
||||
} else {
|
||||
_productsInCart[productId]++;
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Removes an item from the cart.
|
||||
void removeItemFromCart(int productId) {
|
||||
if (_productsInCart.containsKey(productId)) {
|
||||
if (_productsInCart[productId] == 1) {
|
||||
_productsInCart.remove(productId);
|
||||
} else {
|
||||
_productsInCart[productId]--;
|
||||
}
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Returns the Product instance matching the provided id.
|
||||
Product getProductById(int id) {
|
||||
return _availableProducts.firstWhere((Product p) => p.id == id);
|
||||
}
|
||||
|
||||
// Removes everything from the cart.
|
||||
void clearCart() {
|
||||
_productsInCart.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Loads the list of available products from the repo.
|
||||
void loadProducts() {
|
||||
_availableProducts = ProductsRepository.loadProducts(Category.all);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setCategory(Category newCategory) {
|
||||
_selectedCategory = newCategory;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AppStateModel(totalCost: $totalCost)';
|
||||
}
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
// Copyright 2018-present the Flutter authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
enum Category {
|
||||
all,
|
||||
accessories,
|
||||
clothing,
|
||||
home,
|
||||
}
|
||||
|
||||
class Product {
|
||||
const Product({
|
||||
@required this.category,
|
||||
@required this.id,
|
||||
@required this.isFeatured,
|
||||
@required this.name,
|
||||
@required this.price,
|
||||
}) : assert(category != null),
|
||||
assert(id != null),
|
||||
assert(isFeatured != null),
|
||||
assert(name != null),
|
||||
assert(price != null);
|
||||
|
||||
final Category category;
|
||||
final int id;
|
||||
final bool isFeatured;
|
||||
final String name;
|
||||
final int price;
|
||||
|
||||
String get assetName => '$id-0.jpg';
|
||||
String get assetPackage => 'shrine_images';
|
||||
|
||||
@override
|
||||
String toString() => '$name (id=$id)';
|
||||
}
|
||||
@ -1,293 +0,0 @@
|
||||
// Copyright 2018-present the Flutter authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:flutter_gallery/demo/shrine/model/product.dart';
|
||||
|
||||
class ProductsRepository {
|
||||
static List<Product> loadProducts(Category category) {
|
||||
const List<Product> allProducts = <Product>[
|
||||
Product(
|
||||
category: Category.accessories,
|
||||
id: 0,
|
||||
isFeatured: true,
|
||||
name: 'Vagabond sack',
|
||||
price: 120,
|
||||
),
|
||||
Product(
|
||||
category: Category.accessories,
|
||||
id: 1,
|
||||
isFeatured: true,
|
||||
name: 'Stella sunglasses',
|
||||
price: 58,
|
||||
),
|
||||
Product(
|
||||
category: Category.accessories,
|
||||
id: 2,
|
||||
isFeatured: false,
|
||||
name: 'Whitney belt',
|
||||
price: 35,
|
||||
),
|
||||
Product(
|
||||
category: Category.accessories,
|
||||
id: 3,
|
||||
isFeatured: true,
|
||||
name: 'Garden strand',
|
||||
price: 98,
|
||||
),
|
||||
Product(
|
||||
category: Category.accessories,
|
||||
id: 4,
|
||||
isFeatured: false,
|
||||
name: 'Strut earrings',
|
||||
price: 34,
|
||||
),
|
||||
Product(
|
||||
category: Category.accessories,
|
||||
id: 5,
|
||||
isFeatured: false,
|
||||
name: 'Varsity socks',
|
||||
price: 12,
|
||||
),
|
||||
Product(
|
||||
category: Category.accessories,
|
||||
id: 6,
|
||||
isFeatured: false,
|
||||
name: 'Weave keyring',
|
||||
price: 16,
|
||||
),
|
||||
Product(
|
||||
category: Category.accessories,
|
||||
id: 7,
|
||||
isFeatured: true,
|
||||
name: 'Gatsby hat',
|
||||
price: 40,
|
||||
),
|
||||
Product(
|
||||
category: Category.accessories,
|
||||
id: 8,
|
||||
isFeatured: true,
|
||||
name: 'Shrug bag',
|
||||
price: 198,
|
||||
),
|
||||
Product(
|
||||
category: Category.home,
|
||||
id: 9,
|
||||
isFeatured: true,
|
||||
name: 'Gilt desk trio',
|
||||
price: 58,
|
||||
),
|
||||
Product(
|
||||
category: Category.home,
|
||||
id: 10,
|
||||
isFeatured: false,
|
||||
name: 'Copper wire rack',
|
||||
price: 18,
|
||||
),
|
||||
Product(
|
||||
category: Category.home,
|
||||
id: 11,
|
||||
isFeatured: false,
|
||||
name: 'Soothe ceramic set',
|
||||
price: 28,
|
||||
),
|
||||
Product(
|
||||
category: Category.home,
|
||||
id: 12,
|
||||
isFeatured: false,
|
||||
name: 'Hurrahs tea set',
|
||||
price: 34,
|
||||
),
|
||||
Product(
|
||||
category: Category.home,
|
||||
id: 13,
|
||||
isFeatured: true,
|
||||
name: 'Blue stone mug',
|
||||
price: 18,
|
||||
),
|
||||
Product(
|
||||
category: Category.home,
|
||||
id: 14,
|
||||
isFeatured: true,
|
||||
name: 'Rainwater tray',
|
||||
price: 27,
|
||||
),
|
||||
Product(
|
||||
category: Category.home,
|
||||
id: 15,
|
||||
isFeatured: true,
|
||||
name: 'Chambray napkins',
|
||||
price: 16,
|
||||
),
|
||||
Product(
|
||||
category: Category.home,
|
||||
id: 16,
|
||||
isFeatured: true,
|
||||
name: 'Succulent planters',
|
||||
price: 16,
|
||||
),
|
||||
Product(
|
||||
category: Category.home,
|
||||
id: 17,
|
||||
isFeatured: false,
|
||||
name: 'Quartet table',
|
||||
price: 175,
|
||||
),
|
||||
Product(
|
||||
category: Category.home,
|
||||
id: 18,
|
||||
isFeatured: true,
|
||||
name: 'Kitchen quattro',
|
||||
price: 129,
|
||||
),
|
||||
Product(
|
||||
category: Category.clothing,
|
||||
id: 19,
|
||||
isFeatured: false,
|
||||
name: 'Clay sweater',
|
||||
price: 48,
|
||||
),
|
||||
Product(
|
||||
category: Category.clothing,
|
||||
id: 20,
|
||||
isFeatured: false,
|
||||
name: 'Sea tunic',
|
||||
price: 45,
|
||||
),
|
||||
Product(
|
||||
category: Category.clothing,
|
||||
id: 21,
|
||||
isFeatured: false,
|
||||
name: 'Plaster tunic',
|
||||
price: 38,
|
||||
),
|
||||
Product(
|
||||
category: Category.clothing,
|
||||
id: 22,
|
||||
isFeatured: false,
|
||||
name: 'White pinstripe shirt',
|
||||
price: 70,
|
||||
),
|
||||
Product(
|
||||
category: Category.clothing,
|
||||
id: 23,
|
||||
isFeatured: false,
|
||||
name: 'Chambray shirt',
|
||||
price: 70,
|
||||
),
|
||||
Product(
|
||||
category: Category.clothing,
|
||||
id: 24,
|
||||
isFeatured: true,
|
||||
name: 'Seabreeze sweater',
|
||||
price: 60,
|
||||
),
|
||||
Product(
|
||||
category: Category.clothing,
|
||||
id: 25,
|
||||
isFeatured: false,
|
||||
name: 'Gentry jacket',
|
||||
price: 178,
|
||||
),
|
||||
Product(
|
||||
category: Category.clothing,
|
||||
id: 26,
|
||||
isFeatured: false,
|
||||
name: 'Navy trousers',
|
||||
price: 74,
|
||||
),
|
||||
Product(
|
||||
category: Category.clothing,
|
||||
id: 27,
|
||||
isFeatured: true,
|
||||
name: 'Walter henley (white)',
|
||||
price: 38,
|
||||
),
|
||||
Product(
|
||||
category: Category.clothing,
|
||||
id: 28,
|
||||
isFeatured: true,
|
||||
name: 'Surf and perf shirt',
|
||||
price: 48,
|
||||
),
|
||||
Product(
|
||||
category: Category.clothing,
|
||||
id: 29,
|
||||
isFeatured: true,
|
||||
name: 'Ginger scarf',
|
||||
price: 98,
|
||||
),
|
||||
Product(
|
||||
category: Category.clothing,
|
||||
id: 30,
|
||||
isFeatured: true,
|
||||
name: 'Ramona crossover',
|
||||
price: 68,
|
||||
),
|
||||
Product(
|
||||
category: Category.clothing,
|
||||
id: 31,
|
||||
isFeatured: false,
|
||||
name: 'Chambray shirt',
|
||||
price: 38,
|
||||
),
|
||||
Product(
|
||||
category: Category.clothing,
|
||||
id: 32,
|
||||
isFeatured: false,
|
||||
name: 'Classic white collar',
|
||||
price: 58,
|
||||
),
|
||||
Product(
|
||||
category: Category.clothing,
|
||||
id: 33,
|
||||
isFeatured: true,
|
||||
name: 'Cerise scallop tee',
|
||||
price: 42,
|
||||
),
|
||||
Product(
|
||||
category: Category.clothing,
|
||||
id: 34,
|
||||
isFeatured: false,
|
||||
name: 'Shoulder rolls tee',
|
||||
price: 27,
|
||||
),
|
||||
Product(
|
||||
category: Category.clothing,
|
||||
id: 35,
|
||||
isFeatured: false,
|
||||
name: 'Grey slouch tank',
|
||||
price: 24,
|
||||
),
|
||||
Product(
|
||||
category: Category.clothing,
|
||||
id: 36,
|
||||
isFeatured: false,
|
||||
name: 'Sunshirt dress',
|
||||
price: 58,
|
||||
),
|
||||
Product(
|
||||
category: Category.clothing,
|
||||
id: 37,
|
||||
isFeatured: true,
|
||||
name: 'Fine lines tee',
|
||||
price: 58,
|
||||
),
|
||||
];
|
||||
if (category == Category.all) {
|
||||
return allProducts;
|
||||
} else {
|
||||
return allProducts.where((Product p) => p.category == category).toList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,275 +0,0 @@
|
||||
// Copyright 2018-present the Flutter authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:scoped_model/scoped_model.dart';
|
||||
|
||||
import 'package:flutter_gallery/demo/shrine/colors.dart';
|
||||
import 'package:flutter_gallery/demo/shrine/expanding_bottom_sheet.dart';
|
||||
import 'package:flutter_gallery/demo/shrine/model/app_state_model.dart';
|
||||
import 'package:flutter_gallery/demo/shrine/model/product.dart';
|
||||
|
||||
const double _leftColumnWidth = 60.0;
|
||||
|
||||
class ShoppingCartPage extends StatefulWidget {
|
||||
@override
|
||||
_ShoppingCartPageState createState() => _ShoppingCartPageState();
|
||||
}
|
||||
|
||||
class _ShoppingCartPageState extends State<ShoppingCartPage> {
|
||||
List<Widget> _createShoppingCartRows(AppStateModel model) {
|
||||
return model.productsInCart.keys
|
||||
.map((int id) => ShoppingCartRow(
|
||||
product: model.getProductById(id),
|
||||
quantity: model.productsInCart[id],
|
||||
onPressed: () {
|
||||
model.removeItemFromCart(id);
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData localTheme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: kShrinePink50,
|
||||
body: SafeArea(
|
||||
child: Container(
|
||||
child: ScopedModelDescendant<AppStateModel>(
|
||||
builder: (BuildContext context, Widget child, AppStateModel model) {
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
ListView(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
width: _leftColumnWidth,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.keyboard_arrow_down),
|
||||
onPressed: () => ExpandingBottomSheet.of(context).close(),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'CART',
|
||||
style: localTheme.textTheme.subhead.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(width: 16.0),
|
||||
Text('${model.totalCartQuantity} ITEMS'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
Column(
|
||||
children: _createShoppingCartRows(model),
|
||||
),
|
||||
ShoppingCartSummary(model: model),
|
||||
const SizedBox(height: 100.0),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
bottom: 16.0,
|
||||
left: 16.0,
|
||||
right: 16.0,
|
||||
child: RaisedButton(
|
||||
shape: const BeveledRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(7.0)),
|
||||
),
|
||||
color: kShrinePink100,
|
||||
splashColor: kShrineBrown600,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: Text('CLEAR CART'),
|
||||
),
|
||||
onPressed: () {
|
||||
model.clearCart();
|
||||
ExpandingBottomSheet.of(context).close();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShoppingCartSummary extends StatelessWidget {
|
||||
const ShoppingCartSummary({this.model});
|
||||
|
||||
final AppStateModel model;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TextStyle smallAmountStyle = Theme.of(context).textTheme.body1.copyWith(color: kShrineBrown600);
|
||||
final TextStyle largeAmountStyle = Theme.of(context).textTheme.display1;
|
||||
final NumberFormat formatter = NumberFormat.simpleCurrency(
|
||||
decimalDigits: 2,
|
||||
locale: Localizations.localeOf(context).toString(),
|
||||
);
|
||||
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
const SizedBox(width: _leftColumnWidth),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 16.0),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Expanded(
|
||||
child: Text('TOTAL'),
|
||||
),
|
||||
Text(
|
||||
formatter.format(model.totalCost),
|
||||
style: largeAmountStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
const Expanded(
|
||||
child: Text('Subtotal:'),
|
||||
),
|
||||
Text(
|
||||
formatter.format(model.subtotalCost),
|
||||
style: smallAmountStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4.0),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
const Expanded(
|
||||
child: Text('Shipping:'),
|
||||
),
|
||||
Text(
|
||||
formatter.format(model.shippingCost),
|
||||
style: smallAmountStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4.0),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
const Expanded(
|
||||
child: Text('Tax:'),
|
||||
),
|
||||
Text(
|
||||
formatter.format(model.tax),
|
||||
style: smallAmountStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShoppingCartRow extends StatelessWidget {
|
||||
const ShoppingCartRow({
|
||||
@required this.product,
|
||||
@required this.quantity,
|
||||
this.onPressed,
|
||||
});
|
||||
|
||||
final Product product;
|
||||
final int quantity;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final NumberFormat formatter = NumberFormat.simpleCurrency(
|
||||
decimalDigits: 0,
|
||||
locale: Localizations.localeOf(context).toString(),
|
||||
);
|
||||
final ThemeData localTheme = Theme.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Row(
|
||||
key: ValueKey<int>(product.id),
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
width: _leftColumnWidth,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.remove_circle_outline),
|
||||
onPressed: onPressed,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 16.0),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Image.asset(
|
||||
product.assetName,
|
||||
package: product.assetPackage,
|
||||
fit: BoxFit.cover,
|
||||
width: 75.0,
|
||||
height: 75.0,
|
||||
),
|
||||
const SizedBox(width: 16.0),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text('Quantity: $quantity'),
|
||||
),
|
||||
Text('x ${formatter.format(product.price)}'),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
product.name,
|
||||
style: localTheme.textTheme.subhead.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
const Divider(
|
||||
color: kShrineBrown900,
|
||||
height: 10.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,95 +0,0 @@
|
||||
// Copyright 2018-present the Flutter authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gallery/demo/shrine/model/product.dart';
|
||||
import 'package:flutter_gallery/demo/shrine/supplemental/product_columns.dart';
|
||||
|
||||
class AsymmetricView extends StatelessWidget {
|
||||
const AsymmetricView({Key key, this.products}) : super(key: key);
|
||||
|
||||
final List<Product> products;
|
||||
|
||||
List<Container> _buildColumns(BuildContext context) {
|
||||
if (products == null || products.isEmpty) {
|
||||
return const <Container>[];
|
||||
}
|
||||
|
||||
// This will return a list of columns. It will oscillate between the two
|
||||
// kinds of columns. Even cases of the index (0, 2, 4, etc) will be
|
||||
// TwoProductCardColumn and the odd cases will be OneProductCardColumn.
|
||||
//
|
||||
// Each pair of columns will advance us 3 products forward (2 + 1). That's
|
||||
// some kinda awkward math so we use _evenCasesIndex and _oddCasesIndex as
|
||||
// helpers for creating the index of the product list that will correspond
|
||||
// to the index of the list of columns.
|
||||
return List<Container>.generate(_listItemCount(products.length), (int index) {
|
||||
double width = .59 * MediaQuery.of(context).size.width;
|
||||
Widget column;
|
||||
if (index % 2 == 0) {
|
||||
/// Even cases
|
||||
final int bottom = _evenCasesIndex(index);
|
||||
column = TwoProductCardColumn(
|
||||
bottom: products[bottom],
|
||||
top: products.length - 1 >= bottom + 1
|
||||
? products[bottom + 1]
|
||||
: null,
|
||||
);
|
||||
width += 32.0;
|
||||
} else {
|
||||
/// Odd cases
|
||||
column = OneProductCardColumn(
|
||||
product: products[_oddCasesIndex(index)],
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
width: width,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: column,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
int _evenCasesIndex(int input) {
|
||||
// The operator ~/ is a cool one. It's the truncating division operator. It
|
||||
// divides the number and if there's a remainder / decimal, it cuts it off.
|
||||
// This is like dividing and then casting the result to int. Also, it's
|
||||
// functionally equivalent to floor() in this case.
|
||||
return input ~/ 2 * 3;
|
||||
}
|
||||
|
||||
int _oddCasesIndex(int input) {
|
||||
assert(input > 0);
|
||||
return (input / 2).ceil() * 3 - 1;
|
||||
}
|
||||
|
||||
int _listItemCount(int totalItems) {
|
||||
return (totalItems % 3 == 0)
|
||||
? totalItems ~/ 3 * 2
|
||||
: (totalItems / 3).ceil() * 2 - 1;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.fromLTRB(0.0, 34.0, 16.0, 44.0),
|
||||
children: _buildColumns(context),
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,134 +0,0 @@
|
||||
// Copyright 2018-present the Flutter authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'dart:ui' show lerpDouble;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class CutCornersBorder extends OutlineInputBorder {
|
||||
const CutCornersBorder({
|
||||
BorderSide borderSide = BorderSide.none,
|
||||
BorderRadius borderRadius = const BorderRadius.all(Radius.circular(2.0)),
|
||||
this.cut = 7.0,
|
||||
double gapPadding = 2.0,
|
||||
}) : super(
|
||||
borderSide: borderSide,
|
||||
borderRadius: borderRadius,
|
||||
gapPadding: gapPadding,
|
||||
);
|
||||
|
||||
@override
|
||||
CutCornersBorder copyWith({
|
||||
BorderSide borderSide,
|
||||
BorderRadius borderRadius,
|
||||
double gapPadding,
|
||||
double cut,
|
||||
}) {
|
||||
return CutCornersBorder(
|
||||
borderSide: borderSide ?? this.borderSide,
|
||||
borderRadius: borderRadius ?? this.borderRadius,
|
||||
gapPadding: gapPadding ?? this.gapPadding,
|
||||
cut: cut ?? this.cut,
|
||||
);
|
||||
}
|
||||
|
||||
final double cut;
|
||||
|
||||
@override
|
||||
ShapeBorder lerpFrom(ShapeBorder a, double t) {
|
||||
if (a is CutCornersBorder) {
|
||||
final CutCornersBorder outline = a;
|
||||
return CutCornersBorder(
|
||||
borderRadius: BorderRadius.lerp(outline.borderRadius, borderRadius, t),
|
||||
borderSide: BorderSide.lerp(outline.borderSide, borderSide, t),
|
||||
cut: cut,
|
||||
gapPadding: outline.gapPadding,
|
||||
);
|
||||
}
|
||||
return super.lerpFrom(a, t);
|
||||
}
|
||||
|
||||
@override
|
||||
ShapeBorder lerpTo(ShapeBorder b, double t) {
|
||||
if (b is CutCornersBorder) {
|
||||
final CutCornersBorder outline = b;
|
||||
return CutCornersBorder(
|
||||
borderRadius: BorderRadius.lerp(borderRadius, outline.borderRadius, t),
|
||||
borderSide: BorderSide.lerp(borderSide, outline.borderSide, t),
|
||||
cut: cut,
|
||||
gapPadding: outline.gapPadding,
|
||||
);
|
||||
}
|
||||
return super.lerpTo(b, t);
|
||||
}
|
||||
|
||||
Path _notchedCornerPath(Rect center, [double start = 0.0, double extent = 0.0]) {
|
||||
final Path path = Path();
|
||||
if (start > 0.0 || extent > 0.0) {
|
||||
path.relativeMoveTo(extent + start, center.top);
|
||||
_notchedSidesAndBottom(center, path);
|
||||
path..lineTo(center.left + cut, center.top)..lineTo(start, center.top);
|
||||
} else {
|
||||
path.moveTo(center.left + cut, center.top);
|
||||
_notchedSidesAndBottom(center, path);
|
||||
path.lineTo(center.left + cut, center.top);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
Path _notchedSidesAndBottom(Rect center, Path path) {
|
||||
return path
|
||||
..lineTo(center.right - cut, center.top)
|
||||
..lineTo(center.right, center.top + cut)
|
||||
..lineTo(center.right, center.top + center.height - cut)
|
||||
..lineTo(center.right - cut, center.top + center.height)
|
||||
..lineTo(center.left + cut, center.top + center.height)
|
||||
..lineTo(center.left, center.top + center.height - cut)
|
||||
..lineTo(center.left, center.top + cut);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(
|
||||
Canvas canvas,
|
||||
Rect rect, {
|
||||
double gapStart,
|
||||
double gapExtent = 0.0,
|
||||
double gapPercentage = 0.0,
|
||||
TextDirection textDirection,
|
||||
}) {
|
||||
assert(gapExtent != null);
|
||||
assert(gapPercentage >= 0.0 && gapPercentage <= 1.0);
|
||||
|
||||
final Paint paint = borderSide.toPaint();
|
||||
final RRect outer = borderRadius.toRRect(rect);
|
||||
if (gapStart == null || gapExtent <= 0.0 || gapPercentage == 0.0) {
|
||||
canvas.drawPath(_notchedCornerPath(outer.middleRect), paint);
|
||||
} else {
|
||||
final double extent = lerpDouble(0.0, gapExtent + gapPadding * 2.0, gapPercentage);
|
||||
switch (textDirection) {
|
||||
case TextDirection.rtl: {
|
||||
final Path path = _notchedCornerPath(outer.middleRect, gapStart + gapPadding - extent, extent);
|
||||
canvas.drawPath(path, paint);
|
||||
break;
|
||||
}
|
||||
case TextDirection.ltr: {
|
||||
final Path path = _notchedCornerPath(outer.middleRect, gapStart - gapPadding, extent);
|
||||
canvas.drawPath(path, paint);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,97 +0,0 @@
|
||||
// Copyright 2018-present the Flutter authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:scoped_model/scoped_model.dart';
|
||||
|
||||
import 'package:flutter_gallery/demo/shrine/model/app_state_model.dart';
|
||||
import 'package:flutter_gallery/demo/shrine/model/product.dart';
|
||||
|
||||
class ProductCard extends StatelessWidget {
|
||||
const ProductCard({ this.imageAspectRatio = 33 / 49, this.product })
|
||||
: assert(imageAspectRatio == null || imageAspectRatio > 0);
|
||||
|
||||
final double imageAspectRatio;
|
||||
final Product product;
|
||||
|
||||
static const double kTextBoxHeight = 65.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final NumberFormat formatter = NumberFormat.simpleCurrency(
|
||||
decimalDigits: 0,
|
||||
locale: Localizations.localeOf(context).toString(),
|
||||
);
|
||||
|
||||
final ThemeData theme = Theme.of(context);
|
||||
|
||||
final Image imageWidget = Image.asset(
|
||||
product.assetName,
|
||||
package: product.assetPackage,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
|
||||
return ScopedModelDescendant<AppStateModel>(
|
||||
builder: (BuildContext context, Widget child, AppStateModel model) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
model.addProductToCart(product.id);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
AspectRatio(
|
||||
aspectRatio: imageAspectRatio,
|
||||
child: imageWidget,
|
||||
),
|
||||
SizedBox(
|
||||
height: kTextBoxHeight * MediaQuery.of(context).textScaleFactor,
|
||||
width: 121.0,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
product == null ? '' : product.name,
|
||||
style: theme.textTheme.button,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
const SizedBox(height: 4.0),
|
||||
Text(
|
||||
product == null ? '' : formatter.format(product.price),
|
||||
style: theme.textTheme.caption,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Icon(Icons.add_shopping_cart),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,89 +0,0 @@
|
||||
// Copyright 2018-present the Flutter authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gallery/demo/shrine/model/product.dart';
|
||||
import 'package:flutter_gallery/demo/shrine/supplemental/product_card.dart';
|
||||
|
||||
class TwoProductCardColumn extends StatelessWidget {
|
||||
const TwoProductCardColumn({
|
||||
@required this.bottom,
|
||||
this.top,
|
||||
}) : assert(bottom != null);
|
||||
|
||||
final Product bottom, top;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
|
||||
const double spacerHeight = 44.0;
|
||||
|
||||
final double heightOfCards = (constraints.biggest.height - spacerHeight) / 2.0;
|
||||
final double availableHeightForImages = heightOfCards - ProductCard.kTextBoxHeight;
|
||||
// Ensure the cards take up the available space as long as the screen is
|
||||
// sufficiently tall, otherwise fallback on a constant aspect ratio.
|
||||
final double imageAspectRatio = availableHeightForImages >= 0.0
|
||||
? constraints.biggest.width / availableHeightForImages
|
||||
: 49.0 / 33.0;
|
||||
|
||||
return ListView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: 28.0),
|
||||
child: top != null
|
||||
? ProductCard(
|
||||
imageAspectRatio: imageAspectRatio,
|
||||
product: top,
|
||||
)
|
||||
: SizedBox(
|
||||
height: heightOfCards > 0 ? heightOfCards : spacerHeight,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: spacerHeight),
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 28.0),
|
||||
child: ProductCard(
|
||||
imageAspectRatio: imageAspectRatio,
|
||||
product: bottom,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class OneProductCardColumn extends StatelessWidget {
|
||||
const OneProductCardColumn({this.product});
|
||||
|
||||
final Product product;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
reverse: true,
|
||||
children: <Widget>[
|
||||
const SizedBox(
|
||||
height: 40.0,
|
||||
),
|
||||
ProductCard(
|
||||
product: product,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gallery/demo/shrine/app.dart';
|
||||
|
||||
class ShrineDemo extends StatelessWidget {
|
||||
const ShrineDemo({ Key key }) : super(key: key);
|
||||
|
||||
static const String routeName = '/shrine'; // Used by the Gallery app.
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ShrineApp();
|
||||
}
|
||||
@ -1,194 +0,0 @@
|
||||
// Copyright 2014 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:ui' show Vertices;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'transformations_demo_board.dart';
|
||||
import 'transformations_demo_edit_board_point.dart';
|
||||
import 'transformations_demo_gesture_transformable.dart';
|
||||
|
||||
class TransformationsDemo extends StatefulWidget {
|
||||
const TransformationsDemo({ Key key }) : super(key: key);
|
||||
|
||||
static const String routeName = '/transformations';
|
||||
|
||||
@override _TransformationsDemoState createState() => _TransformationsDemoState();
|
||||
}
|
||||
class _TransformationsDemoState extends State<TransformationsDemo> {
|
||||
// The radius of a hexagon tile in pixels.
|
||||
static const double _kHexagonRadius = 32.0;
|
||||
// The margin between hexagons.
|
||||
static const double _kHexagonMargin = 1.0;
|
||||
// The radius of the entire board in hexagons, not including the center.
|
||||
static const int _kBoardRadius = 8;
|
||||
|
||||
bool _reset = false;
|
||||
Board _board = Board(
|
||||
boardRadius: _kBoardRadius,
|
||||
hexagonRadius: _kHexagonRadius,
|
||||
hexagonMargin: _kHexagonMargin,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build (BuildContext context) {
|
||||
final BoardPainter painter = BoardPainter(
|
||||
board: _board,
|
||||
);
|
||||
|
||||
// The scene is drawn by a CustomPaint, but user interaction is handled by
|
||||
// the GestureTransformable parent widget.
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('2D Tranformations'),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.help),
|
||||
tooltip: 'Help',
|
||||
onPressed: () {
|
||||
showDialog<Column>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => instructionDialog,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
// Draw the scene as big as is available, but allow the user to
|
||||
// translate beyond that to a visibleSize that's a bit bigger.
|
||||
final Size size = Size(constraints.maxWidth, constraints.maxHeight);
|
||||
final Size visibleSize = Size(size.width * 3, size.height * 2);
|
||||
return GestureTransformable(
|
||||
reset: _reset,
|
||||
onResetEnd: () {
|
||||
setState(() {
|
||||
_reset = false;
|
||||
});
|
||||
},
|
||||
child: CustomPaint(
|
||||
painter: painter,
|
||||
),
|
||||
boundaryRect: Rect.fromLTWH(
|
||||
-visibleSize.width / 2,
|
||||
-visibleSize.height / 2,
|
||||
visibleSize.width,
|
||||
visibleSize.height,
|
||||
),
|
||||
// Center the board in the middle of the screen. It's drawn centered
|
||||
// at the origin, which is the top left corner of the
|
||||
// GestureTransformable.
|
||||
initialTranslation: Offset(size.width / 2, size.height / 2),
|
||||
onTapUp: _onTapUp,
|
||||
size: size,
|
||||
);
|
||||
},
|
||||
),
|
||||
floatingActionButton: _board.selected == null ? resetButton : editButton,
|
||||
);
|
||||
}
|
||||
|
||||
Widget get instructionDialog {
|
||||
return AlertDialog(
|
||||
title: const Text('2D Transformations'),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const <Widget>[
|
||||
Text('Tap to edit hex tiles, and use gestures to move around the scene:\n'),
|
||||
Text('- Drag to pan.'),
|
||||
Text('- Pinch to zoom.'),
|
||||
Text('- Rotate with two fingers.'),
|
||||
Text('\nYou can always press the home button to return to the starting orientation!'),
|
||||
],
|
||||
),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
child: const Text('OK'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
FloatingActionButton get resetButton {
|
||||
return FloatingActionButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_reset = true;
|
||||
});
|
||||
},
|
||||
tooltip: 'Reset Transform',
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
child: const Icon(Icons.home),
|
||||
);
|
||||
}
|
||||
|
||||
FloatingActionButton get editButton {
|
||||
return FloatingActionButton(
|
||||
onPressed: () {
|
||||
if (_board.selected == null) {
|
||||
return;
|
||||
}
|
||||
showModalBottomSheet<Widget>(context: context, builder: (BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 150,
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: EditBoardPoint(
|
||||
boardPoint: _board.selected,
|
||||
onColorSelection: (Color color) {
|
||||
setState(() {
|
||||
_board = _board.copyWithBoardPointColor(_board.selected, color);
|
||||
Navigator.pop(context);
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
tooltip: 'Edit Tile',
|
||||
child: const Icon(Icons.edit),
|
||||
);
|
||||
}
|
||||
|
||||
void _onTapUp(TapUpDetails details) {
|
||||
final Offset scenePoint = details.globalPosition;
|
||||
final BoardPoint boardPoint = _board.pointToBoardPoint(scenePoint);
|
||||
setState(() {
|
||||
_board = _board.copyWithSelected(boardPoint);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// CustomPainter is what is passed to CustomPaint and actually draws the scene
|
||||
// when its `paint` method is called.
|
||||
class BoardPainter extends CustomPainter {
|
||||
const BoardPainter({
|
||||
this.board,
|
||||
});
|
||||
|
||||
final Board board;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
void drawBoardPoint(BoardPoint boardPoint) {
|
||||
final Color color = boardPoint.color.withOpacity(
|
||||
board.selected == boardPoint ? 0.2 : 1.0,
|
||||
);
|
||||
final Vertices vertices = board.getVerticesForBoardPoint(boardPoint, color);
|
||||
canvas.drawVertices(vertices, BlendMode.color, Paint());
|
||||
}
|
||||
|
||||
board.forEach(drawBoardPoint);
|
||||
}
|
||||
|
||||
// We should repaint whenever the board changes, such as board.selected.
|
||||
@override
|
||||
bool shouldRepaint(BoardPainter oldDelegate) {
|
||||
return oldDelegate.board != board;
|
||||
}
|
||||
}
|
||||
@ -1,290 +0,0 @@
|
||||
// Copyright 2014 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:collection' show IterableMixin;
|
||||
import 'dart:math';
|
||||
import 'dart:ui' show Vertices;
|
||||
import 'package:flutter/material.dart' hide Gradient;
|
||||
import 'package:vector_math/vector_math_64.dart' show Vector3;
|
||||
|
||||
// The entire state of the hex board and abstraction to get information about
|
||||
// it. Iterable so that all BoardPoints on the board can be iterated over.
|
||||
@immutable
|
||||
class Board extends Object with IterableMixin<BoardPoint> {
|
||||
Board({
|
||||
@required this.boardRadius,
|
||||
@required this.hexagonRadius,
|
||||
@required this.hexagonMargin,
|
||||
this.selected,
|
||||
List<BoardPoint> boardPoints,
|
||||
}) : assert(boardRadius > 0),
|
||||
assert(hexagonRadius > 0),
|
||||
assert(hexagonMargin >= 0) {
|
||||
// Set up the positions for the center hexagon where the entire board is
|
||||
// centered on the origin.
|
||||
// Start point of hexagon (top vertex).
|
||||
final Point<double> hexStart = Point<double>(0, -hexagonRadius);
|
||||
final double hexagonRadiusPadded = hexagonRadius - hexagonMargin;
|
||||
final double centerToFlat = sqrt(3) / 2 * hexagonRadiusPadded;
|
||||
positionsForHexagonAtOrigin.addAll(<Offset>[
|
||||
Offset(hexStart.x, hexStart.y),
|
||||
Offset(hexStart.x + centerToFlat, hexStart.y + 0.5 * hexagonRadiusPadded),
|
||||
Offset(hexStart.x + centerToFlat, hexStart.y + 1.5 * hexagonRadiusPadded),
|
||||
Offset(hexStart.x + centerToFlat, hexStart.y + 1.5 * hexagonRadiusPadded),
|
||||
Offset(hexStart.x, hexStart.y + 2 * hexagonRadiusPadded),
|
||||
Offset(hexStart.x, hexStart.y + 2 * hexagonRadiusPadded),
|
||||
Offset(hexStart.x - centerToFlat, hexStart.y + 1.5 * hexagonRadiusPadded),
|
||||
Offset(hexStart.x - centerToFlat, hexStart.y + 1.5 * hexagonRadiusPadded),
|
||||
Offset(hexStart.x - centerToFlat, hexStart.y + 0.5 * hexagonRadiusPadded),
|
||||
]);
|
||||
|
||||
if (boardPoints != null) {
|
||||
_boardPoints.addAll(boardPoints);
|
||||
} else {
|
||||
// Generate boardPoints for a fresh board.
|
||||
BoardPoint boardPoint = _getNextBoardPoint(null);
|
||||
while (boardPoint != null) {
|
||||
_boardPoints.add(boardPoint);
|
||||
boardPoint = _getNextBoardPoint(boardPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final int boardRadius; // Number of hexagons from center to edge.
|
||||
final double hexagonRadius; // Pixel radius of a hexagon (center to vertex).
|
||||
final double hexagonMargin; // Margin between hexagons.
|
||||
final List<Offset> positionsForHexagonAtOrigin = <Offset>[];
|
||||
final BoardPoint selected;
|
||||
final List<BoardPoint> _boardPoints = <BoardPoint>[];
|
||||
|
||||
@override
|
||||
Iterator<BoardPoint> get iterator => _BoardIterator(_boardPoints);
|
||||
|
||||
// For a given q axial coordinate, get the range of possible r values
|
||||
// See the definition of BoardPoint for more information about hex grids and
|
||||
// axial coordinates.
|
||||
_Range _getRRangeForQ(int q) {
|
||||
int rStart;
|
||||
int rEnd;
|
||||
if (q <= 0) {
|
||||
rStart = -boardRadius - q;
|
||||
rEnd = boardRadius;
|
||||
} else {
|
||||
rEnd = boardRadius - q;
|
||||
rStart = -boardRadius;
|
||||
}
|
||||
|
||||
return _Range(rStart, rEnd);
|
||||
}
|
||||
|
||||
// Get the BoardPoint that comes after the given BoardPoint. If given null,
|
||||
// returns the origin BoardPoint. If given BoardPoint is the last, returns
|
||||
// null.
|
||||
BoardPoint _getNextBoardPoint (BoardPoint boardPoint) {
|
||||
// If before the first element.
|
||||
if (boardPoint == null) {
|
||||
return BoardPoint(-boardRadius, 0);
|
||||
}
|
||||
|
||||
final _Range rRange = _getRRangeForQ(boardPoint.q);
|
||||
|
||||
// If at or after the last element.
|
||||
if (boardPoint.q >= boardRadius && boardPoint.r >= rRange.max) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If wrapping from one q to the next.
|
||||
if (boardPoint.r >= rRange.max) {
|
||||
return BoardPoint(boardPoint.q + 1, _getRRangeForQ(boardPoint.q + 1).min);
|
||||
}
|
||||
|
||||
// Otherwise we're just incrementing r.
|
||||
return BoardPoint(boardPoint.q, boardPoint.r + 1);
|
||||
}
|
||||
|
||||
// Check if the board point is actually on the board.
|
||||
bool _validateBoardPoint(BoardPoint boardPoint) {
|
||||
const BoardPoint center = BoardPoint(0, 0);
|
||||
final int distanceFromCenter = getDistance(center, boardPoint);
|
||||
return distanceFromCenter <= boardRadius;
|
||||
}
|
||||
|
||||
// Get the distance between two BoardPoins.
|
||||
static int getDistance(BoardPoint a, BoardPoint b) {
|
||||
final Vector3 a3 = a.cubeCoordinates;
|
||||
final Vector3 b3 = b.cubeCoordinates;
|
||||
return
|
||||
((a3.x - b3.x).abs() + (a3.y - b3.y).abs() + (a3.z - b3.z).abs()) ~/ 2;
|
||||
}
|
||||
|
||||
// Return the q,r BoardPoint for a point in the scene, where the origin is in
|
||||
// the center of the board in both coordinate systems. If no BoardPoint at the
|
||||
// location, return null.
|
||||
BoardPoint pointToBoardPoint(Offset point) {
|
||||
final BoardPoint boardPoint = BoardPoint(
|
||||
((sqrt(3) / 3 * point.dx - 1 / 3 * point.dy) / hexagonRadius).round(),
|
||||
((2 / 3 * point.dy) / hexagonRadius).round(),
|
||||
);
|
||||
|
||||
if (!_validateBoardPoint(boardPoint)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _boardPoints.firstWhere((BoardPoint boardPointI) {
|
||||
return boardPointI.q == boardPoint.q && boardPointI.r == boardPoint.r;
|
||||
});
|
||||
}
|
||||
|
||||
// Return a scene point for the center of a hexagon given its q,r point.
|
||||
Point<double> boardPointToPoint(BoardPoint boardPoint) {
|
||||
return Point<double>(
|
||||
sqrt(3) * hexagonRadius * boardPoint.q + sqrt(3) / 2 * hexagonRadius * boardPoint.r,
|
||||
1.5 * hexagonRadius * boardPoint.r,
|
||||
);
|
||||
}
|
||||
|
||||
// Get Vertices that can be drawn to a Canvas for the given BoardPoint.
|
||||
Vertices getVerticesForBoardPoint(BoardPoint boardPoint, Color color) {
|
||||
final Point<double> centerOfHexZeroCenter = boardPointToPoint(boardPoint);
|
||||
|
||||
final List<Offset> positions = positionsForHexagonAtOrigin.map((Offset offset) {
|
||||
return offset.translate(centerOfHexZeroCenter.x, centerOfHexZeroCenter.y);
|
||||
}).toList();
|
||||
|
||||
return Vertices(
|
||||
VertexMode.triangleFan,
|
||||
positions,
|
||||
colors: List<Color>.filled(positions.length, color),
|
||||
);
|
||||
}
|
||||
|
||||
// Return a new board with the given BoardPoint selected.
|
||||
Board copyWithSelected(BoardPoint boardPoint) {
|
||||
if (selected == boardPoint) {
|
||||
return this;
|
||||
}
|
||||
final Board nextBoard = Board(
|
||||
boardRadius: boardRadius,
|
||||
hexagonRadius: hexagonRadius,
|
||||
hexagonMargin: hexagonMargin,
|
||||
selected: boardPoint,
|
||||
boardPoints: _boardPoints,
|
||||
);
|
||||
return nextBoard;
|
||||
}
|
||||
|
||||
// Return a new board where boardPoint has the given color.
|
||||
Board copyWithBoardPointColor(BoardPoint boardPoint, Color color) {
|
||||
final BoardPoint nextBoardPoint = boardPoint.copyWithColor(color);
|
||||
final int boardPointIndex = _boardPoints.indexWhere((BoardPoint boardPointI) =>
|
||||
boardPointI.q == boardPoint.q && boardPointI.r == boardPoint.r
|
||||
);
|
||||
|
||||
if (elementAt(boardPointIndex) == boardPoint && boardPoint.color == color) {
|
||||
return this;
|
||||
}
|
||||
|
||||
final List<BoardPoint> nextBoardPoints = List<BoardPoint>.from(_boardPoints);
|
||||
nextBoardPoints[boardPointIndex] = nextBoardPoint;
|
||||
final BoardPoint selectedBoardPoint = boardPoint == selected
|
||||
? nextBoardPoint
|
||||
: selected;
|
||||
return Board(
|
||||
boardRadius: boardRadius,
|
||||
hexagonRadius: hexagonRadius,
|
||||
hexagonMargin: hexagonMargin,
|
||||
selected: selectedBoardPoint,
|
||||
boardPoints: nextBoardPoints,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BoardIterator extends Iterator<BoardPoint> {
|
||||
_BoardIterator(this.boardPoints);
|
||||
|
||||
final List<BoardPoint> boardPoints;
|
||||
int currentIndex;
|
||||
|
||||
@override
|
||||
BoardPoint current;
|
||||
|
||||
@override
|
||||
bool moveNext() {
|
||||
if (currentIndex == null) {
|
||||
currentIndex = 0;
|
||||
} else {
|
||||
currentIndex++;
|
||||
}
|
||||
|
||||
if (currentIndex >= boardPoints.length) {
|
||||
current = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
current = boardPoints[currentIndex];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// A range of q/r board coordinate values.
|
||||
@immutable
|
||||
class _Range {
|
||||
const _Range(this.min, this.max)
|
||||
: assert(min != null),
|
||||
assert(max != null),
|
||||
assert(min <= max);
|
||||
|
||||
final int min;
|
||||
final int max;
|
||||
}
|
||||
|
||||
final Set<Color> boardPointColors = <Color>{
|
||||
Colors.grey,
|
||||
Colors.black,
|
||||
Colors.red,
|
||||
Colors.blue,
|
||||
};
|
||||
|
||||
// A location on the board in axial coordinates.
|
||||
// Axial coordinates use two integers, q and r, to locate a hexagon on a grid.
|
||||
// https://www.redblobgames.com/grids/hexagons/#coordinates-axial
|
||||
@immutable
|
||||
class BoardPoint {
|
||||
const BoardPoint(this.q, this.r, {
|
||||
this.color = Colors.grey,
|
||||
});
|
||||
|
||||
final int q;
|
||||
final int r;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BoardPoint($q, $r, $color)';
|
||||
}
|
||||
|
||||
// Only compares by location.
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
if (other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
final BoardPoint boardPoint = other;
|
||||
return boardPoint.q == q && boardPoint.r == r;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(q, r);
|
||||
|
||||
BoardPoint copyWithColor(Color nextColor) => BoardPoint(q, r, color: nextColor);
|
||||
|
||||
// Convert from q,r axial coords to x,y,z cube coords.
|
||||
Vector3 get cubeCoordinates {
|
||||
return Vector3(
|
||||
q.toDouble(),
|
||||
r.toDouble(),
|
||||
(-q - r).toDouble(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,74 +0,0 @@
|
||||
// Copyright 2014 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// A generic widget for a list of selectable colors.
|
||||
@immutable
|
||||
class ColorPicker extends StatelessWidget {
|
||||
const ColorPicker({
|
||||
@required this.colors,
|
||||
@required this.selectedColor,
|
||||
this.onColorSelection,
|
||||
}) : assert(colors != null),
|
||||
assert(selectedColor != null);
|
||||
|
||||
final Set<Color> colors;
|
||||
final Color selectedColor;
|
||||
final ValueChanged<Color> onColorSelection;
|
||||
|
||||
@override
|
||||
Widget build (BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: colors.map((Color color) {
|
||||
return _ColorPickerSwatch(
|
||||
color: color,
|
||||
selected: color == selectedColor,
|
||||
onTap: () {
|
||||
if (onColorSelection != null) {
|
||||
onColorSelection(color);
|
||||
}
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// A single selectable color widget in the ColorPicker.
|
||||
@immutable
|
||||
class _ColorPickerSwatch extends StatelessWidget {
|
||||
const _ColorPickerSwatch({
|
||||
@required this.color,
|
||||
@required this.selected,
|
||||
this.onTap,
|
||||
}) : assert(color != null),
|
||||
assert(selected != null);
|
||||
|
||||
final Color color;
|
||||
final bool selected;
|
||||
final Function onTap;
|
||||
|
||||
@override
|
||||
Widget build (BuildContext context) {
|
||||
return Container(
|
||||
width: 60.0,
|
||||
height: 60.0,
|
||||
padding: const EdgeInsets.fromLTRB(2.0, 0.0, 2.0, 0.0),
|
||||
child: RawMaterialButton(
|
||||
fillColor: color,
|
||||
onPressed: () {
|
||||
if (onTap != null) {
|
||||
onTap();
|
||||
}
|
||||
},
|
||||
child: !selected ? null : const Icon(
|
||||
Icons.check,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
// Copyright 2014 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'transformations_demo_board.dart';
|
||||
import 'transformations_demo_color_picker.dart';
|
||||
|
||||
// The panel for editing a board point.
|
||||
@immutable
|
||||
class EditBoardPoint extends StatelessWidget {
|
||||
const EditBoardPoint({
|
||||
Key key,
|
||||
@required this.boardPoint,
|
||||
this.onColorSelection,
|
||||
}) : assert(boardPoint != null),
|
||||
super(key: key);
|
||||
|
||||
final BoardPoint boardPoint;
|
||||
final ValueChanged<Color> onColorSelection;
|
||||
|
||||
@override
|
||||
Widget build (BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'${boardPoint.q}, ${boardPoint.r}',
|
||||
textAlign: TextAlign.right,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
ColorPicker(
|
||||
colors: boardPointColors,
|
||||
selectedColor: boardPoint.color,
|
||||
onColorSelection: onColorSelection,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,575 +0,0 @@
|
||||
// Copyright 2014 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:vector_math/vector_math_64.dart' show Vector3;
|
||||
import 'transformations_demo_inertial_motion.dart';
|
||||
|
||||
// This widget allows 2D transform interactions on its child in relation to its
|
||||
// parent. The user can transform the child by dragging to pan or pinching to
|
||||
// zoom and rotate. All event callbacks for GestureDetector are supported, and
|
||||
// the coordinates that are given are untransformed and in relation to the
|
||||
// original position of the child.
|
||||
@immutable
|
||||
class GestureTransformable extends StatefulWidget {
|
||||
const GestureTransformable({
|
||||
Key key,
|
||||
// The child to perform the transformations on.
|
||||
@required this.child,
|
||||
// The desired visible size of the widget and the area that is receptive to
|
||||
// gestures. If a widget that's as big as possible is desired, then wrap
|
||||
// this in a LayoutBuilder and pass
|
||||
// `Size(constraints.maxWidth, constraints.maxHeight)`.
|
||||
@required this.size,
|
||||
// The scale will be clamped to between these values. A maxScale of null has
|
||||
// no bounds. minScale must be greater than zero.
|
||||
this.maxScale = 2.5,
|
||||
this.minScale = 0.8,
|
||||
// Transforms will be limited so that the viewport can not view beyond this
|
||||
// Rect. The Rect does not rotate with the rest of the scene, so it is
|
||||
// always aligned with the viewport. A null boundaryRect results in no
|
||||
// limits to the distance that the viewport can be transformed to see.
|
||||
this.boundaryRect,
|
||||
// Initial values for the transform can be provided.
|
||||
this.initialTranslation,
|
||||
this.initialScale,
|
||||
this.initialRotation,
|
||||
// Any and all of the possible transformations can be disabled.
|
||||
this.disableTranslation = false,
|
||||
this.disableScale = false,
|
||||
this.disableRotation = false,
|
||||
// If set to true, this widget will animate back to its initial transform
|
||||
// and call onResetEnd when done. When utilizing reset, onResetEnd should
|
||||
// also be implemented, and it should set reset to false when called.
|
||||
this.reset = false,
|
||||
// Access to event callbacks from GestureDetector. Called with untransformed
|
||||
// coordinates in an Offset.
|
||||
this.onTapDown,
|
||||
this.onTapUp,
|
||||
this.onTap,
|
||||
this.onTapCancel,
|
||||
this.onDoubleTap,
|
||||
this.onLongPress,
|
||||
this.onLongPressUp,
|
||||
this.onVerticalDragDown,
|
||||
this.onVerticalDragStart,
|
||||
this.onVerticalDragUpdate,
|
||||
this.onVerticalDragEnd,
|
||||
this.onVerticalDragCancel,
|
||||
this.onHorizontalDragDown,
|
||||
this.onHorizontalDragStart,
|
||||
this.onHorizontalDragUpdate,
|
||||
this.onHorizontalDragEnd,
|
||||
this.onHorizontalDragCancel,
|
||||
this.onPanDown,
|
||||
this.onPanStart,
|
||||
this.onPanUpdate,
|
||||
this.onPanEnd,
|
||||
this.onPanCancel,
|
||||
this.onResetEnd,
|
||||
this.onScaleStart,
|
||||
this.onScaleUpdate,
|
||||
this.onScaleEnd,
|
||||
}) : assert(child != null),
|
||||
assert(size != null),
|
||||
assert(minScale != null),
|
||||
assert(minScale > 0),
|
||||
assert(disableTranslation != null),
|
||||
assert(disableScale != null),
|
||||
assert(disableRotation != null),
|
||||
assert(reset != null),
|
||||
assert(
|
||||
!reset || onResetEnd != null,
|
||||
'Must implement onResetEnd to use reset.',
|
||||
),
|
||||
super(key: key);
|
||||
|
||||
final Widget child;
|
||||
final Size size;
|
||||
final bool reset;
|
||||
final GestureTapDownCallback onTapDown;
|
||||
final GestureTapUpCallback onTapUp;
|
||||
final GestureTapCallback onTap;
|
||||
final GestureTapCancelCallback onTapCancel;
|
||||
final GestureTapCallback onDoubleTap;
|
||||
final GestureLongPressCallback onLongPress;
|
||||
final GestureLongPressUpCallback onLongPressUp;
|
||||
final GestureDragDownCallback onVerticalDragDown;
|
||||
final GestureDragStartCallback onVerticalDragStart;
|
||||
final GestureDragUpdateCallback onVerticalDragUpdate;
|
||||
final GestureDragEndCallback onVerticalDragEnd;
|
||||
final GestureDragCancelCallback onVerticalDragCancel;
|
||||
final GestureDragDownCallback onHorizontalDragDown;
|
||||
final GestureDragStartCallback onHorizontalDragStart;
|
||||
final GestureDragUpdateCallback onHorizontalDragUpdate;
|
||||
final GestureDragEndCallback onHorizontalDragEnd;
|
||||
final GestureDragCancelCallback onHorizontalDragCancel;
|
||||
final GestureDragDownCallback onPanDown;
|
||||
final GestureDragStartCallback onPanStart;
|
||||
final GestureDragUpdateCallback onPanUpdate;
|
||||
final GestureDragEndCallback onPanEnd;
|
||||
final GestureDragCancelCallback onPanCancel;
|
||||
final VoidCallback onResetEnd;
|
||||
final GestureScaleStartCallback onScaleStart;
|
||||
final GestureScaleUpdateCallback onScaleUpdate;
|
||||
final GestureScaleEndCallback onScaleEnd;
|
||||
final double maxScale;
|
||||
final double minScale;
|
||||
final Rect boundaryRect;
|
||||
final bool disableTranslation;
|
||||
final bool disableScale;
|
||||
final bool disableRotation;
|
||||
final Offset initialTranslation;
|
||||
final double initialScale;
|
||||
final double initialRotation;
|
||||
|
||||
@override _GestureTransformableState createState() => _GestureTransformableState();
|
||||
}
|
||||
|
||||
// A single user event can only represent one of these gestures. The user can't
|
||||
// do multiple at the same time, which results in more precise transformations.
|
||||
enum _GestureType {
|
||||
translate,
|
||||
scale,
|
||||
rotate,
|
||||
}
|
||||
|
||||
// This is public only for access from a unit test.
|
||||
class _GestureTransformableState extends State<GestureTransformable> with TickerProviderStateMixin {
|
||||
Animation<Offset> _animation;
|
||||
AnimationController _controller;
|
||||
Animation<Matrix4> _animationReset;
|
||||
AnimationController _controllerReset;
|
||||
// The translation that will be applied to the scene (not viewport).
|
||||
// A positive x offset moves the scene right, viewport left.
|
||||
// A positive y offset moves the scene down, viewport up.
|
||||
Offset _translateFromScene; // Point where a single translation began.
|
||||
double _scaleStart; // Scale value at start of scaling gesture.
|
||||
double _rotationStart = 0.0; // Rotation at start of rotation gesture.
|
||||
Rect _boundaryRect;
|
||||
Matrix4 _transform = Matrix4.identity();
|
||||
double _currentRotation = 0.0;
|
||||
_GestureType gestureType;
|
||||
|
||||
// The transformation matrix that gives the initial home position.
|
||||
Matrix4 get _initialTransform {
|
||||
Matrix4 matrix = Matrix4.identity();
|
||||
if (widget.initialTranslation != null) {
|
||||
matrix = matrixTranslate(matrix, widget.initialTranslation);
|
||||
}
|
||||
if (widget.initialScale != null) {
|
||||
matrix = matrixScale(matrix, widget.initialScale);
|
||||
}
|
||||
if (widget.initialRotation != null) {
|
||||
matrix = matrixRotate(matrix, widget.initialRotation, Offset.zero);
|
||||
}
|
||||
return matrix;
|
||||
}
|
||||
|
||||
// Return the scene point at the given viewport point.
|
||||
static Offset fromViewport(Offset viewportPoint, Matrix4 transform) {
|
||||
// On viewportPoint, perform the inverse transformation of the scene to get
|
||||
// where the point would be in the scene before the transformation.
|
||||
final Matrix4 inverseMatrix = Matrix4.inverted(transform);
|
||||
final Vector3 untransformed = inverseMatrix.transform3(Vector3(
|
||||
viewportPoint.dx,
|
||||
viewportPoint.dy,
|
||||
0,
|
||||
));
|
||||
return Offset(untransformed.x, untransformed.y);
|
||||
}
|
||||
|
||||
// Get the offset of the current widget from the global screen coordinates.
|
||||
// TODO(justinmc): Protect against calling this during first build.
|
||||
static Offset getOffset(BuildContext context) {
|
||||
final RenderBox renderObject = context.findRenderObject();
|
||||
return renderObject.localToGlobal(Offset.zero);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_boundaryRect = widget.boundaryRect ?? Offset.zero & widget.size;
|
||||
_transform = _initialTransform;
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
);
|
||||
_controllerReset = AnimationController(
|
||||
vsync: this,
|
||||
);
|
||||
if (widget.reset) {
|
||||
_animateResetInitialize();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(GestureTransformable oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.reset && !oldWidget.reset && _animationReset == null) {
|
||||
_animateResetInitialize();
|
||||
} else if (!widget.reset && oldWidget.reset && _animationReset != null) {
|
||||
_animateResetStop();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// A GestureDetector allows the detection of panning and zooming gestures on
|
||||
// its child, which is the CustomPaint.
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque, // Necessary when translating off screen
|
||||
onTapDown: widget.onTapDown == null ? null : (TapDownDetails details) {
|
||||
widget.onTapDown(TapDownDetails(
|
||||
globalPosition: fromViewport(details.globalPosition - getOffset(context), _transform),
|
||||
));
|
||||
},
|
||||
onTapUp: widget.onTapUp == null ? null : (TapUpDetails details) {
|
||||
widget.onTapUp(TapUpDetails(
|
||||
globalPosition: fromViewport(details.globalPosition - getOffset(context), _transform),
|
||||
));
|
||||
},
|
||||
onTap: widget.onTap,
|
||||
onTapCancel: widget.onTapCancel,
|
||||
onDoubleTap: widget.onDoubleTap,
|
||||
onLongPress: widget.onLongPress,
|
||||
onLongPressUp: widget.onLongPressUp,
|
||||
onVerticalDragDown: widget.onVerticalDragDown == null ? null : (DragDownDetails details) {
|
||||
widget.onVerticalDragDown(DragDownDetails(
|
||||
globalPosition: fromViewport(details.globalPosition - getOffset(context), _transform),
|
||||
));
|
||||
},
|
||||
onVerticalDragStart: widget.onVerticalDragStart == null ? null : (DragStartDetails details) {
|
||||
widget.onVerticalDragStart(DragStartDetails(
|
||||
globalPosition: fromViewport(details.globalPosition - getOffset(context), _transform),
|
||||
));
|
||||
},
|
||||
onVerticalDragUpdate: widget.onVerticalDragUpdate == null ? null : (DragUpdateDetails details) {
|
||||
widget.onVerticalDragUpdate(DragUpdateDetails(
|
||||
globalPosition: fromViewport(details.globalPosition - getOffset(context), _transform),
|
||||
));
|
||||
},
|
||||
onVerticalDragEnd: widget.onVerticalDragEnd,
|
||||
onVerticalDragCancel: widget.onVerticalDragCancel,
|
||||
onHorizontalDragDown: widget.onHorizontalDragDown == null ? null : (DragDownDetails details) {
|
||||
widget.onHorizontalDragDown(DragDownDetails(
|
||||
globalPosition: fromViewport(details.globalPosition - getOffset(context), _transform),
|
||||
));
|
||||
},
|
||||
onHorizontalDragStart: widget.onHorizontalDragStart == null ? null : (DragStartDetails details) {
|
||||
widget.onHorizontalDragStart(DragStartDetails(
|
||||
globalPosition: fromViewport(details.globalPosition - getOffset(context), _transform),
|
||||
));
|
||||
},
|
||||
onHorizontalDragUpdate: widget.onHorizontalDragUpdate == null ? null : (DragUpdateDetails details) {
|
||||
widget.onHorizontalDragUpdate(DragUpdateDetails(
|
||||
globalPosition: fromViewport(details.globalPosition - getOffset(context), _transform),
|
||||
));
|
||||
},
|
||||
onHorizontalDragEnd: widget.onHorizontalDragEnd,
|
||||
onHorizontalDragCancel: widget.onHorizontalDragCancel,
|
||||
onPanDown: widget.onPanDown == null ? null : (DragDownDetails details) {
|
||||
widget.onPanDown(DragDownDetails(
|
||||
globalPosition: fromViewport(details.globalPosition - getOffset(context), _transform),
|
||||
));
|
||||
},
|
||||
onPanStart: widget.onPanStart == null ? null : (DragStartDetails details) {
|
||||
widget.onPanStart(DragStartDetails(
|
||||
globalPosition: fromViewport(details.globalPosition - getOffset(context), _transform),
|
||||
));
|
||||
},
|
||||
onPanUpdate: widget.onPanUpdate == null ? null : (DragUpdateDetails details) {
|
||||
widget.onPanUpdate(DragUpdateDetails(
|
||||
globalPosition: fromViewport(details.globalPosition - getOffset(context), _transform),
|
||||
));
|
||||
},
|
||||
onPanEnd: widget.onPanEnd,
|
||||
onPanCancel: widget.onPanCancel,
|
||||
onScaleEnd: _onScaleEnd,
|
||||
onScaleStart: _onScaleStart,
|
||||
onScaleUpdate: _onScaleUpdate,
|
||||
child: ClipRect(
|
||||
// The scene is panned/zoomed/rotated using this Transform widget.
|
||||
child: Transform(
|
||||
transform: _transform,
|
||||
child: Container(
|
||||
child: widget.child,
|
||||
height: widget.size.height,
|
||||
width: widget.size.width,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Return a new matrix representing the given matrix after applying the given
|
||||
// translation.
|
||||
Matrix4 matrixTranslate(Matrix4 matrix, Offset translation) {
|
||||
if (widget.disableTranslation || translation == Offset.zero) {
|
||||
return matrix;
|
||||
}
|
||||
|
||||
// Clamp translation so the viewport remains inside _boundaryRect.
|
||||
final double scale = _transform.getMaxScaleOnAxis();
|
||||
final Size scaledSize = widget.size / scale;
|
||||
final Rect viewportBoundaries = Rect.fromLTRB(
|
||||
_boundaryRect.left,
|
||||
_boundaryRect.top,
|
||||
_boundaryRect.right - scaledSize.width,
|
||||
_boundaryRect.bottom - scaledSize.height,
|
||||
);
|
||||
// Translation is reversed (a positive translation moves the scene to the
|
||||
// right, viewport to the left).
|
||||
final Rect translationBoundaries = Rect.fromLTRB(
|
||||
-scale * viewportBoundaries.right,
|
||||
-scale * viewportBoundaries.bottom,
|
||||
-scale * viewportBoundaries.left,
|
||||
-scale * viewportBoundaries.top,
|
||||
);
|
||||
final Matrix4 nextMatrix = matrix.clone()..translate(
|
||||
translation.dx,
|
||||
translation.dy,
|
||||
);
|
||||
final Vector3 nextTranslationVector = nextMatrix.getTranslation();
|
||||
final Offset nextTranslation = Offset(
|
||||
nextTranslationVector.x,
|
||||
nextTranslationVector.y,
|
||||
);
|
||||
final bool inBoundaries = translationBoundaries.contains(
|
||||
Offset(nextTranslation.dx, nextTranslation.dy),
|
||||
);
|
||||
if (!inBoundaries) {
|
||||
// TODO(justinmc): Instead of canceling translation when it goes out of
|
||||
// bounds, stop translation at boundary.
|
||||
return matrix;
|
||||
}
|
||||
|
||||
return nextMatrix;
|
||||
}
|
||||
|
||||
// Return a new matrix representing the given matrix after applying the given
|
||||
// scale transform.
|
||||
Matrix4 matrixScale(Matrix4 matrix, double scale) {
|
||||
if (widget.disableScale || scale == 1) {
|
||||
return matrix;
|
||||
}
|
||||
assert(scale != 0);
|
||||
|
||||
// Don't allow a scale that moves the viewport outside of _boundaryRect.
|
||||
final Offset tl = fromViewport(const Offset(0, 0), _transform);
|
||||
final Offset tr = fromViewport(Offset(widget.size.width, 0), _transform);
|
||||
final Offset bl = fromViewport(Offset(0, widget.size.height), _transform);
|
||||
final Offset br = fromViewport(
|
||||
Offset(widget.size.width, widget.size.height),
|
||||
_transform,
|
||||
);
|
||||
if (!_boundaryRect.contains(tl)
|
||||
|| !_boundaryRect.contains(tr)
|
||||
|| !_boundaryRect.contains(bl)
|
||||
|| !_boundaryRect.contains(br)) {
|
||||
return matrix;
|
||||
}
|
||||
|
||||
// Don't allow a scale that results in an overall scale beyond min/max
|
||||
// scale.
|
||||
final double currentScale = _transform.getMaxScaleOnAxis();
|
||||
final double totalScale = currentScale * scale;
|
||||
final double clampedTotalScale = totalScale.clamp(
|
||||
widget.minScale,
|
||||
widget.maxScale,
|
||||
);
|
||||
final double clampedScale = clampedTotalScale / currentScale;
|
||||
return matrix..scale(clampedScale);
|
||||
}
|
||||
|
||||
// Return a new matrix representing the given matrix after applying the given
|
||||
// rotation transform.
|
||||
// Rotating the scene cannot cause the viewport to view beyond _boundaryRect.
|
||||
Matrix4 matrixRotate(Matrix4 matrix, double rotation, Offset focalPoint) {
|
||||
if (widget.disableRotation || rotation == 0) {
|
||||
return matrix;
|
||||
}
|
||||
final Offset focalPointScene = fromViewport(focalPoint, matrix);
|
||||
return matrix
|
||||
..translate(focalPointScene.dx, focalPointScene.dy)
|
||||
..rotateZ(-rotation)
|
||||
..translate(-focalPointScene.dx, -focalPointScene.dy);
|
||||
}
|
||||
|
||||
// Handle the start of a gesture of _GestureType.
|
||||
void _onScaleStart(ScaleStartDetails details) {
|
||||
if (widget.onScaleStart != null) {
|
||||
widget.onScaleStart(details);
|
||||
}
|
||||
|
||||
if (_controller.isAnimating) {
|
||||
_controller.stop();
|
||||
_controller.reset();
|
||||
_animation?.removeListener(_onAnimate);
|
||||
_animation = null;
|
||||
}
|
||||
if (_controllerReset.isAnimating) {
|
||||
_animateResetStop();
|
||||
}
|
||||
|
||||
gestureType = null;
|
||||
setState(() {
|
||||
_scaleStart = _transform.getMaxScaleOnAxis();
|
||||
_translateFromScene = fromViewport(details.focalPoint, _transform);
|
||||
_rotationStart = _currentRotation;
|
||||
});
|
||||
}
|
||||
|
||||
// Handle an update to an ongoing gesture of _GestureType.
|
||||
void _onScaleUpdate(ScaleUpdateDetails details) {
|
||||
double scale = _transform.getMaxScaleOnAxis();
|
||||
if (widget.onScaleUpdate != null) {
|
||||
widget.onScaleUpdate(ScaleUpdateDetails(
|
||||
focalPoint: fromViewport(details.focalPoint, _transform),
|
||||
scale: details.scale,
|
||||
rotation: details.rotation,
|
||||
));
|
||||
}
|
||||
final Offset focalPointScene = fromViewport(
|
||||
details.focalPoint,
|
||||
_transform,
|
||||
);
|
||||
if (gestureType == null) {
|
||||
// Decide which type of gesture this is by comparing the amount of scale
|
||||
// and rotation in the gesture, if any. Scale starts at 1 and rotation
|
||||
// starts at 0. Translate will have 0 scale and 0 rotation because it uses
|
||||
// only one finger.
|
||||
if ((details.scale - 1).abs() > details.rotation.abs()) {
|
||||
gestureType = _GestureType.scale;
|
||||
} else if (details.rotation != 0) {
|
||||
gestureType = _GestureType.rotate;
|
||||
} else {
|
||||
gestureType = _GestureType.translate;
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
if (gestureType == _GestureType.scale && _scaleStart != null) {
|
||||
// details.scale gives us the amount to change the scale as of the
|
||||
// start of this gesture, so calculate the amount to scale as of the
|
||||
// previous call to _onScaleUpdate.
|
||||
final double desiredScale = _scaleStart * details.scale;
|
||||
final double scaleChange = desiredScale / scale;
|
||||
_transform = matrixScale(_transform, scaleChange);
|
||||
scale = _transform.getMaxScaleOnAxis();
|
||||
|
||||
// While scaling, translate such that the user's two fingers stay on the
|
||||
// same places in the scene. That means that the focal point of the
|
||||
// scale should be on the same place in the scene before and after the
|
||||
// scale.
|
||||
final Offset focalPointSceneNext = fromViewport(
|
||||
details.focalPoint,
|
||||
_transform,
|
||||
);
|
||||
_transform = matrixTranslate(_transform, focalPointSceneNext - focalPointScene);
|
||||
} else if (gestureType == _GestureType.rotate && details.rotation != 0.0) {
|
||||
final double desiredRotation = _rotationStart + details.rotation;
|
||||
_transform = matrixRotate(_transform, _currentRotation - desiredRotation, details.focalPoint);
|
||||
_currentRotation = desiredRotation;
|
||||
} else if (_translateFromScene != null && details.scale == 1.0) {
|
||||
// Translate so that the same point in the scene is underneath the
|
||||
// focal point before and after the movement.
|
||||
final Offset translationChange = focalPointScene - _translateFromScene;
|
||||
_transform = matrixTranslate(_transform, translationChange);
|
||||
_translateFromScene = fromViewport(details.focalPoint, _transform);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle the end of a gesture of _GestureType.
|
||||
void _onScaleEnd(ScaleEndDetails details) {
|
||||
if (widget.onScaleEnd != null) {
|
||||
widget.onScaleEnd(details);
|
||||
}
|
||||
setState(() {
|
||||
_scaleStart = null;
|
||||
_rotationStart = null;
|
||||
_translateFromScene = null;
|
||||
});
|
||||
|
||||
_animation?.removeListener(_onAnimate);
|
||||
_controller.reset();
|
||||
|
||||
// If the scale ended with velocity, animate inertial movement
|
||||
final double velocityTotal = details.velocity.pixelsPerSecond.dx.abs()
|
||||
+ details.velocity.pixelsPerSecond.dy.abs();
|
||||
if (velocityTotal == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Vector3 translationVector = _transform.getTranslation();
|
||||
final Offset translation = Offset(translationVector.x, translationVector.y);
|
||||
final InertialMotion inertialMotion = InertialMotion(details.velocity, translation);
|
||||
_animation = Tween<Offset>(
|
||||
begin: translation,
|
||||
end: inertialMotion.finalPosition,
|
||||
).animate(_controller);
|
||||
_controller.duration = Duration(milliseconds: inertialMotion.duration.toInt());
|
||||
_animation.addListener(_onAnimate);
|
||||
_controller.fling();
|
||||
}
|
||||
|
||||
// Handle inertia drag animation.
|
||||
void _onAnimate() {
|
||||
setState(() {
|
||||
// Translate _transform such that the resulting translation is
|
||||
// _animation.value.
|
||||
final Vector3 translationVector = _transform.getTranslation();
|
||||
final Offset translation = Offset(translationVector.x, translationVector.y);
|
||||
final Offset translationScene = fromViewport(translation, _transform);
|
||||
final Offset animationScene = fromViewport(_animation.value, _transform);
|
||||
final Offset translationChangeScene = animationScene - translationScene;
|
||||
_transform = matrixTranslate(_transform, translationChangeScene);
|
||||
});
|
||||
if (!_controller.isAnimating) {
|
||||
_animation?.removeListener(_onAnimate);
|
||||
_animation = null;
|
||||
_controller.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle reset to home transform animation.
|
||||
void _onAnimateReset() {
|
||||
setState(() {
|
||||
_transform = _animationReset.value;
|
||||
});
|
||||
if (!_controllerReset.isAnimating) {
|
||||
_animationReset?.removeListener(_onAnimateReset);
|
||||
_animationReset = null;
|
||||
_controllerReset.reset();
|
||||
widget.onResetEnd();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the reset to home transform animation.
|
||||
void _animateResetInitialize() {
|
||||
_controllerReset.reset();
|
||||
_animationReset = Matrix4Tween(
|
||||
begin: _transform,
|
||||
end: _initialTransform,
|
||||
).animate(_controllerReset);
|
||||
_controllerReset.duration = const Duration(milliseconds: 400);
|
||||
_animationReset.addListener(_onAnimateReset);
|
||||
_controllerReset.forward();
|
||||
}
|
||||
|
||||
// Stop a running reset to home transform animation.
|
||||
void _animateResetStop() {
|
||||
_controllerReset.stop();
|
||||
_animationReset?.removeListener(_onAnimateReset);
|
||||
_animationReset = null;
|
||||
_controllerReset.reset();
|
||||
widget.onResetEnd();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_controllerReset.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@ -1,72 +0,0 @@
|
||||
// Copyright 2014 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:vector_math/vector_math.dart' show Vector2;
|
||||
|
||||
// Provides calculations for an object moving with inertia and friction using
|
||||
// the equation of motion from physics.
|
||||
// https://en.wikipedia.org/wiki/Equations_of_motion#Constant_translational_acceleration_in_a_straight_line
|
||||
// TODO(justinmc): Can this be replaced with friction_simulation.dart?
|
||||
@immutable
|
||||
class InertialMotion {
|
||||
const InertialMotion(this._initialVelocity, this._initialPosition);
|
||||
|
||||
static const double _kFrictionalAcceleration = 0.01; // How quickly to stop
|
||||
final Velocity _initialVelocity;
|
||||
final Offset _initialPosition;
|
||||
|
||||
// The position when the motion stops.
|
||||
Offset get finalPosition {
|
||||
return _getPositionAt(Duration(milliseconds: duration.toInt()));
|
||||
}
|
||||
|
||||
// The total time that the animation takes start to stop in milliseconds.
|
||||
double get duration {
|
||||
return (_initialVelocity.pixelsPerSecond.dx / 1000 / _acceleration.x).abs();
|
||||
}
|
||||
|
||||
// The acceleration opposing the initial velocity in x and y components.
|
||||
Vector2 get _acceleration {
|
||||
// TODO(justinmc): Find actual velocity instead of summing?
|
||||
final double velocityTotal = _initialVelocity.pixelsPerSecond.dx.abs()
|
||||
+ _initialVelocity.pixelsPerSecond.dy.abs();
|
||||
final double vRatioX = _initialVelocity.pixelsPerSecond.dx / velocityTotal;
|
||||
final double vRatioY = _initialVelocity.pixelsPerSecond.dy / velocityTotal;
|
||||
return Vector2(
|
||||
_kFrictionalAcceleration * vRatioX,
|
||||
_kFrictionalAcceleration * vRatioY,
|
||||
);
|
||||
}
|
||||
|
||||
// The position at a given time.
|
||||
Offset _getPositionAt(Duration time) {
|
||||
final double xf = _getPosition(
|
||||
r0: _initialPosition.dx,
|
||||
v0: _initialVelocity.pixelsPerSecond.dx / 1000,
|
||||
t: time.inMilliseconds,
|
||||
a: _acceleration.x,
|
||||
);
|
||||
final double yf = _getPosition(
|
||||
r0: _initialPosition.dy,
|
||||
v0: _initialVelocity.pixelsPerSecond.dy / 1000,
|
||||
t: time.inMilliseconds,
|
||||
a: _acceleration.y,
|
||||
);
|
||||
return Offset(xf, yf);
|
||||
}
|
||||
|
||||
// Solve the equation of motion to find the position at a given point in time
|
||||
// in one dimension.
|
||||
double _getPosition({double r0, double v0, int t, double a}) {
|
||||
// Stop movement when it would otherwise reverse direction.
|
||||
final double stopTime = (v0 / a).abs();
|
||||
if (t > stopTime) {
|
||||
t = stopTime.toInt();
|
||||
}
|
||||
|
||||
return r0 + v0 * t + 0.5 * a * pow(t, 2);
|
||||
}
|
||||
}
|
||||
@ -1,74 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TextStyleItem extends StatelessWidget {
|
||||
const TextStyleItem({
|
||||
Key key,
|
||||
@required this.name,
|
||||
@required this.style,
|
||||
@required this.text,
|
||||
}) : assert(name != null),
|
||||
assert(style != null),
|
||||
assert(text != null),
|
||||
super(key: key);
|
||||
|
||||
final String name;
|
||||
final TextStyle style;
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final TextStyle nameStyle = theme.textTheme.caption.copyWith(color: theme.textTheme.caption.color);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
width: 72.0,
|
||||
child: Text(name, style: nameStyle),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(text, style: style.copyWith(height: 1.0)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TypographyDemo extends StatelessWidget {
|
||||
static const String routeName = '/typography';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TextTheme textTheme = Theme.of(context).textTheme;
|
||||
final List<Widget> styleItems = <Widget>[
|
||||
if (MediaQuery.of(context).size.width > 500.0)
|
||||
TextStyleItem(name: 'Display 4', style: textTheme.display4, text: 'Light 112sp'),
|
||||
TextStyleItem(name: 'Display 3', style: textTheme.display3, text: 'Regular 56sp'),
|
||||
TextStyleItem(name: 'Display 2', style: textTheme.display2, text: 'Regular 45sp'),
|
||||
TextStyleItem(name: 'Display 1', style: textTheme.display1, text: 'Regular 34sp'),
|
||||
TextStyleItem(name: 'Headline', style: textTheme.headline, text: 'Regular 24sp'),
|
||||
TextStyleItem(name: 'Title', style: textTheme.title, text: 'Medium 20sp'),
|
||||
TextStyleItem(name: 'Subheading', style: textTheme.subhead, text: 'Regular 16sp'),
|
||||
TextStyleItem(name: 'Body 2', style: textTheme.body2, text: 'Medium 14sp'),
|
||||
TextStyleItem(name: 'Body 1', style: textTheme.body1, text: 'Regular 14sp'),
|
||||
TextStyleItem(name: 'Caption', style: textTheme.caption, text: 'Regular 12sp'),
|
||||
TextStyleItem(name: 'Button', style: textTheme.button, text: 'MEDIUM (ALL CAPS) 14sp'),
|
||||
];
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Typography')),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Scrollbar(child: ListView(children: styleItems)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,434 +0,0 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:connectivity/connectivity.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:device_info/device_info.dart';
|
||||
|
||||
class VideoCard extends StatelessWidget {
|
||||
const VideoCard({ Key key, this.controller, this.title, this.subtitle }) : super(key: key);
|
||||
|
||||
final VideoPlayerController controller;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
|
||||
Widget _buildInlineVideo() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 30.0),
|
||||
child: Center(
|
||||
child: AspectRatio(
|
||||
aspectRatio: 3 / 2,
|
||||
child: Hero(
|
||||
tag: controller,
|
||||
child: VideoPlayerLoading(controller),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFullScreenVideo() {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(title),
|
||||
),
|
||||
body: Center(
|
||||
child: AspectRatio(
|
||||
aspectRatio: 3 / 2,
|
||||
child: Hero(
|
||||
tag: controller,
|
||||
child: VideoPlayPause(controller),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget fullScreenRoutePageBuilder(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return _buildFullScreenVideo();
|
||||
}
|
||||
|
||||
void pushFullScreenWidget() {
|
||||
final TransitionRoute<void> route = PageRouteBuilder<void>(
|
||||
settings: RouteSettings(name: title, isInitialRoute: false),
|
||||
pageBuilder: fullScreenRoutePageBuilder,
|
||||
);
|
||||
|
||||
route.completed.then((void value) {
|
||||
controller.setVolume(0.0);
|
||||
});
|
||||
|
||||
controller.setVolume(1.0);
|
||||
Navigator.of(context).push(route);
|
||||
}
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Card(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
ListTile(title: Text(title), subtitle: Text(subtitle)),
|
||||
GestureDetector(
|
||||
onTap: pushFullScreenWidget,
|
||||
child: _buildInlineVideo(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VideoPlayerLoading extends StatefulWidget {
|
||||
const VideoPlayerLoading(this.controller);
|
||||
|
||||
final VideoPlayerController controller;
|
||||
|
||||
@override
|
||||
_VideoPlayerLoadingState createState() => _VideoPlayerLoadingState();
|
||||
}
|
||||
|
||||
class _VideoPlayerLoadingState extends State<VideoPlayerLoading> {
|
||||
bool _initialized;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initialized = widget.controller.value.initialized;
|
||||
widget.controller.addListener(() {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
final bool controllerInitialized = widget.controller.value.initialized;
|
||||
if (_initialized != controllerInitialized) {
|
||||
setState(() {
|
||||
_initialized = controllerInitialized;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_initialized) {
|
||||
return VideoPlayer(widget.controller);
|
||||
}
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
VideoPlayer(widget.controller),
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
],
|
||||
fit: StackFit.expand,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VideoPlayPause extends StatefulWidget {
|
||||
const VideoPlayPause(this.controller);
|
||||
|
||||
final VideoPlayerController controller;
|
||||
|
||||
@override
|
||||
State createState() => _VideoPlayPauseState();
|
||||
}
|
||||
|
||||
class _VideoPlayPauseState extends State<VideoPlayPause> {
|
||||
_VideoPlayPauseState() {
|
||||
listener = () {
|
||||
if (mounted)
|
||||
setState(() { });
|
||||
};
|
||||
}
|
||||
|
||||
FadeAnimation imageFadeAnimation;
|
||||
VoidCallback listener;
|
||||
|
||||
VideoPlayerController get controller => widget.controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller.addListener(listener);
|
||||
}
|
||||
|
||||
@override
|
||||
void deactivate() {
|
||||
controller.removeListener(listener);
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
fit: StackFit.expand,
|
||||
children: <Widget>[
|
||||
GestureDetector(
|
||||
child: VideoPlayerLoading(controller),
|
||||
onTap: () {
|
||||
if (!controller.value.initialized) {
|
||||
return;
|
||||
}
|
||||
if (controller.value.isPlaying) {
|
||||
imageFadeAnimation = const FadeAnimation(
|
||||
child: Icon(Icons.pause, size: 100.0),
|
||||
);
|
||||
controller.pause();
|
||||
} else {
|
||||
imageFadeAnimation = const FadeAnimation(
|
||||
child: Icon(Icons.play_arrow, size: 100.0),
|
||||
);
|
||||
controller.play();
|
||||
}
|
||||
},
|
||||
),
|
||||
Center(child: imageFadeAnimation),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FadeAnimation extends StatefulWidget {
|
||||
const FadeAnimation({
|
||||
this.child,
|
||||
this.duration = const Duration(milliseconds: 500),
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final Duration duration;
|
||||
|
||||
@override
|
||||
_FadeAnimationState createState() => _FadeAnimationState();
|
||||
}
|
||||
|
||||
class _FadeAnimationState extends State<FadeAnimation> with SingleTickerProviderStateMixin {
|
||||
AnimationController animationController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
animationController = AnimationController(
|
||||
duration: widget.duration,
|
||||
vsync: this,
|
||||
);
|
||||
animationController.addListener(() {
|
||||
if (mounted) {
|
||||
setState(() { });
|
||||
}
|
||||
});
|
||||
animationController.forward(from: 0.0);
|
||||
}
|
||||
|
||||
@override
|
||||
void deactivate() {
|
||||
animationController.stop();
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(FadeAnimation oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.child != widget.child) {
|
||||
animationController.forward(from: 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return animationController.isAnimating
|
||||
? Opacity(
|
||||
opacity: 1.0 - animationController.value,
|
||||
child: widget.child,
|
||||
)
|
||||
: Container();
|
||||
}
|
||||
}
|
||||
|
||||
class ConnectivityOverlay extends StatefulWidget {
|
||||
const ConnectivityOverlay({
|
||||
this.child,
|
||||
this.connectedCompleter,
|
||||
this.scaffoldKey,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final Completer<void> connectedCompleter;
|
||||
final GlobalKey<ScaffoldState> scaffoldKey;
|
||||
|
||||
@override
|
||||
_ConnectivityOverlayState createState() => _ConnectivityOverlayState();
|
||||
}
|
||||
|
||||
class _ConnectivityOverlayState extends State<ConnectivityOverlay> {
|
||||
StreamSubscription<ConnectivityResult> connectivitySubscription;
|
||||
bool connected = true;
|
||||
|
||||
static const Widget errorSnackBar = SnackBar(
|
||||
backgroundColor: Colors.red,
|
||||
content: ListTile(
|
||||
title: Text('No network'),
|
||||
subtitle: Text(
|
||||
'To load the videos you must have an active network connection',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Stream<ConnectivityResult> connectivityStream() async* {
|
||||
final Connectivity connectivity = Connectivity();
|
||||
ConnectivityResult previousResult = await connectivity.checkConnectivity();
|
||||
yield previousResult;
|
||||
await for (ConnectivityResult result
|
||||
in connectivity.onConnectivityChanged) {
|
||||
if (result != previousResult) {
|
||||
yield result;
|
||||
previousResult = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
connectivitySubscription = connectivityStream().listen(
|
||||
(ConnectivityResult connectivityResult) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (connectivityResult == ConnectivityResult.none) {
|
||||
widget.scaffoldKey.currentState.showSnackBar(errorSnackBar);
|
||||
} else {
|
||||
if (!widget.connectedCompleter.isCompleted) {
|
||||
widget.connectedCompleter.complete(null);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
connectivitySubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
}
|
||||
|
||||
class VideoDemo extends StatefulWidget {
|
||||
const VideoDemo({ Key key }) : super(key: key);
|
||||
|
||||
static const String routeName = '/video';
|
||||
|
||||
@override
|
||||
_VideoDemoState createState() => _VideoDemoState();
|
||||
}
|
||||
|
||||
final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
|
||||
|
||||
Future<bool> isIOSSimulator() async {
|
||||
return Platform.isIOS && !(await deviceInfoPlugin.iosInfo).isPhysicalDevice;
|
||||
}
|
||||
|
||||
class _VideoDemoState extends State<VideoDemo> with SingleTickerProviderStateMixin {
|
||||
final VideoPlayerController butterflyController = VideoPlayerController.asset(
|
||||
'videos/butterfly.mp4',
|
||||
package: 'flutter_gallery_assets',
|
||||
);
|
||||
|
||||
// TODO(sigurdm): This should not be stored here.
|
||||
static const String beeUri = 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4';
|
||||
final VideoPlayerController beeController = VideoPlayerController.network(beeUri);
|
||||
|
||||
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
final Completer<void> connectedCompleter = Completer<void>();
|
||||
bool isSupported = true;
|
||||
bool isDisposed = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
Future<void> initController(VideoPlayerController controller, String name) async {
|
||||
print('> VideoDemo initController "$name" ${isDisposed ? "DISPOSED" : ""}');
|
||||
controller.setLooping(true);
|
||||
controller.setVolume(0.0);
|
||||
controller.play();
|
||||
await connectedCompleter.future;
|
||||
await controller.initialize();
|
||||
if (mounted) {
|
||||
print('< VideoDemo initController "$name" done ${isDisposed ? "DISPOSED" : ""}');
|
||||
setState(() { });
|
||||
}
|
||||
}
|
||||
|
||||
initController(butterflyController, 'butterfly');
|
||||
initController(beeController, 'bee');
|
||||
isIOSSimulator().then<void>((bool result) {
|
||||
isSupported = !result;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
print('> VideoDemo dispose');
|
||||
isDisposed = true;
|
||||
butterflyController.dispose();
|
||||
beeController.dispose();
|
||||
print('< VideoDemo dispose');
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
key: scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: const Text('Videos'),
|
||||
),
|
||||
body: isSupported
|
||||
? ConnectivityOverlay(
|
||||
child: Scrollbar(
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
VideoCard(
|
||||
title: 'Butterfly',
|
||||
subtitle: '… flutters by',
|
||||
controller: butterflyController,
|
||||
),
|
||||
VideoCard(
|
||||
title: 'Bee',
|
||||
subtitle: '… gently buzzing',
|
||||
controller: beeController,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
connectedCompleter: connectedCompleter,
|
||||
scaffoldKey: scaffoldKey,
|
||||
)
|
||||
: const Center(
|
||||
child: Text(
|
||||
'Video playback not supported on the iOS Simulator.',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/foundation.dart' show defaultTargetPlatform;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class _LinkTextSpan extends TextSpan {
|
||||
|
||||
// Beware!
|
||||
//
|
||||
// This class is only safe because the TapGestureRecognizer is not
|
||||
// given a deadline and therefore never allocates any resources.
|
||||
//
|
||||
// In any other situation -- setting a deadline, using any of the less trivial
|
||||
// recognizers, etc -- you would have to manage the gesture recognizer's
|
||||
// lifetime and call dispose() when the TextSpan was no longer being rendered.
|
||||
//
|
||||
// Since TextSpan itself is @immutable, this means that you would have to
|
||||
// manage the recognizer from outside the TextSpan, e.g. in the State of a
|
||||
// stateful widget that then hands the recognizer to the TextSpan.
|
||||
|
||||
_LinkTextSpan({ TextStyle style, String url, String text }) : super(
|
||||
style: style,
|
||||
text: text ?? url,
|
||||
recognizer: TapGestureRecognizer()..onTap = () {
|
||||
launch(url, forceSafariVC: false);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void showGalleryAboutDialog(BuildContext context) {
|
||||
final ThemeData themeData = Theme.of(context);
|
||||
final TextStyle aboutTextStyle = themeData.textTheme.body2;
|
||||
final TextStyle linkStyle = themeData.textTheme.body2.copyWith(color: themeData.accentColor);
|
||||
|
||||
showAboutDialog(
|
||||
context: context,
|
||||
applicationVersion: 'January 2019',
|
||||
applicationIcon: const FlutterLogo(),
|
||||
applicationLegalese: '© 2019 The Chromium Authors',
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 24.0),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
style: aboutTextStyle,
|
||||
text: 'Flutter is an open-source project to help developers '
|
||||
'build high-performance, high-fidelity, mobile apps for '
|
||||
'${defaultTargetPlatform == TargetPlatform.iOS ? 'multiple platforms' : 'iOS and Android'} '
|
||||
'from a single codebase. This design lab is a playground '
|
||||
"and showcase of Flutter's many widgets, behaviors, "
|
||||
'animations, layouts, and more. Learn more about Flutter at ',
|
||||
),
|
||||
_LinkTextSpan(
|
||||
style: linkStyle,
|
||||
url: 'https://flutter.dev',
|
||||
),
|
||||
TextSpan(
|
||||
style: aboutTextStyle,
|
||||
text: '.\n\nTo see the source code for this app, please visit the ',
|
||||
),
|
||||
_LinkTextSpan(
|
||||
style: linkStyle,
|
||||
url: 'https://goo.gl/iv1p4G',
|
||||
text: 'flutter github repo',
|
||||
),
|
||||
TextSpan(
|
||||
style: aboutTextStyle,
|
||||
text: '.',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -1,168 +0,0 @@
|
||||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart' show defaultTargetPlatform;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart' show timeDilation;
|
||||
import 'package:flutter_gallery/demo/shrine/model/app_state_model.dart';
|
||||
import 'package:scoped_model/scoped_model.dart';
|
||||
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'demos.dart';
|
||||
import 'home.dart';
|
||||
import 'options.dart';
|
||||
import 'scales.dart';
|
||||
import 'themes.dart';
|
||||
import 'updater.dart';
|
||||
|
||||
class GalleryApp extends StatefulWidget {
|
||||
const GalleryApp({
|
||||
Key key,
|
||||
this.updateUrlFetcher,
|
||||
this.enablePerformanceOverlay = true,
|
||||
this.enableRasterCacheImagesCheckerboard = true,
|
||||
this.enableOffscreenLayersCheckerboard = true,
|
||||
this.onSendFeedback,
|
||||
this.testMode = false,
|
||||
}) : super(key: key);
|
||||
|
||||
final UpdateUrlFetcher updateUrlFetcher;
|
||||
final bool enablePerformanceOverlay;
|
||||
final bool enableRasterCacheImagesCheckerboard;
|
||||
final bool enableOffscreenLayersCheckerboard;
|
||||
final VoidCallback onSendFeedback;
|
||||
final bool testMode;
|
||||
|
||||
@override
|
||||
_GalleryAppState createState() => _GalleryAppState();
|
||||
}
|
||||
|
||||
class _GalleryAppState extends State<GalleryApp> {
|
||||
GalleryOptions _options;
|
||||
Timer _timeDilationTimer;
|
||||
AppStateModel model;
|
||||
|
||||
Map<String, WidgetBuilder> _buildRoutes() {
|
||||
// For a different example of how to set up an application routing table
|
||||
// using named routes, consider the example in the Navigator class documentation:
|
||||
// https://docs.flutter.io/flutter/widgets/Navigator-class.html
|
||||
return Map<String, WidgetBuilder>.fromIterable(
|
||||
kAllGalleryDemos,
|
||||
key: (dynamic demo) => '${demo.routeName}',
|
||||
value: (dynamic demo) => demo.buildRoute,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_options = GalleryOptions(
|
||||
themeMode: ThemeMode.system,
|
||||
textScaleFactor: kAllGalleryTextScaleValues[0],
|
||||
timeDilation: timeDilation,
|
||||
platform: defaultTargetPlatform,
|
||||
);
|
||||
model = AppStateModel()..loadProducts();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timeDilationTimer?.cancel();
|
||||
_timeDilationTimer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleOptionsChanged(GalleryOptions newOptions) {
|
||||
setState(() {
|
||||
if (_options.timeDilation != newOptions.timeDilation) {
|
||||
_timeDilationTimer?.cancel();
|
||||
_timeDilationTimer = null;
|
||||
if (newOptions.timeDilation > 1.0) {
|
||||
// We delay the time dilation change long enough that the user can see
|
||||
// that UI has started reacting and then we slam on the brakes so that
|
||||
// they see that the time is in fact now dilated.
|
||||
_timeDilationTimer = Timer(const Duration(milliseconds: 150), () {
|
||||
timeDilation = newOptions.timeDilation;
|
||||
});
|
||||
} else {
|
||||
timeDilation = newOptions.timeDilation;
|
||||
}
|
||||
}
|
||||
|
||||
_options = newOptions;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _applyTextScaleFactor(Widget child) {
|
||||
return Builder(
|
||||
builder: (BuildContext context) {
|
||||
return MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
textScaleFactor: _options.textScaleFactor.scale,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget home = GalleryHome(
|
||||
testMode: widget.testMode,
|
||||
optionsPage: GalleryOptionsPage(
|
||||
options: _options,
|
||||
onOptionsChanged: _handleOptionsChanged,
|
||||
onSendFeedback: widget.onSendFeedback ?? () {
|
||||
launch('https://github.com/flutter/flutter/issues/new/choose', forceSafariVC: false);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.updateUrlFetcher != null) {
|
||||
home = Updater(
|
||||
updateUrlFetcher: widget.updateUrlFetcher,
|
||||
child: home,
|
||||
);
|
||||
}
|
||||
|
||||
return ScopedModel<AppStateModel>(
|
||||
model: model,
|
||||
child: MaterialApp(
|
||||
theme: kLightGalleryTheme.copyWith(platform: _options.platform),
|
||||
darkTheme: kDarkGalleryTheme.copyWith(platform: _options.platform),
|
||||
themeMode: _options.themeMode,
|
||||
title: 'Flutter Gallery',
|
||||
color: Colors.grey,
|
||||
showPerformanceOverlay: _options.showPerformanceOverlay,
|
||||
checkerboardOffscreenLayers: _options.showOffscreenLayersCheckerboard,
|
||||
checkerboardRasterCacheImages: _options.showRasterCacheImagesCheckerboard,
|
||||
routes: _buildRoutes(),
|
||||
builder: (BuildContext context, Widget child) {
|
||||
return Directionality(
|
||||
textDirection: _options.textDirection,
|
||||
child: _applyTextScaleFactor(
|
||||
// Specifically use a blank Cupertino theme here and do not transfer
|
||||
// over the Material primary color etc except the brightness to
|
||||
// showcase standard iOS looks.
|
||||
Builder(builder: (BuildContext context) {
|
||||
return CupertinoTheme(
|
||||
data: CupertinoThemeData(
|
||||
brightness: Theme.of(context).brightness,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
home: home,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,355 +0,0 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const double _kFrontHeadingHeight = 32.0; // front layer beveled rectangle
|
||||
const double _kFrontClosedHeight = 92.0; // front layer height when closed
|
||||
const double _kBackAppBarHeight = 56.0; // back layer (options) appbar height
|
||||
|
||||
// The size of the front layer heading's left and right beveled corners.
|
||||
final Animatable<BorderRadius> _kFrontHeadingBevelRadius = BorderRadiusTween(
|
||||
begin: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12.0),
|
||||
topRight: Radius.circular(12.0),
|
||||
),
|
||||
end: const BorderRadius.only(
|
||||
topLeft: Radius.circular(_kFrontHeadingHeight),
|
||||
topRight: Radius.circular(_kFrontHeadingHeight),
|
||||
),
|
||||
);
|
||||
|
||||
class _TappableWhileStatusIs extends StatefulWidget {
|
||||
const _TappableWhileStatusIs(
|
||||
this.status, {
|
||||
Key key,
|
||||
this.controller,
|
||||
this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
final AnimationController controller;
|
||||
final AnimationStatus status;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
_TappableWhileStatusIsState createState() => _TappableWhileStatusIsState();
|
||||
}
|
||||
|
||||
class _TappableWhileStatusIsState extends State<_TappableWhileStatusIs> {
|
||||
bool _active;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.controller.addStatusListener(_handleStatusChange);
|
||||
_active = widget.controller.status == widget.status;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeStatusListener(_handleStatusChange);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleStatusChange(AnimationStatus status) {
|
||||
final bool value = widget.controller.status == widget.status;
|
||||
if (_active != value) {
|
||||
setState(() {
|
||||
_active = value;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = AbsorbPointer(
|
||||
absorbing: !_active,
|
||||
child: widget.child,
|
||||
);
|
||||
|
||||
if (!_active) {
|
||||
child = FocusScope(
|
||||
canRequestFocus: false,
|
||||
debugLabel: '$_TappableWhileStatusIs',
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
class _CrossFadeTransition extends AnimatedWidget {
|
||||
const _CrossFadeTransition({
|
||||
Key key,
|
||||
this.alignment = Alignment.center,
|
||||
Animation<double> progress,
|
||||
this.child0,
|
||||
this.child1,
|
||||
}) : super(key: key, listenable: progress);
|
||||
|
||||
final AlignmentGeometry alignment;
|
||||
final Widget child0;
|
||||
final Widget child1;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Animation<double> progress = listenable;
|
||||
|
||||
final double opacity1 = CurvedAnimation(
|
||||
parent: ReverseAnimation(progress),
|
||||
curve: const Interval(0.5, 1.0),
|
||||
).value;
|
||||
|
||||
final double opacity2 = CurvedAnimation(
|
||||
parent: progress,
|
||||
curve: const Interval(0.5, 1.0),
|
||||
).value;
|
||||
|
||||
return Stack(
|
||||
alignment: alignment,
|
||||
children: <Widget>[
|
||||
Opacity(
|
||||
opacity: opacity1,
|
||||
child: Semantics(
|
||||
scopesRoute: true,
|
||||
explicitChildNodes: true,
|
||||
child: child1,
|
||||
),
|
||||
),
|
||||
Opacity(
|
||||
opacity: opacity2,
|
||||
child: Semantics(
|
||||
scopesRoute: true,
|
||||
explicitChildNodes: true,
|
||||
child: child0,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BackAppBar extends StatelessWidget {
|
||||
const _BackAppBar({
|
||||
Key key,
|
||||
this.leading = const SizedBox(width: 56.0),
|
||||
@required this.title,
|
||||
this.trailing,
|
||||
}) : assert(leading != null), assert(title != null), super(key: key);
|
||||
|
||||
final Widget leading;
|
||||
final Widget title;
|
||||
final Widget trailing;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return IconTheme.merge(
|
||||
data: theme.primaryIconTheme,
|
||||
child: DefaultTextStyle(
|
||||
style: theme.primaryTextTheme.title,
|
||||
child: SizedBox(
|
||||
height: _kBackAppBarHeight,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
width: 56.0,
|
||||
child: leading,
|
||||
),
|
||||
Expanded(
|
||||
child: title,
|
||||
),
|
||||
if (trailing != null)
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
width: 56.0,
|
||||
child: trailing,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Backdrop extends StatefulWidget {
|
||||
const Backdrop({
|
||||
this.frontAction,
|
||||
this.frontTitle,
|
||||
this.frontHeading,
|
||||
this.frontLayer,
|
||||
this.backTitle,
|
||||
this.backLayer,
|
||||
});
|
||||
|
||||
final Widget frontAction;
|
||||
final Widget frontTitle;
|
||||
final Widget frontLayer;
|
||||
final Widget frontHeading;
|
||||
final Widget backTitle;
|
||||
final Widget backLayer;
|
||||
|
||||
@override
|
||||
_BackdropState createState() => _BackdropState();
|
||||
}
|
||||
|
||||
class _BackdropState extends State<Backdrop> with SingleTickerProviderStateMixin {
|
||||
final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop');
|
||||
AnimationController _controller;
|
||||
Animation<double> _frontOpacity;
|
||||
|
||||
static final Animatable<double> _frontOpacityTween = Tween<double>(begin: 0.2, end: 1.0)
|
||||
.chain(CurveTween(curve: const Interval(0.0, 0.4, curve: Curves.easeInOut)));
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
value: 1.0,
|
||||
vsync: this,
|
||||
);
|
||||
_frontOpacity = _controller.drive(_frontOpacityTween);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
double get _backdropHeight {
|
||||
// Warning: this can be safely called from the event handlers but it may
|
||||
// not be called at build time.
|
||||
final RenderBox renderBox = _backdropKey.currentContext.findRenderObject();
|
||||
return math.max(0.0, renderBox.size.height - _kBackAppBarHeight - _kFrontClosedHeight);
|
||||
}
|
||||
|
||||
void _handleDragUpdate(DragUpdateDetails details) {
|
||||
_controller.value -= details.primaryDelta / (_backdropHeight ?? details.primaryDelta);
|
||||
}
|
||||
|
||||
void _handleDragEnd(DragEndDetails details) {
|
||||
if (_controller.isAnimating || _controller.status == AnimationStatus.completed)
|
||||
return;
|
||||
|
||||
final double flingVelocity = details.velocity.pixelsPerSecond.dy / _backdropHeight;
|
||||
if (flingVelocity < 0.0)
|
||||
_controller.fling(velocity: math.max(2.0, -flingVelocity));
|
||||
else if (flingVelocity > 0.0)
|
||||
_controller.fling(velocity: math.min(-2.0, -flingVelocity));
|
||||
else
|
||||
_controller.fling(velocity: _controller.value < 0.5 ? -2.0 : 2.0);
|
||||
}
|
||||
|
||||
void _toggleFrontLayer() {
|
||||
final AnimationStatus status = _controller.status;
|
||||
final bool isOpen = status == AnimationStatus.completed || status == AnimationStatus.forward;
|
||||
_controller.fling(velocity: isOpen ? -2.0 : 2.0);
|
||||
}
|
||||
|
||||
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
|
||||
final Animation<RelativeRect> frontRelativeRect = _controller.drive(RelativeRectTween(
|
||||
begin: RelativeRect.fromLTRB(0.0, constraints.biggest.height - _kFrontClosedHeight, 0.0, 0.0),
|
||||
end: const RelativeRect.fromLTRB(0.0, _kBackAppBarHeight, 0.0, 0.0),
|
||||
));
|
||||
return Stack(
|
||||
key: _backdropKey,
|
||||
children: <Widget>[
|
||||
// Back layer
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
_BackAppBar(
|
||||
leading: widget.frontAction,
|
||||
title: _CrossFadeTransition(
|
||||
progress: _controller,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child0: Semantics(namesRoute: true, child: widget.frontTitle),
|
||||
child1: Semantics(namesRoute: true, child: widget.backTitle),
|
||||
),
|
||||
trailing: IconButton(
|
||||
onPressed: _toggleFrontLayer,
|
||||
tooltip: 'Toggle options page',
|
||||
icon: AnimatedIcon(
|
||||
icon: AnimatedIcons.close_menu,
|
||||
progress: _controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _TappableWhileStatusIs(
|
||||
AnimationStatus.dismissed,
|
||||
controller: _controller,
|
||||
child: Visibility(
|
||||
child: widget.backLayer,
|
||||
visible: _controller.status != AnimationStatus.completed,
|
||||
maintainState: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Front layer
|
||||
PositionedTransition(
|
||||
rect: frontRelativeRect,
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (BuildContext context, Widget child) {
|
||||
return PhysicalShape(
|
||||
elevation: 12.0,
|
||||
color: Theme.of(context).canvasColor,
|
||||
clipper: ShapeBorderClipper(
|
||||
shape: BeveledRectangleBorder(
|
||||
borderRadius: _kFrontHeadingBevelRadius.transform(_controller.value),
|
||||
),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: _TappableWhileStatusIs(
|
||||
AnimationStatus.completed,
|
||||
controller: _controller,
|
||||
child: FadeTransition(
|
||||
opacity: _frontOpacity,
|
||||
child: widget.frontLayer,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// The front "heading" is a (typically transparent) widget that's stacked on
|
||||
// top of, and at the top of, the front layer. It adds support for dragging
|
||||
// the front layer up and down and for opening and closing the front layer
|
||||
// with a tap. It may obscure part of the front layer's topmost child.
|
||||
if (widget.frontHeading != null)
|
||||
PositionedTransition(
|
||||
rect: frontRelativeRect,
|
||||
child: ExcludeSemantics(
|
||||
child: Container(
|
||||
alignment: Alignment.topLeft,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: _toggleFrontLayer,
|
||||
onVerticalDragUpdate: _handleDragUpdate,
|
||||
onVerticalDragEnd: _handleDragEnd,
|
||||
child: widget.frontHeading,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: _buildStack);
|
||||
}
|
||||
}
|
||||
@ -1,261 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'demos.dart';
|
||||
import 'example_code_parser.dart';
|
||||
import 'syntax_highlighter.dart';
|
||||
|
||||
class ComponentDemoTabData {
|
||||
ComponentDemoTabData({
|
||||
this.demoWidget,
|
||||
this.exampleCodeTag,
|
||||
this.description,
|
||||
this.tabName,
|
||||
this.documentationUrl,
|
||||
});
|
||||
|
||||
final Widget demoWidget;
|
||||
final String exampleCodeTag;
|
||||
final String description;
|
||||
final String tabName;
|
||||
final String documentationUrl;
|
||||
|
||||
@override
|
||||
bool operator==(Object other) {
|
||||
if (other.runtimeType != runtimeType)
|
||||
return false;
|
||||
final ComponentDemoTabData typedOther = other;
|
||||
return typedOther.tabName == tabName
|
||||
&& typedOther.description == description
|
||||
&& typedOther.documentationUrl == documentationUrl;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(tabName, description, documentationUrl);
|
||||
}
|
||||
|
||||
class TabbedComponentDemoScaffold extends StatelessWidget {
|
||||
const TabbedComponentDemoScaffold({
|
||||
this.title,
|
||||
this.demos,
|
||||
this.actions,
|
||||
this.isScrollable = true,
|
||||
this.showExampleCodeAction = true,
|
||||
});
|
||||
|
||||
final List<ComponentDemoTabData> demos;
|
||||
final String title;
|
||||
final List<Widget> actions;
|
||||
final bool isScrollable;
|
||||
final bool showExampleCodeAction;
|
||||
|
||||
void _showExampleCode(BuildContext context) {
|
||||
final String tag = demos[DefaultTabController.of(context).index].exampleCodeTag;
|
||||
if (tag != null) {
|
||||
Navigator.push(context, MaterialPageRoute<FullScreenCodeDialog>(
|
||||
builder: (BuildContext context) => FullScreenCodeDialog(exampleCodeTag: tag)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showApiDocumentation(BuildContext context) async {
|
||||
final String url = demos[DefaultTabController.of(context).index].documentationUrl;
|
||||
if (url == null)
|
||||
return;
|
||||
|
||||
if (await canLaunch(url)) {
|
||||
await launch(url);
|
||||
} else {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return SimpleDialog(
|
||||
title: const Text('Couldn\'t display URL:'),
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Text(url),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: demos.length,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(title),
|
||||
actions: <Widget>[
|
||||
...?actions,
|
||||
Builder(
|
||||
builder: (BuildContext context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.library_books, semanticLabel: 'Show documentation'),
|
||||
onPressed: () => _showApiDocumentation(context),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (showExampleCodeAction)
|
||||
Builder(
|
||||
builder: (BuildContext context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.code),
|
||||
tooltip: 'Show example code',
|
||||
onPressed: () => _showExampleCode(context),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
bottom: TabBar(
|
||||
isScrollable: isScrollable,
|
||||
tabs: demos.map<Widget>((ComponentDemoTabData data) => Tab(text: data.tabName)).toList(),
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
children: demos.map<Widget>((ComponentDemoTabData demo) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(demo.description,
|
||||
style: Theme.of(context).textTheme.subhead,
|
||||
),
|
||||
),
|
||||
Expanded(child: demo.demoWidget),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FullScreenCodeDialog extends StatefulWidget {
|
||||
const FullScreenCodeDialog({ this.exampleCodeTag });
|
||||
|
||||
final String exampleCodeTag;
|
||||
|
||||
@override
|
||||
FullScreenCodeDialogState createState() => FullScreenCodeDialogState();
|
||||
}
|
||||
|
||||
class FullScreenCodeDialogState extends State<FullScreenCodeDialog> {
|
||||
|
||||
String _exampleCode;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
getExampleCode(widget.exampleCodeTag, DefaultAssetBundle.of(context)).then<void>((String code) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_exampleCode = code ?? 'Example code not found';
|
||||
});
|
||||
}
|
||||
});
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final SyntaxHighlighterStyle style = Theme.of(context).brightness == Brightness.dark
|
||||
? SyntaxHighlighterStyle.darkThemeStyle()
|
||||
: SyntaxHighlighterStyle.lightThemeStyle();
|
||||
|
||||
Widget body;
|
||||
if (_exampleCode == null) {
|
||||
body = const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
} else {
|
||||
body = SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
style: const TextStyle(fontFamily: 'monospace', fontSize: 10.0),
|
||||
children: <TextSpan>[
|
||||
DartSyntaxHighlighter(style).format(_exampleCode),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.clear,
|
||||
semanticLabel: 'Close',
|
||||
),
|
||||
onPressed: () { Navigator.pop(context); },
|
||||
),
|
||||
title: const Text('Example code'),
|
||||
),
|
||||
body: body,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MaterialDemoDocumentationButton extends StatelessWidget {
|
||||
MaterialDemoDocumentationButton(String routeName, { Key key })
|
||||
: documentationUrl = kDemoDocumentationUrl[routeName],
|
||||
assert(
|
||||
kDemoDocumentationUrl[routeName] != null,
|
||||
'A documentation URL was not specified for demo route $routeName in kAllGalleryDemos',
|
||||
),
|
||||
super(key: key);
|
||||
|
||||
final String documentationUrl;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.library_books),
|
||||
tooltip: 'API documentation',
|
||||
onPressed: () => launch(documentationUrl, forceWebView: true),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CupertinoDemoDocumentationButton extends StatelessWidget {
|
||||
CupertinoDemoDocumentationButton(String routeName, { Key key })
|
||||
: documentationUrl = kDemoDocumentationUrl[routeName],
|
||||
assert(
|
||||
kDemoDocumentationUrl[routeName] != null,
|
||||
'A documentation URL was not specified for demo route $routeName in kAllGalleryDemos',
|
||||
),
|
||||
super(key: key);
|
||||
|
||||
final String documentationUrl;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Semantics(
|
||||
label: 'API documentation',
|
||||
child: const Icon(CupertinoIcons.book),
|
||||
),
|
||||
onPressed: () => launch(documentationUrl, forceWebView: true),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,587 +0,0 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../demo/all.dart';
|
||||
import 'icons.dart';
|
||||
|
||||
class GalleryDemoCategory {
|
||||
const GalleryDemoCategory._({
|
||||
@required this.name,
|
||||
@required this.icon,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final IconData icon;
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
if (identical(this, other))
|
||||
return true;
|
||||
if (runtimeType != other.runtimeType)
|
||||
return false;
|
||||
final GalleryDemoCategory typedOther = other;
|
||||
return typedOther.name == name && typedOther.icon == icon;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(name, icon);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '$runtimeType($name)';
|
||||
}
|
||||
}
|
||||
|
||||
const GalleryDemoCategory _kDemos = GalleryDemoCategory._(
|
||||
name: 'Studies',
|
||||
icon: GalleryIcons.animation,
|
||||
);
|
||||
|
||||
const GalleryDemoCategory _kStyle = GalleryDemoCategory._(
|
||||
name: 'Style',
|
||||
icon: GalleryIcons.custom_typography,
|
||||
);
|
||||
|
||||
const GalleryDemoCategory _kMaterialComponents = GalleryDemoCategory._(
|
||||
name: 'Material',
|
||||
icon: GalleryIcons.category_mdc,
|
||||
);
|
||||
|
||||
const GalleryDemoCategory _kCupertinoComponents = GalleryDemoCategory._(
|
||||
name: 'Cupertino',
|
||||
icon: GalleryIcons.phone_iphone,
|
||||
);
|
||||
|
||||
const GalleryDemoCategory _kMedia = GalleryDemoCategory._(
|
||||
name: 'Media',
|
||||
icon: GalleryIcons.drive_video,
|
||||
);
|
||||
|
||||
class GalleryDemo {
|
||||
const GalleryDemo({
|
||||
@required this.title,
|
||||
@required this.icon,
|
||||
this.subtitle,
|
||||
@required this.category,
|
||||
@required this.routeName,
|
||||
this.documentationUrl,
|
||||
@required this.buildRoute,
|
||||
}) : assert(title != null),
|
||||
assert(category != null),
|
||||
assert(routeName != null),
|
||||
assert(buildRoute != null);
|
||||
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final String subtitle;
|
||||
final GalleryDemoCategory category;
|
||||
final String routeName;
|
||||
final WidgetBuilder buildRoute;
|
||||
final String documentationUrl;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '$runtimeType($title $routeName)';
|
||||
}
|
||||
}
|
||||
|
||||
List<GalleryDemo> _buildGalleryDemos() {
|
||||
final List<GalleryDemo> galleryDemos = <GalleryDemo>[
|
||||
// Demos
|
||||
GalleryDemo(
|
||||
title: 'Shrine',
|
||||
subtitle: 'Basic shopping app',
|
||||
icon: GalleryIcons.shrine,
|
||||
category: _kDemos,
|
||||
routeName: ShrineDemo.routeName,
|
||||
buildRoute: (BuildContext context) => const ShrineDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Fortnightly',
|
||||
subtitle: 'Newspaper typography app',
|
||||
icon: GalleryIcons.custom_typography,
|
||||
category: _kDemos,
|
||||
routeName: FortnightlyDemo.routeName,
|
||||
buildRoute: (BuildContext context) => FortnightlyDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Contact profile',
|
||||
subtitle: 'Address book entry with a flexible appbar',
|
||||
icon: GalleryIcons.account_box,
|
||||
category: _kDemos,
|
||||
routeName: ContactsDemo.routeName,
|
||||
buildRoute: (BuildContext context) => ContactsDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Animation',
|
||||
subtitle: 'Section organizer',
|
||||
icon: GalleryIcons.animation,
|
||||
category: _kDemos,
|
||||
routeName: AnimationDemo.routeName,
|
||||
buildRoute: (BuildContext context) => const AnimationDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: '2D Transformations',
|
||||
subtitle: 'Pan, Zoom, Rotate',
|
||||
icon: GalleryIcons.grid_on,
|
||||
category: _kDemos,
|
||||
routeName: TransformationsDemo.routeName,
|
||||
buildRoute: (BuildContext context) => const TransformationsDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Pesto',
|
||||
subtitle: 'Simple recipe browser',
|
||||
icon: Icons.adjust,
|
||||
category: _kDemos,
|
||||
routeName: PestoDemo.routeName,
|
||||
buildRoute: (BuildContext context) => const PestoDemo(),
|
||||
),
|
||||
|
||||
// Style
|
||||
GalleryDemo(
|
||||
title: 'Colors',
|
||||
subtitle: 'All of the predefined colors',
|
||||
icon: GalleryIcons.colors,
|
||||
category: _kStyle,
|
||||
routeName: ColorsDemo.routeName,
|
||||
buildRoute: (BuildContext context) => ColorsDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Typography',
|
||||
subtitle: 'All of the predefined text styles',
|
||||
icon: GalleryIcons.custom_typography,
|
||||
category: _kStyle,
|
||||
routeName: TypographyDemo.routeName,
|
||||
buildRoute: (BuildContext context) => TypographyDemo(),
|
||||
),
|
||||
|
||||
// Material Components
|
||||
GalleryDemo(
|
||||
title: 'Backdrop',
|
||||
subtitle: 'Select a front layer from back layer',
|
||||
icon: GalleryIcons.backdrop,
|
||||
category: _kMaterialComponents,
|
||||
routeName: BackdropDemo.routeName,
|
||||
buildRoute: (BuildContext context) => BackdropDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Banner',
|
||||
subtitle: 'Displaying a banner within a list',
|
||||
icon: GalleryIcons.lists_leave_behind,
|
||||
category: _kMaterialComponents,
|
||||
routeName: BannerDemo.routeName,
|
||||
documentationUrl: 'https://api.flutter.dev/flutter/material/MaterialBanner-class.html',
|
||||
buildRoute: (BuildContext context) => const BannerDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Bottom app bar',
|
||||
subtitle: 'Optional floating action button notch',
|
||||
icon: GalleryIcons.bottom_app_bar,
|
||||
category: _kMaterialComponents,
|
||||
routeName: BottomAppBarDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/BottomAppBar-class.html',
|
||||
buildRoute: (BuildContext context) => BottomAppBarDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Bottom navigation',
|
||||
subtitle: 'Bottom navigation with cross-fading views',
|
||||
icon: GalleryIcons.bottom_navigation,
|
||||
category: _kMaterialComponents,
|
||||
routeName: BottomNavigationDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/BottomNavigationBar-class.html',
|
||||
buildRoute: (BuildContext context) => BottomNavigationDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Bottom sheet: Modal',
|
||||
subtitle: 'A dismissible bottom sheet',
|
||||
icon: GalleryIcons.bottom_sheets,
|
||||
category: _kMaterialComponents,
|
||||
routeName: ModalBottomSheetDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/showModalBottomSheet.html',
|
||||
buildRoute: (BuildContext context) => ModalBottomSheetDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Bottom sheet: Persistent',
|
||||
subtitle: 'A bottom sheet that sticks around',
|
||||
icon: GalleryIcons.bottom_sheet_persistent,
|
||||
category: _kMaterialComponents,
|
||||
routeName: PersistentBottomSheetDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/ScaffoldState/showBottomSheet.html',
|
||||
buildRoute: (BuildContext context) => PersistentBottomSheetDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Buttons',
|
||||
subtitle: 'Flat, raised, dropdown, and more',
|
||||
icon: GalleryIcons.generic_buttons,
|
||||
category: _kMaterialComponents,
|
||||
routeName: ButtonsDemo.routeName,
|
||||
buildRoute: (BuildContext context) => ButtonsDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Buttons: Floating Action Button',
|
||||
subtitle: 'FAB with transitions',
|
||||
icon: GalleryIcons.buttons,
|
||||
category: _kMaterialComponents,
|
||||
routeName: TabsFabDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/FloatingActionButton-class.html',
|
||||
buildRoute: (BuildContext context) => TabsFabDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Cards',
|
||||
subtitle: 'Baseline cards with rounded corners',
|
||||
icon: GalleryIcons.cards,
|
||||
category: _kMaterialComponents,
|
||||
routeName: CardsDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/Card-class.html',
|
||||
buildRoute: (BuildContext context) => CardsDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Chips',
|
||||
subtitle: 'Labeled with delete buttons and avatars',
|
||||
icon: GalleryIcons.chips,
|
||||
category: _kMaterialComponents,
|
||||
routeName: ChipDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/Chip-class.html',
|
||||
buildRoute: (BuildContext context) => ChipDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Data tables',
|
||||
subtitle: 'Rows and columns',
|
||||
icon: GalleryIcons.data_table,
|
||||
category: _kMaterialComponents,
|
||||
routeName: DataTableDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/PaginatedDataTable-class.html',
|
||||
buildRoute: (BuildContext context) => DataTableDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Dialogs',
|
||||
subtitle: 'Simple, alert, and fullscreen',
|
||||
icon: GalleryIcons.dialogs,
|
||||
category: _kMaterialComponents,
|
||||
routeName: DialogDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/showDialog.html',
|
||||
buildRoute: (BuildContext context) => DialogDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Elevations',
|
||||
subtitle: 'Shadow values on cards',
|
||||
// TODO(larche): Change to custom icon for elevations when one exists.
|
||||
icon: GalleryIcons.cupertino_progress,
|
||||
category: _kMaterialComponents,
|
||||
routeName: ElevationDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/Material/elevation.html',
|
||||
buildRoute: (BuildContext context) => ElevationDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Expand/collapse list control',
|
||||
subtitle: 'A list with one sub-list level',
|
||||
icon: GalleryIcons.expand_all,
|
||||
category: _kMaterialComponents,
|
||||
routeName: ExpansionTileListDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/ExpansionTile-class.html',
|
||||
buildRoute: (BuildContext context) => ExpansionTileListDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Expansion panels',
|
||||
subtitle: 'List of expanding panels',
|
||||
icon: GalleryIcons.expand_all,
|
||||
category: _kMaterialComponents,
|
||||
routeName: ExpansionPanelsDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/ExpansionPanel-class.html',
|
||||
buildRoute: (BuildContext context) => ExpansionPanelsDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Grid',
|
||||
subtitle: 'Row and column layout',
|
||||
icon: GalleryIcons.grid_on,
|
||||
category: _kMaterialComponents,
|
||||
routeName: GridListDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/widgets/GridView-class.html',
|
||||
buildRoute: (BuildContext context) => const GridListDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Icons',
|
||||
subtitle: 'Enabled and disabled icons with opacity',
|
||||
icon: GalleryIcons.sentiment_very_satisfied,
|
||||
category: _kMaterialComponents,
|
||||
routeName: IconsDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/IconButton-class.html',
|
||||
buildRoute: (BuildContext context) => IconsDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Lists',
|
||||
subtitle: 'Scrolling list layouts',
|
||||
icon: GalleryIcons.list_alt,
|
||||
category: _kMaterialComponents,
|
||||
routeName: ListDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/ListTile-class.html',
|
||||
buildRoute: (BuildContext context) => const ListDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Lists: leave-behind list items',
|
||||
subtitle: 'List items with hidden actions',
|
||||
icon: GalleryIcons.lists_leave_behind,
|
||||
category: _kMaterialComponents,
|
||||
routeName: LeaveBehindDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/widgets/Dismissible-class.html',
|
||||
buildRoute: (BuildContext context) => const LeaveBehindDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Lists: reorderable',
|
||||
subtitle: 'Reorderable lists',
|
||||
icon: GalleryIcons.list_alt,
|
||||
category: _kMaterialComponents,
|
||||
routeName: ReorderableListDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/ReorderableListView-class.html',
|
||||
buildRoute: (BuildContext context) => const ReorderableListDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Menus',
|
||||
subtitle: 'Menu buttons and simple menus',
|
||||
icon: GalleryIcons.more_vert,
|
||||
category: _kMaterialComponents,
|
||||
routeName: MenuDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/PopupMenuButton-class.html',
|
||||
buildRoute: (BuildContext context) => const MenuDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Navigation drawer',
|
||||
subtitle: 'Navigation drawer with standard header',
|
||||
icon: GalleryIcons.menu,
|
||||
category: _kMaterialComponents,
|
||||
routeName: DrawerDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/Drawer-class.html',
|
||||
buildRoute: (BuildContext context) => DrawerDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Pagination',
|
||||
subtitle: 'PageView with indicator',
|
||||
icon: GalleryIcons.page_control,
|
||||
category: _kMaterialComponents,
|
||||
routeName: PageSelectorDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/TabBarView-class.html',
|
||||
buildRoute: (BuildContext context) => PageSelectorDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Pickers',
|
||||
subtitle: 'Date and time selection widgets',
|
||||
icon: GalleryIcons.event,
|
||||
category: _kMaterialComponents,
|
||||
routeName: DateAndTimePickerDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/showDatePicker.html',
|
||||
buildRoute: (BuildContext context) => DateAndTimePickerDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Progress indicators',
|
||||
subtitle: 'Linear, circular, indeterminate',
|
||||
icon: GalleryIcons.progress_activity,
|
||||
category: _kMaterialComponents,
|
||||
routeName: ProgressIndicatorDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/LinearProgressIndicator-class.html',
|
||||
buildRoute: (BuildContext context) => ProgressIndicatorDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Pull to refresh',
|
||||
subtitle: 'Refresh indicators',
|
||||
icon: GalleryIcons.refresh,
|
||||
category: _kMaterialComponents,
|
||||
routeName: OverscrollDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/RefreshIndicator-class.html',
|
||||
buildRoute: (BuildContext context) => const OverscrollDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Search',
|
||||
subtitle: 'Expandable search',
|
||||
icon: Icons.search,
|
||||
category: _kMaterialComponents,
|
||||
routeName: SearchDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/showSearch.html',
|
||||
buildRoute: (BuildContext context) => SearchDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Selection controls',
|
||||
subtitle: 'Checkboxes, radio buttons, and switches',
|
||||
icon: GalleryIcons.check_box,
|
||||
category: _kMaterialComponents,
|
||||
routeName: SelectionControlsDemo.routeName,
|
||||
buildRoute: (BuildContext context) => SelectionControlsDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Sliders',
|
||||
subtitle: 'Widgets for selecting a value by swiping',
|
||||
icon: GalleryIcons.sliders,
|
||||
category: _kMaterialComponents,
|
||||
routeName: SliderDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/Slider-class.html',
|
||||
buildRoute: (BuildContext context) => SliderDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Snackbar',
|
||||
subtitle: 'Temporary messaging',
|
||||
icon: GalleryIcons.snackbar,
|
||||
category: _kMaterialComponents,
|
||||
routeName: SnackBarDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/ScaffoldState/showSnackBar.html',
|
||||
buildRoute: (BuildContext context) => const SnackBarDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Tabs',
|
||||
subtitle: 'Tabs with independently scrollable views',
|
||||
icon: GalleryIcons.tabs,
|
||||
category: _kMaterialComponents,
|
||||
routeName: TabsDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/TabBarView-class.html',
|
||||
buildRoute: (BuildContext context) => TabsDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Tabs: Scrolling',
|
||||
subtitle: 'Tab bar that scrolls',
|
||||
category: _kMaterialComponents,
|
||||
icon: GalleryIcons.tabs,
|
||||
routeName: ScrollableTabsDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/TabBar-class.html',
|
||||
buildRoute: (BuildContext context) => ScrollableTabsDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Text fields',
|
||||
subtitle: 'Single line of editable text and numbers',
|
||||
icon: GalleryIcons.text_fields_alt,
|
||||
category: _kMaterialComponents,
|
||||
routeName: TextFormFieldDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/TextFormField-class.html',
|
||||
buildRoute: (BuildContext context) => const TextFormFieldDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Tooltips',
|
||||
subtitle: 'Short message displayed on long-press',
|
||||
icon: GalleryIcons.tooltip,
|
||||
category: _kMaterialComponents,
|
||||
routeName: TooltipDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/material/Tooltip-class.html',
|
||||
buildRoute: (BuildContext context) => TooltipDemo(),
|
||||
),
|
||||
|
||||
// Cupertino Components
|
||||
GalleryDemo(
|
||||
title: 'Activity Indicator',
|
||||
icon: GalleryIcons.cupertino_progress,
|
||||
category: _kCupertinoComponents,
|
||||
routeName: CupertinoProgressIndicatorDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/cupertino/CupertinoActivityIndicator-class.html',
|
||||
buildRoute: (BuildContext context) => CupertinoProgressIndicatorDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Alerts',
|
||||
icon: GalleryIcons.dialogs,
|
||||
category: _kCupertinoComponents,
|
||||
routeName: CupertinoAlertDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/cupertino/showCupertinoDialog.html',
|
||||
buildRoute: (BuildContext context) => CupertinoAlertDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Buttons',
|
||||
icon: GalleryIcons.generic_buttons,
|
||||
category: _kCupertinoComponents,
|
||||
routeName: CupertinoButtonsDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/cupertino/CupertinoButton-class.html',
|
||||
buildRoute: (BuildContext context) => CupertinoButtonsDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Navigation',
|
||||
icon: GalleryIcons.bottom_navigation,
|
||||
category: _kCupertinoComponents,
|
||||
routeName: CupertinoNavigationDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/cupertino/CupertinoTabScaffold-class.html',
|
||||
buildRoute: (BuildContext context) => CupertinoNavigationDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Pickers',
|
||||
icon: GalleryIcons.event,
|
||||
category: _kCupertinoComponents,
|
||||
routeName: CupertinoPickerDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/cupertino/CupertinoPicker-class.html',
|
||||
buildRoute: (BuildContext context) => CupertinoPickerDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Pull to refresh',
|
||||
icon: GalleryIcons.cupertino_pull_to_refresh,
|
||||
category: _kCupertinoComponents,
|
||||
routeName: CupertinoRefreshControlDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/cupertino/CupertinoSliverRefreshControl-class.html',
|
||||
buildRoute: (BuildContext context) => CupertinoRefreshControlDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Segmented Control',
|
||||
icon: GalleryIcons.tabs,
|
||||
category: _kCupertinoComponents,
|
||||
routeName: CupertinoSegmentedControlDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/cupertino/CupertinoSegmentedControl-class.html',
|
||||
buildRoute: (BuildContext context) => CupertinoSegmentedControlDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Sliders',
|
||||
icon: GalleryIcons.sliders,
|
||||
category: _kCupertinoComponents,
|
||||
routeName: CupertinoSliderDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/cupertino/CupertinoSlider-class.html',
|
||||
buildRoute: (BuildContext context) => CupertinoSliderDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Switches',
|
||||
icon: GalleryIcons.cupertino_switch,
|
||||
category: _kCupertinoComponents,
|
||||
routeName: CupertinoSwitchDemo.routeName,
|
||||
documentationUrl: 'https://docs.flutter.io/flutter/cupertino/CupertinoSwitch-class.html',
|
||||
buildRoute: (BuildContext context) => CupertinoSwitchDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Text Fields',
|
||||
icon: GalleryIcons.text_fields_alt,
|
||||
category: _kCupertinoComponents,
|
||||
routeName: CupertinoTextFieldDemo.routeName,
|
||||
buildRoute: (BuildContext context) => CupertinoTextFieldDemo(),
|
||||
),
|
||||
|
||||
// Media
|
||||
GalleryDemo(
|
||||
title: 'Animated images',
|
||||
subtitle: 'GIF and WebP animations',
|
||||
icon: GalleryIcons.animation,
|
||||
category: _kMedia,
|
||||
routeName: ImagesDemo.routeName,
|
||||
buildRoute: (BuildContext context) => ImagesDemo(),
|
||||
),
|
||||
GalleryDemo(
|
||||
title: 'Video',
|
||||
subtitle: 'Video playback',
|
||||
icon: GalleryIcons.drive_video,
|
||||
category: _kMedia,
|
||||
routeName: VideoDemo.routeName,
|
||||
buildRoute: (BuildContext context) => const VideoDemo(),
|
||||
),
|
||||
];
|
||||
return galleryDemos;
|
||||
}
|
||||
|
||||
final List<GalleryDemo> kAllGalleryDemos = _buildGalleryDemos();
|
||||
|
||||
final Set<GalleryDemoCategory> kAllGalleryDemoCategories =
|
||||
kAllGalleryDemos.map<GalleryDemoCategory>((GalleryDemo demo) => demo.category).toSet();
|
||||
|
||||
final Map<GalleryDemoCategory, List<GalleryDemo>> kGalleryCategoryToDemos =
|
||||
Map<GalleryDemoCategory, List<GalleryDemo>>.fromIterable(
|
||||
kAllGalleryDemoCategories,
|
||||
value: (dynamic category) {
|
||||
return kAllGalleryDemos.where((GalleryDemo demo) => demo.category == category).toList();
|
||||
},
|
||||
);
|
||||
|
||||
final Map<String, String> kDemoDocumentationUrl =
|
||||
Map<String, String>.fromIterable(
|
||||
kAllGalleryDemos.where((GalleryDemo demo) => demo.documentationUrl != null),
|
||||
key: (dynamic demo) => demo.routeName,
|
||||
value: (dynamic demo) => demo.documentationUrl,
|
||||
);
|
||||
@ -1,286 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// Note: This code is not runnable, it contains code snippets displayed in the
|
||||
// gallery.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ButtonsDemo {
|
||||
void setState(VoidCallback callback) { }
|
||||
BuildContext context;
|
||||
|
||||
void buttons() {
|
||||
|
||||
// START buttons_raised
|
||||
// Create a raised button.
|
||||
RaisedButton(
|
||||
child: const Text('BUTTON TITLE'),
|
||||
onPressed: () {
|
||||
// Perform some action
|
||||
},
|
||||
);
|
||||
|
||||
// Create a disabled button.
|
||||
// Buttons are disabled when onPressed isn't
|
||||
// specified or is null.
|
||||
const RaisedButton(
|
||||
child: Text('BUTTON TITLE'),
|
||||
onPressed: null,
|
||||
);
|
||||
|
||||
// Create a button with an icon and a
|
||||
// title.
|
||||
RaisedButton.icon(
|
||||
icon: const Icon(Icons.add, size: 18.0),
|
||||
label: const Text('BUTTON TITLE'),
|
||||
onPressed: () {
|
||||
// Perform some action
|
||||
},
|
||||
);
|
||||
// END
|
||||
|
||||
// START buttons_outline
|
||||
// Create an outline button.
|
||||
OutlineButton(
|
||||
child: const Text('BUTTON TITLE'),
|
||||
onPressed: () {
|
||||
// Perform some action
|
||||
},
|
||||
);
|
||||
|
||||
// Create a disabled button.
|
||||
// Buttons are disabled when onPressed isn't
|
||||
// specified or is null.
|
||||
const OutlineButton(
|
||||
child: Text('BUTTON TITLE'),
|
||||
onPressed: null,
|
||||
);
|
||||
|
||||
// Create a button with an icon and a
|
||||
// title.
|
||||
OutlineButton.icon(
|
||||
icon: const Icon(Icons.add, size: 18.0),
|
||||
label: const Text('BUTTON TITLE'),
|
||||
onPressed: () {
|
||||
// Perform some action
|
||||
},
|
||||
);
|
||||
// END
|
||||
|
||||
// START buttons_flat
|
||||
// Create a flat button.
|
||||
FlatButton(
|
||||
child: const Text('BUTTON TITLE'),
|
||||
onPressed: () {
|
||||
// Perform some action
|
||||
},
|
||||
);
|
||||
|
||||
// Create a disabled button.
|
||||
// Buttons are disabled when onPressed isn't
|
||||
// specified or is null.
|
||||
const FlatButton(
|
||||
child: Text('BUTTON TITLE'),
|
||||
onPressed: null,
|
||||
);
|
||||
// END
|
||||
|
||||
|
||||
// START buttons_dropdown
|
||||
// Member variable holding value.
|
||||
String dropdownValue;
|
||||
|
||||
// Dropdown button with string values.
|
||||
DropdownButton<String>(
|
||||
value: dropdownValue,
|
||||
onChanged: (String newValue) {
|
||||
// null indicates the user didn't select a
|
||||
// new value.
|
||||
setState(() {
|
||||
if (newValue != null)
|
||||
dropdownValue = newValue;
|
||||
});
|
||||
},
|
||||
items: <String>['One', 'Two', 'Free', 'Four']
|
||||
.map<DropdownMenuItem<String>>((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value));
|
||||
})
|
||||
.toList(),
|
||||
);
|
||||
// END
|
||||
|
||||
|
||||
// START buttons_icon
|
||||
// Member variable holding toggle value.
|
||||
bool value;
|
||||
|
||||
// Toggleable icon button.
|
||||
IconButton(
|
||||
icon: const Icon(Icons.thumb_up),
|
||||
onPressed: () {
|
||||
setState(() => value = !value);
|
||||
},
|
||||
color: value ? Theme.of(context).primaryColor : null,
|
||||
);
|
||||
// END
|
||||
|
||||
|
||||
// START buttons_action
|
||||
// Floating action button in Scaffold.
|
||||
Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Demo'),
|
||||
),
|
||||
floatingActionButton: const FloatingActionButton(
|
||||
child: Icon(Icons.add),
|
||||
onPressed: null,
|
||||
),
|
||||
);
|
||||
// END
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class SelectionControls {
|
||||
void setState(VoidCallback callback) { }
|
||||
|
||||
void selectionControls() {
|
||||
|
||||
// START selectioncontrols_checkbox
|
||||
// Member variable holding the checkbox's value.
|
||||
bool checkboxValue = false;
|
||||
|
||||
// Create a checkbox.
|
||||
Checkbox(
|
||||
value: checkboxValue,
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
checkboxValue = value;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Create a tristate checkbox.
|
||||
Checkbox(
|
||||
tristate: true,
|
||||
value: checkboxValue,
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
checkboxValue = value;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Create a disabled checkbox.
|
||||
// Checkboxes are disabled when onChanged isn't
|
||||
// specified or null.
|
||||
const Checkbox(value: false, onChanged: null);
|
||||
// END
|
||||
|
||||
|
||||
// START selectioncontrols_radio
|
||||
// Member variable holding value.
|
||||
int radioValue = 0;
|
||||
|
||||
// Method setting value.
|
||||
void handleRadioValueChanged(int value) {
|
||||
setState(() {
|
||||
radioValue = value;
|
||||
});
|
||||
}
|
||||
|
||||
// Creates a set of radio buttons.
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Radio<int>(
|
||||
value: 0,
|
||||
groupValue: radioValue,
|
||||
onChanged: handleRadioValueChanged,
|
||||
),
|
||||
Radio<int>(
|
||||
value: 1,
|
||||
groupValue: radioValue,
|
||||
onChanged: handleRadioValueChanged,
|
||||
),
|
||||
Radio<int>(
|
||||
value: 2,
|
||||
groupValue: radioValue,
|
||||
onChanged: handleRadioValueChanged,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// Creates a disabled radio button.
|
||||
const Radio<int>(
|
||||
value: 0,
|
||||
groupValue: 0,
|
||||
onChanged: null,
|
||||
);
|
||||
// END
|
||||
|
||||
|
||||
// START selectioncontrols_switch
|
||||
// Member variable holding value.
|
||||
bool switchValue = false;
|
||||
|
||||
// Create a switch.
|
||||
Switch(
|
||||
value: switchValue,
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
switchValue = value;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Create a disabled switch.
|
||||
// Switches are disabled when onChanged isn't
|
||||
// specified or null.
|
||||
const Switch(value: false, onChanged: null);
|
||||
// END
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class GridLists {
|
||||
void gridlists() {
|
||||
// START gridlists
|
||||
// Creates a scrollable grid list with images
|
||||
// loaded from the web.
|
||||
GridView.count(
|
||||
crossAxisCount: 3,
|
||||
childAspectRatio: 1.0,
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
mainAxisSpacing: 4.0,
|
||||
crossAxisSpacing: 4.0,
|
||||
children: <String>[
|
||||
'https://example.com/image-0.jpg',
|
||||
'https://example.com/image-1.jpg',
|
||||
'https://example.com/image-2.jpg',
|
||||
'...',
|
||||
'https://example.com/image-n.jpg',
|
||||
].map<Widget>((String url) {
|
||||
return GridTile(
|
||||
footer: GridTileBar(
|
||||
title: Text(url),
|
||||
),
|
||||
child: Image.network(url, fit: BoxFit.cover),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
// END
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AnimatedImage {
|
||||
void animatedImage() {
|
||||
// START animated_image
|
||||
Image.network('https://example.com/animated-image.gif');
|
||||
// END
|
||||
}
|
||||
}
|
||||
@ -1,55 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
const String _kStartTag = '// START ';
|
||||
const String _kEndTag = '// END';
|
||||
|
||||
Map<String, String> _exampleCode;
|
||||
|
||||
Future<String> getExampleCode(String tag, AssetBundle bundle) async {
|
||||
if (_exampleCode == null)
|
||||
await _parseExampleCode(bundle);
|
||||
return _exampleCode[tag];
|
||||
}
|
||||
|
||||
Future<void> _parseExampleCode(AssetBundle bundle) async {
|
||||
final String code = await bundle.loadString('lib/gallery/example_code.dart') ??
|
||||
'// lib/gallery/example_code.dart not found\n';
|
||||
_exampleCode = <String, String>{};
|
||||
|
||||
final List<String> lines = code.split('\n');
|
||||
|
||||
List<String> codeBlock;
|
||||
String codeTag;
|
||||
|
||||
for (String line in lines) {
|
||||
if (codeBlock == null) {
|
||||
// Outside a block.
|
||||
if (line.startsWith(_kStartTag)) {
|
||||
// Starting a new code block.
|
||||
codeBlock = <String>[];
|
||||
codeTag = line.substring(_kStartTag.length).trim();
|
||||
} else {
|
||||
// Just skipping the line.
|
||||
}
|
||||
} else {
|
||||
// Inside a block.
|
||||
if (line.startsWith(_kEndTag)) {
|
||||
// Add the block.
|
||||
_exampleCode[codeTag] = codeBlock.join('\n');
|
||||
codeBlock = null;
|
||||
codeTag = null;
|
||||
} else {
|
||||
// Add to the current block
|
||||
// trimRight() to remove any \r on Windows
|
||||
// without removing any useful indentation
|
||||
codeBlock.add(line.trimRight());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,410 +0,0 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||
|
||||
import 'backdrop.dart';
|
||||
import 'demos.dart';
|
||||
|
||||
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
|
||||
const Color _kFlutterBlue = Color(0xFF003D75);
|
||||
const double _kDemoItemHeight = 64.0;
|
||||
const Duration _kFrontLayerSwitchDuration = Duration(milliseconds: 300);
|
||||
|
||||
class _FlutterLogo extends StatelessWidget {
|
||||
const _FlutterLogo({ Key key }) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: 34.0,
|
||||
height: 34.0,
|
||||
decoration: const BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(
|
||||
'logos/flutter_white/logo.png',
|
||||
package: _kGalleryAssetsPackage,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CategoryItem extends StatelessWidget {
|
||||
const _CategoryItem({
|
||||
Key key,
|
||||
this.category,
|
||||
this.onTap,
|
||||
}) : super (key: key);
|
||||
|
||||
final GalleryDemoCategory category;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final bool isDark = theme.brightness == Brightness.dark;
|
||||
|
||||
// This repaint boundary prevents the entire _CategoriesPage from being
|
||||
// repainted when the button's ink splash animates.
|
||||
return RepaintBoundary(
|
||||
child: RawMaterialButton(
|
||||
padding: EdgeInsets.zero,
|
||||
hoverColor: theme.primaryColor.withOpacity(0.05),
|
||||
splashColor: theme.primaryColor.withOpacity(0.12),
|
||||
highlightColor: Colors.transparent,
|
||||
onPressed: onTap,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: Icon(
|
||||
category.icon,
|
||||
size: 60.0,
|
||||
color: isDark ? Colors.white : _kFlutterBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10.0),
|
||||
Container(
|
||||
height: 48.0,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
category.name,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.subhead.copyWith(
|
||||
fontFamily: 'GoogleSans',
|
||||
color: isDark ? Colors.white : _kFlutterBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CategoriesPage extends StatelessWidget {
|
||||
const _CategoriesPage({
|
||||
Key key,
|
||||
this.categories,
|
||||
this.onCategoryTap,
|
||||
}) : super(key: key);
|
||||
|
||||
final Iterable<GalleryDemoCategory> categories;
|
||||
final ValueChanged<GalleryDemoCategory> onCategoryTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const double aspectRatio = 160.0 / 180.0;
|
||||
final List<GalleryDemoCategory> categoriesList = categories.toList();
|
||||
final int columnCount = (MediaQuery.of(context).orientation == Orientation.portrait) ? 2 : 3;
|
||||
|
||||
return Semantics(
|
||||
scopesRoute: true,
|
||||
namesRoute: true,
|
||||
label: 'categories',
|
||||
explicitChildNodes: true,
|
||||
child: SingleChildScrollView(
|
||||
key: const PageStorageKey<String>('categories'),
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
final double columnWidth = constraints.biggest.width / columnCount.toDouble();
|
||||
final double rowHeight = math.min(225.0, columnWidth * aspectRatio);
|
||||
final int rowCount = (categories.length + columnCount - 1) ~/ columnCount;
|
||||
|
||||
// This repaint boundary prevents the inner contents of the front layer
|
||||
// from repainting when the backdrop toggle triggers a repaint on the
|
||||
// LayoutBuilder.
|
||||
return RepaintBoundary(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: List<Widget>.generate(rowCount, (int rowIndex) {
|
||||
final int columnCountForRow = rowIndex == rowCount - 1
|
||||
? categories.length - columnCount * math.max(0, rowCount - 1)
|
||||
: columnCount;
|
||||
|
||||
return Row(
|
||||
children: List<Widget>.generate(columnCountForRow, (int columnIndex) {
|
||||
final int index = rowIndex * columnCount + columnIndex;
|
||||
final GalleryDemoCategory category = categoriesList[index];
|
||||
|
||||
return SizedBox(
|
||||
width: columnWidth,
|
||||
height: rowHeight,
|
||||
child: _CategoryItem(
|
||||
category: category,
|
||||
onTap: () {
|
||||
onCategoryTap(category);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DemoItem extends StatelessWidget {
|
||||
const _DemoItem({ Key key, this.demo }) : super(key: key);
|
||||
|
||||
final GalleryDemo demo;
|
||||
|
||||
void _launchDemo(BuildContext context) {
|
||||
if (demo.routeName != null) {
|
||||
Timeline.instantSync('Start Transition', arguments: <String, String>{
|
||||
'from': '/',
|
||||
'to': demo.routeName,
|
||||
});
|
||||
Navigator.pushNamed(context, demo.routeName);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final bool isDark = theme.brightness == Brightness.dark;
|
||||
final double textScaleFactor = MediaQuery.textScaleFactorOf(context);
|
||||
return RawMaterialButton(
|
||||
padding: EdgeInsets.zero,
|
||||
splashColor: theme.primaryColor.withOpacity(0.12),
|
||||
highlightColor: Colors.transparent,
|
||||
onPressed: () {
|
||||
_launchDemo(context);
|
||||
},
|
||||
child: Container(
|
||||
constraints: BoxConstraints(minHeight: _kDemoItemHeight * textScaleFactor),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 56.0,
|
||||
height: 56.0,
|
||||
alignment: Alignment.center,
|
||||
child: Icon(
|
||||
demo.icon,
|
||||
size: 24.0,
|
||||
color: isDark ? Colors.white : _kFlutterBlue,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
demo.title,
|
||||
style: theme.textTheme.subhead.copyWith(
|
||||
color: isDark ? Colors.white : const Color(0xFF202124),
|
||||
),
|
||||
),
|
||||
if (demo.subtitle != null)
|
||||
Text(
|
||||
demo.subtitle,
|
||||
style: theme.textTheme.body1.copyWith(
|
||||
color: isDark ? Colors.white : const Color(0xFF60646B)
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 44.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DemosPage extends StatelessWidget {
|
||||
const _DemosPage(this.category);
|
||||
|
||||
final GalleryDemoCategory category;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// When overriding ListView.padding, it is necessary to manually handle
|
||||
// safe areas.
|
||||
final double windowBottomPadding = MediaQuery.of(context).padding.bottom;
|
||||
return KeyedSubtree(
|
||||
key: const ValueKey<String>('GalleryDemoList'), // So the tests can find this ListView
|
||||
child: Semantics(
|
||||
scopesRoute: true,
|
||||
namesRoute: true,
|
||||
label: category.name,
|
||||
explicitChildNodes: true,
|
||||
child: ListView(
|
||||
dragStartBehavior: DragStartBehavior.down,
|
||||
key: PageStorageKey<String>(category.name),
|
||||
padding: EdgeInsets.only(top: 8.0, bottom: windowBottomPadding),
|
||||
children: kGalleryCategoryToDemos[category].map<Widget>((GalleryDemo demo) {
|
||||
return _DemoItem(demo: demo);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GalleryHome extends StatefulWidget {
|
||||
const GalleryHome({
|
||||
Key key,
|
||||
this.testMode = false,
|
||||
this.optionsPage,
|
||||
}) : super(key: key);
|
||||
|
||||
final Widget optionsPage;
|
||||
final bool testMode;
|
||||
|
||||
// In checked mode our MaterialApp will show the default "debug" banner.
|
||||
// Otherwise show the "preview" banner.
|
||||
static bool showPreviewBanner = true;
|
||||
|
||||
@override
|
||||
_GalleryHomeState createState() => _GalleryHomeState();
|
||||
}
|
||||
|
||||
class _GalleryHomeState extends State<GalleryHome> with SingleTickerProviderStateMixin {
|
||||
static final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
AnimationController _controller;
|
||||
GalleryDemoCategory _category;
|
||||
|
||||
static Widget _topHomeLayout(Widget currentChild, List<Widget> previousChildren) {
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
...previousChildren,
|
||||
if (currentChild != null) currentChild,
|
||||
],
|
||||
alignment: Alignment.topCenter,
|
||||
);
|
||||
}
|
||||
|
||||
static const AnimatedSwitcherLayoutBuilder _centerHomeLayout = AnimatedSwitcher.defaultLayoutBuilder;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
debugLabel: 'preview banner',
|
||||
vsync: this,
|
||||
)..forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final bool isDark = theme.brightness == Brightness.dark;
|
||||
final MediaQueryData media = MediaQuery.of(context);
|
||||
final bool centerHome = media.orientation == Orientation.portrait && media.size.height < 800.0;
|
||||
|
||||
const Curve switchOutCurve = Interval(0.4, 1.0, curve: Curves.fastOutSlowIn);
|
||||
const Curve switchInCurve = Interval(0.4, 1.0, curve: Curves.fastOutSlowIn);
|
||||
|
||||
Widget home = Scaffold(
|
||||
key: _scaffoldKey,
|
||||
backgroundColor: isDark ? _kFlutterBlue : theme.primaryColor,
|
||||
body: SafeArea(
|
||||
bottom: false,
|
||||
child: WillPopScope(
|
||||
onWillPop: () {
|
||||
// Pop the category page if Android back button is pressed.
|
||||
if (_category != null) {
|
||||
setState(() => _category = null);
|
||||
return Future<bool>.value(false);
|
||||
}
|
||||
return Future<bool>.value(true);
|
||||
},
|
||||
child: Backdrop(
|
||||
backTitle: const Text('Options'),
|
||||
backLayer: widget.optionsPage,
|
||||
frontAction: AnimatedSwitcher(
|
||||
duration: _kFrontLayerSwitchDuration,
|
||||
switchOutCurve: switchOutCurve,
|
||||
switchInCurve: switchInCurve,
|
||||
child: _category == null
|
||||
? const _FlutterLogo()
|
||||
: IconButton(
|
||||
icon: const BackButtonIcon(),
|
||||
tooltip: 'Back',
|
||||
onPressed: () => setState(() => _category = null),
|
||||
),
|
||||
),
|
||||
frontTitle: AnimatedSwitcher(
|
||||
duration: _kFrontLayerSwitchDuration,
|
||||
child: _category == null
|
||||
? const Text('Flutter gallery')
|
||||
: Text(_category.name),
|
||||
),
|
||||
frontHeading: widget.testMode ? null : Container(height: 24.0),
|
||||
frontLayer: AnimatedSwitcher(
|
||||
duration: _kFrontLayerSwitchDuration,
|
||||
switchOutCurve: switchOutCurve,
|
||||
switchInCurve: switchInCurve,
|
||||
layoutBuilder: centerHome ? _centerHomeLayout : _topHomeLayout,
|
||||
child: _category != null
|
||||
? _DemosPage(_category)
|
||||
: _CategoriesPage(
|
||||
categories: kAllGalleryDemoCategories,
|
||||
onCategoryTap: (GalleryDemoCategory category) {
|
||||
setState(() => _category = category);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
assert(() {
|
||||
GalleryHome.showPreviewBanner = false;
|
||||
return true;
|
||||
}());
|
||||
|
||||
if (GalleryHome.showPreviewBanner) {
|
||||
home = Stack(
|
||||
fit: StackFit.expand,
|
||||
children: <Widget>[
|
||||
home,
|
||||
FadeTransition(
|
||||
opacity: CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
||||
child: const Banner(
|
||||
message: 'PREVIEW',
|
||||
location: BannerLocation.topEnd,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
home = AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
child: home,
|
||||
value: SystemUiOverlayStyle.light,
|
||||
);
|
||||
|
||||
return home;
|
||||
}
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class GalleryIcons {
|
||||
GalleryIcons._();
|
||||
|
||||
static const IconData tooltip = IconData(0xe900, fontFamily: 'GalleryIcons');
|
||||
static const IconData text_fields_alt = IconData(0xe901, fontFamily: 'GalleryIcons');
|
||||
static const IconData tabs = IconData(0xe902, fontFamily: 'GalleryIcons');
|
||||
static const IconData switches = IconData(0xe903, fontFamily: 'GalleryIcons');
|
||||
static const IconData sliders = IconData(0xe904, fontFamily: 'GalleryIcons');
|
||||
static const IconData shrine = IconData(0xe905, fontFamily: 'GalleryIcons');
|
||||
static const IconData sentiment_very_satisfied = IconData(0xe906, fontFamily: 'GalleryIcons');
|
||||
static const IconData refresh = IconData(0xe907, fontFamily: 'GalleryIcons');
|
||||
static const IconData progress_activity = IconData(0xe908, fontFamily: 'GalleryIcons');
|
||||
static const IconData phone_iphone = IconData(0xe909, fontFamily: 'GalleryIcons');
|
||||
static const IconData page_control = IconData(0xe90a, fontFamily: 'GalleryIcons');
|
||||
static const IconData more_vert = IconData(0xe90b, fontFamily: 'GalleryIcons');
|
||||
static const IconData menu = IconData(0xe90c, fontFamily: 'GalleryIcons');
|
||||
static const IconData list_alt = IconData(0xe90d, fontFamily: 'GalleryIcons');
|
||||
static const IconData grid_on = IconData(0xe90e, fontFamily: 'GalleryIcons');
|
||||
static const IconData expand_all = IconData(0xe90f, fontFamily: 'GalleryIcons');
|
||||
static const IconData event = IconData(0xe910, fontFamily: 'GalleryIcons');
|
||||
static const IconData drive_video = IconData(0xe911, fontFamily: 'GalleryIcons');
|
||||
static const IconData dialogs = IconData(0xe912, fontFamily: 'GalleryIcons');
|
||||
static const IconData data_table = IconData(0xe913, fontFamily: 'GalleryIcons');
|
||||
static const IconData custom_typography = IconData(0xe914, fontFamily: 'GalleryIcons');
|
||||
static const IconData colors = IconData(0xe915, fontFamily: 'GalleryIcons');
|
||||
static const IconData chips = IconData(0xe916, fontFamily: 'GalleryIcons');
|
||||
static const IconData check_box = IconData(0xe917, fontFamily: 'GalleryIcons');
|
||||
static const IconData cards = IconData(0xe918, fontFamily: 'GalleryIcons');
|
||||
static const IconData buttons = IconData(0xe919, fontFamily: 'GalleryIcons');
|
||||
static const IconData bottom_sheets = IconData(0xe91a, fontFamily: 'GalleryIcons');
|
||||
static const IconData bottom_navigation = IconData(0xe91b, fontFamily: 'GalleryIcons');
|
||||
static const IconData animation = IconData(0xe91c, fontFamily: 'GalleryIcons');
|
||||
static const IconData account_box = IconData(0xe91d, fontFamily: 'GalleryIcons');
|
||||
static const IconData snackbar = IconData(0xe91e, fontFamily: 'GalleryIcons');
|
||||
static const IconData category_mdc = IconData(0xe91f, fontFamily: 'GalleryIcons');
|
||||
static const IconData cupertino_progress = IconData(0xe920, fontFamily: 'GalleryIcons');
|
||||
static const IconData cupertino_pull_to_refresh = IconData(0xe921, fontFamily: 'GalleryIcons');
|
||||
static const IconData cupertino_switch = IconData(0xe922, fontFamily: 'GalleryIcons');
|
||||
static const IconData generic_buttons = IconData(0xe923, fontFamily: 'GalleryIcons');
|
||||
static const IconData backdrop = IconData(0xe924, fontFamily: 'GalleryIcons');
|
||||
static const IconData bottom_app_bar = IconData(0xe925, fontFamily: 'GalleryIcons');
|
||||
static const IconData bottom_sheet_persistent = IconData(0xe926, fontFamily: 'GalleryIcons');
|
||||
static const IconData lists_leave_behind = IconData(0xe927, fontFamily: 'GalleryIcons');
|
||||
}
|
||||
@ -1,487 +0,0 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'about.dart';
|
||||
import 'scales.dart';
|
||||
|
||||
class GalleryOptions {
|
||||
GalleryOptions({
|
||||
this.themeMode,
|
||||
this.textScaleFactor,
|
||||
this.textDirection = TextDirection.ltr,
|
||||
this.timeDilation = 1.0,
|
||||
this.platform,
|
||||
this.showOffscreenLayersCheckerboard = false,
|
||||
this.showRasterCacheImagesCheckerboard = false,
|
||||
this.showPerformanceOverlay = false,
|
||||
});
|
||||
|
||||
final ThemeMode themeMode;
|
||||
final GalleryTextScaleValue textScaleFactor;
|
||||
final TextDirection textDirection;
|
||||
final double timeDilation;
|
||||
final TargetPlatform platform;
|
||||
final bool showPerformanceOverlay;
|
||||
final bool showRasterCacheImagesCheckerboard;
|
||||
final bool showOffscreenLayersCheckerboard;
|
||||
|
||||
GalleryOptions copyWith({
|
||||
ThemeMode themeMode,
|
||||
GalleryTextScaleValue textScaleFactor,
|
||||
TextDirection textDirection,
|
||||
double timeDilation,
|
||||
TargetPlatform platform,
|
||||
bool showPerformanceOverlay,
|
||||
bool showRasterCacheImagesCheckerboard,
|
||||
bool showOffscreenLayersCheckerboard,
|
||||
}) {
|
||||
return GalleryOptions(
|
||||
themeMode: themeMode ?? this.themeMode,
|
||||
textScaleFactor: textScaleFactor ?? this.textScaleFactor,
|
||||
textDirection: textDirection ?? this.textDirection,
|
||||
timeDilation: timeDilation ?? this.timeDilation,
|
||||
platform: platform ?? this.platform,
|
||||
showPerformanceOverlay: showPerformanceOverlay ?? this.showPerformanceOverlay,
|
||||
showOffscreenLayersCheckerboard: showOffscreenLayersCheckerboard ?? this.showOffscreenLayersCheckerboard,
|
||||
showRasterCacheImagesCheckerboard: showRasterCacheImagesCheckerboard ?? this.showRasterCacheImagesCheckerboard,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
if (runtimeType != other.runtimeType)
|
||||
return false;
|
||||
final GalleryOptions typedOther = other;
|
||||
return themeMode == typedOther.themeMode
|
||||
&& textScaleFactor == typedOther.textScaleFactor
|
||||
&& textDirection == typedOther.textDirection
|
||||
&& platform == typedOther.platform
|
||||
&& showPerformanceOverlay == typedOther.showPerformanceOverlay
|
||||
&& showRasterCacheImagesCheckerboard == typedOther.showRasterCacheImagesCheckerboard
|
||||
&& showOffscreenLayersCheckerboard == typedOther.showRasterCacheImagesCheckerboard;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(
|
||||
themeMode,
|
||||
textScaleFactor,
|
||||
textDirection,
|
||||
timeDilation,
|
||||
platform,
|
||||
showPerformanceOverlay,
|
||||
showRasterCacheImagesCheckerboard,
|
||||
showOffscreenLayersCheckerboard,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '$runtimeType($themeMode)';
|
||||
}
|
||||
}
|
||||
|
||||
const double _kItemHeight = 48.0;
|
||||
const EdgeInsetsDirectional _kItemPadding = EdgeInsetsDirectional.only(start: 56.0);
|
||||
|
||||
class _OptionsItem extends StatelessWidget {
|
||||
const _OptionsItem({ Key key, this.child }) : super(key: key);
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double textScaleFactor = MediaQuery.textScaleFactorOf(context);
|
||||
|
||||
return MergeSemantics(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(minHeight: _kItemHeight * textScaleFactor),
|
||||
padding: _kItemPadding,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: DefaultTextStyle(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.fade,
|
||||
child: IconTheme(
|
||||
data: Theme.of(context).primaryIconTheme,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BooleanItem extends StatelessWidget {
|
||||
const _BooleanItem(this.title, this.value, this.onChanged, { this.switchKey });
|
||||
|
||||
final String title;
|
||||
final bool value;
|
||||
final ValueChanged<bool> onChanged;
|
||||
// [switchKey] is used for accessing the switch from driver tests.
|
||||
final Key switchKey;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
return _OptionsItem(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Expanded(child: Text(title)),
|
||||
Switch(
|
||||
key: switchKey,
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
activeColor: const Color(0xFF39CEFD),
|
||||
activeTrackColor: isDark ? Colors.white30 : Colors.black26,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActionItem extends StatelessWidget {
|
||||
const _ActionItem(this.text, this.onTap);
|
||||
|
||||
final String text;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _OptionsItem(
|
||||
child: _FlatButton(
|
||||
onPressed: onTap,
|
||||
child: Text(text),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FlatButton extends StatelessWidget {
|
||||
const _FlatButton({ Key key, this.onPressed, this.child }) : super(key: key);
|
||||
|
||||
final VoidCallback onPressed;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlatButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: onPressed,
|
||||
child: DefaultTextStyle(
|
||||
style: Theme.of(context).primaryTextTheme.subhead,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Heading extends StatelessWidget {
|
||||
const _Heading(this.text);
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return _OptionsItem(
|
||||
child: DefaultTextStyle(
|
||||
style: theme.textTheme.body1.copyWith(
|
||||
fontFamily: 'GoogleSans',
|
||||
color: theme.accentColor,
|
||||
),
|
||||
child: Semantics(
|
||||
child: Text(text),
|
||||
header: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ThemeModeItem extends StatelessWidget {
|
||||
const _ThemeModeItem(this.options, this.onOptionsChanged);
|
||||
|
||||
final GalleryOptions options;
|
||||
final ValueChanged<GalleryOptions> onOptionsChanged;
|
||||
|
||||
static final Map<ThemeMode, String> modeLabels = <ThemeMode, String>{
|
||||
ThemeMode.system: 'System Default',
|
||||
ThemeMode.light: 'Light',
|
||||
ThemeMode.dark: 'Dark',
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _OptionsItem(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const Text('Theme'),
|
||||
Text(
|
||||
'${modeLabels[options.themeMode]}',
|
||||
style: Theme.of(context).primaryTextTheme.body1,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuButton<ThemeMode>(
|
||||
padding: const EdgeInsetsDirectional.only(end: 16.0),
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
initialValue: options.themeMode,
|
||||
itemBuilder: (BuildContext context) {
|
||||
return ThemeMode.values.map<PopupMenuItem<ThemeMode>>((ThemeMode mode) {
|
||||
return PopupMenuItem<ThemeMode>(
|
||||
value: mode,
|
||||
child: Text(modeLabels[mode]),
|
||||
);
|
||||
}).toList();
|
||||
},
|
||||
onSelected: (ThemeMode mode) {
|
||||
onOptionsChanged(
|
||||
options.copyWith(themeMode: mode),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TextScaleFactorItem extends StatelessWidget {
|
||||
const _TextScaleFactorItem(this.options, this.onOptionsChanged);
|
||||
|
||||
final GalleryOptions options;
|
||||
final ValueChanged<GalleryOptions> onOptionsChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _OptionsItem(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const Text('Text size'),
|
||||
Text(
|
||||
'${options.textScaleFactor.label}',
|
||||
style: Theme.of(context).primaryTextTheme.body1,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuButton<GalleryTextScaleValue>(
|
||||
padding: const EdgeInsetsDirectional.only(end: 16.0),
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
itemBuilder: (BuildContext context) {
|
||||
return kAllGalleryTextScaleValues.map<PopupMenuItem<GalleryTextScaleValue>>((GalleryTextScaleValue scaleValue) {
|
||||
return PopupMenuItem<GalleryTextScaleValue>(
|
||||
value: scaleValue,
|
||||
child: Text(scaleValue.label),
|
||||
);
|
||||
}).toList();
|
||||
},
|
||||
onSelected: (GalleryTextScaleValue scaleValue) {
|
||||
onOptionsChanged(
|
||||
options.copyWith(textScaleFactor: scaleValue),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TextDirectionItem extends StatelessWidget {
|
||||
const _TextDirectionItem(this.options, this.onOptionsChanged);
|
||||
|
||||
final GalleryOptions options;
|
||||
final ValueChanged<GalleryOptions> onOptionsChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BooleanItem(
|
||||
'Force RTL',
|
||||
options.textDirection == TextDirection.rtl,
|
||||
(bool value) {
|
||||
onOptionsChanged(
|
||||
options.copyWith(
|
||||
textDirection: value ? TextDirection.rtl : TextDirection.ltr,
|
||||
),
|
||||
);
|
||||
},
|
||||
switchKey: const Key('text_direction'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TimeDilationItem extends StatelessWidget {
|
||||
const _TimeDilationItem(this.options, this.onOptionsChanged);
|
||||
|
||||
final GalleryOptions options;
|
||||
final ValueChanged<GalleryOptions> onOptionsChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BooleanItem(
|
||||
'Slow motion',
|
||||
options.timeDilation != 1.0,
|
||||
(bool value) {
|
||||
onOptionsChanged(
|
||||
options.copyWith(
|
||||
timeDilation: value ? 20.0 : 1.0,
|
||||
),
|
||||
);
|
||||
},
|
||||
switchKey: const Key('slow_motion'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PlatformItem extends StatelessWidget {
|
||||
const _PlatformItem(this.options, this.onOptionsChanged);
|
||||
|
||||
final GalleryOptions options;
|
||||
final ValueChanged<GalleryOptions> onOptionsChanged;
|
||||
|
||||
String _platformLabel(TargetPlatform platform) {
|
||||
switch(platform) {
|
||||
case TargetPlatform.android:
|
||||
return 'Mountain View';
|
||||
case TargetPlatform.fuchsia:
|
||||
return 'Fuchsia';
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
return 'Cupertino';
|
||||
}
|
||||
assert(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _OptionsItem(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const Text('Platform mechanics'),
|
||||
Text(
|
||||
'${_platformLabel(options.platform)}',
|
||||
style: Theme.of(context).primaryTextTheme.body1,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuButton<TargetPlatform>(
|
||||
padding: const EdgeInsetsDirectional.only(end: 16.0),
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
itemBuilder: (BuildContext context) {
|
||||
return TargetPlatform.values.map((TargetPlatform platform) {
|
||||
return PopupMenuItem<TargetPlatform>(
|
||||
value: platform,
|
||||
child: Text(_platformLabel(platform)),
|
||||
);
|
||||
}).toList();
|
||||
},
|
||||
onSelected: (TargetPlatform platform) {
|
||||
onOptionsChanged(
|
||||
options.copyWith(platform: platform),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GalleryOptionsPage extends StatelessWidget {
|
||||
const GalleryOptionsPage({
|
||||
Key key,
|
||||
this.options,
|
||||
this.onOptionsChanged,
|
||||
this.onSendFeedback,
|
||||
}) : super(key: key);
|
||||
|
||||
final GalleryOptions options;
|
||||
final ValueChanged<GalleryOptions> onOptionsChanged;
|
||||
final VoidCallback onSendFeedback;
|
||||
|
||||
List<Widget> _enabledDiagnosticItems() {
|
||||
// Boolean showFoo options with a value of null: don't display
|
||||
// the showFoo option at all.
|
||||
if (options.showOffscreenLayersCheckerboard == null &&
|
||||
options.showRasterCacheImagesCheckerboard == null &&
|
||||
options.showPerformanceOverlay == null)
|
||||
return const <Widget>[];
|
||||
|
||||
return <Widget>[
|
||||
const Divider(),
|
||||
const _Heading('Diagnostics'),
|
||||
if (options.showOffscreenLayersCheckerboard != null)
|
||||
_BooleanItem(
|
||||
'Highlight offscreen layers',
|
||||
options.showOffscreenLayersCheckerboard,
|
||||
(bool value) {
|
||||
onOptionsChanged(options.copyWith(showOffscreenLayersCheckerboard: value));
|
||||
},
|
||||
),
|
||||
if (options.showRasterCacheImagesCheckerboard != null)
|
||||
_BooleanItem(
|
||||
'Highlight raster cache images',
|
||||
options.showRasterCacheImagesCheckerboard,
|
||||
(bool value) {
|
||||
onOptionsChanged(options.copyWith(showRasterCacheImagesCheckerboard: value));
|
||||
},
|
||||
),
|
||||
if (options.showPerformanceOverlay != null)
|
||||
_BooleanItem(
|
||||
'Show performance overlay',
|
||||
options.showPerformanceOverlay,
|
||||
(bool value) {
|
||||
onOptionsChanged(options.copyWith(showPerformanceOverlay: value));
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
|
||||
return DefaultTextStyle(
|
||||
style: theme.primaryTextTheme.subhead,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.only(bottom: 124.0),
|
||||
children: <Widget>[
|
||||
const _Heading('Display'),
|
||||
_ThemeModeItem(options, onOptionsChanged),
|
||||
_TextScaleFactorItem(options, onOptionsChanged),
|
||||
_TextDirectionItem(options, onOptionsChanged),
|
||||
_TimeDilationItem(options, onOptionsChanged),
|
||||
const Divider(),
|
||||
const _Heading('Platform mechanics'),
|
||||
_PlatformItem(options, onOptionsChanged),
|
||||
..._enabledDiagnosticItems(),
|
||||
const Divider(),
|
||||
const _Heading('Flutter gallery'),
|
||||
_ActionItem('About Flutter Gallery', () {
|
||||
showGalleryAboutDialog(context);
|
||||
}),
|
||||
_ActionItem('Send feedback', onSendFeedback),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,37 +0,0 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class GalleryTextScaleValue {
|
||||
const GalleryTextScaleValue(this.scale, this.label);
|
||||
|
||||
final double scale;
|
||||
final String label;
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
if (runtimeType != other.runtimeType)
|
||||
return false;
|
||||
final GalleryTextScaleValue typedOther = other;
|
||||
return scale == typedOther.scale && label == typedOther.label;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(scale, label);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '$runtimeType($label)';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const List<GalleryTextScaleValue> kAllGalleryTextScaleValues = <GalleryTextScaleValue>[
|
||||
GalleryTextScaleValue(null, 'System Default'),
|
||||
GalleryTextScaleValue(0.8, 'Small'),
|
||||
GalleryTextScaleValue(1.0, 'Normal'),
|
||||
GalleryTextScaleValue(1.3, 'Large'),
|
||||
GalleryTextScaleValue(2.0, 'Huge'),
|
||||
];
|
||||
@ -1,356 +0,0 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:string_scanner/string_scanner.dart';
|
||||
|
||||
class SyntaxHighlighterStyle {
|
||||
SyntaxHighlighterStyle({
|
||||
this.baseStyle,
|
||||
this.numberStyle,
|
||||
this.commentStyle,
|
||||
this.keywordStyle,
|
||||
this.stringStyle,
|
||||
this.punctuationStyle,
|
||||
this.classStyle,
|
||||
this.constantStyle,
|
||||
});
|
||||
|
||||
static SyntaxHighlighterStyle lightThemeStyle() {
|
||||
return SyntaxHighlighterStyle(
|
||||
baseStyle: const TextStyle(color: Color(0xFF000000)),
|
||||
numberStyle: const TextStyle(color: Color(0xFF1565C0)),
|
||||
commentStyle: const TextStyle(color: Color(0xFF9E9E9E)),
|
||||
keywordStyle: const TextStyle(color: Color(0xFF9C27B0)),
|
||||
stringStyle: const TextStyle(color: Color(0xFF43A047)),
|
||||
punctuationStyle: const TextStyle(color: Color(0xFF000000)),
|
||||
classStyle: const TextStyle(color: Color(0xFF512DA8)),
|
||||
constantStyle: const TextStyle(color: Color(0xFF795548)),
|
||||
);
|
||||
}
|
||||
|
||||
static SyntaxHighlighterStyle darkThemeStyle() {
|
||||
return SyntaxHighlighterStyle(
|
||||
baseStyle: const TextStyle(color: Color(0xFFFFFFFF)),
|
||||
numberStyle: const TextStyle(color: Color(0xFF1565C0)),
|
||||
commentStyle: const TextStyle(color: Color(0xFF9E9E9E)),
|
||||
keywordStyle: const TextStyle(color: Color(0xFF80CBC4)),
|
||||
stringStyle: const TextStyle(color: Color(0xFF009688)),
|
||||
punctuationStyle: const TextStyle(color: Color(0xFFFFFFFF)),
|
||||
classStyle: const TextStyle(color: Color(0xFF009688)),
|
||||
constantStyle: const TextStyle(color: Color(0xFF795548)),
|
||||
);
|
||||
}
|
||||
|
||||
final TextStyle baseStyle;
|
||||
final TextStyle numberStyle;
|
||||
final TextStyle commentStyle;
|
||||
final TextStyle keywordStyle;
|
||||
final TextStyle stringStyle;
|
||||
final TextStyle punctuationStyle;
|
||||
final TextStyle classStyle;
|
||||
final TextStyle constantStyle;
|
||||
}
|
||||
|
||||
abstract class SyntaxHighlighter {
|
||||
TextSpan format(String src);
|
||||
}
|
||||
|
||||
class DartSyntaxHighlighter extends SyntaxHighlighter {
|
||||
DartSyntaxHighlighter([this._style]) {
|
||||
_spans = <_HighlightSpan>[];
|
||||
_style ??= SyntaxHighlighterStyle.darkThemeStyle();
|
||||
}
|
||||
|
||||
SyntaxHighlighterStyle _style;
|
||||
|
||||
static const List<String> _keywords = <String>[
|
||||
'abstract', 'as', 'assert', 'async', 'await', 'break', 'case', 'catch',
|
||||
'class', 'const', 'continue', 'default', 'deferred', 'do', 'dynamic', 'else',
|
||||
'enum', 'export', 'external', 'extends', 'factory', 'false', 'final',
|
||||
'finally', 'for', 'get', 'if', 'implements', 'import', 'in', 'is', 'library',
|
||||
'new', 'null', 'operator', 'part', 'rethrow', 'return', 'set', 'static',
|
||||
'super', 'switch', 'sync', 'this', 'throw', 'true', 'try', 'typedef', 'var',
|
||||
'void', 'while', 'with', 'yield',
|
||||
];
|
||||
|
||||
static const List<String> _builtInTypes = <String>[
|
||||
'int', 'double', 'num', 'bool',
|
||||
];
|
||||
|
||||
String _src;
|
||||
StringScanner _scanner;
|
||||
|
||||
List<_HighlightSpan> _spans;
|
||||
|
||||
@override
|
||||
TextSpan format(String src) {
|
||||
_src = src;
|
||||
_scanner = StringScanner(_src);
|
||||
|
||||
if (_generateSpans()) {
|
||||
// Successfully parsed the code
|
||||
final List<TextSpan> formattedText = <TextSpan>[];
|
||||
int currentPosition = 0;
|
||||
|
||||
for (_HighlightSpan span in _spans) {
|
||||
if (currentPosition != span.start)
|
||||
formattedText.add(TextSpan(text: _src.substring(currentPosition, span.start)));
|
||||
|
||||
formattedText.add(TextSpan(style: span.textStyle(_style), text: span.textForSpan(_src)));
|
||||
|
||||
currentPosition = span.end;
|
||||
}
|
||||
|
||||
if (currentPosition != _src.length)
|
||||
formattedText.add(TextSpan(text: _src.substring(currentPosition, _src.length)));
|
||||
|
||||
return TextSpan(style: _style.baseStyle, children: formattedText);
|
||||
} else {
|
||||
// Parsing failed, return with only basic formatting
|
||||
return TextSpan(style: _style.baseStyle, text: src);
|
||||
}
|
||||
}
|
||||
|
||||
bool _generateSpans() {
|
||||
int lastLoopPosition = _scanner.position;
|
||||
|
||||
while (!_scanner.isDone) {
|
||||
// Skip White space
|
||||
_scanner.scan(RegExp(r'\s+'));
|
||||
|
||||
// Block comments
|
||||
if (_scanner.scan(RegExp(r'/\*(.|\n)*\*/'))) {
|
||||
_spans.add(_HighlightSpan(
|
||||
_HighlightType.comment,
|
||||
_scanner.lastMatch.start,
|
||||
_scanner.lastMatch.end,
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Line comments
|
||||
if (_scanner.scan('//')) {
|
||||
final int startComment = _scanner.lastMatch.start;
|
||||
|
||||
bool eof = false;
|
||||
int endComment;
|
||||
if (_scanner.scan(RegExp(r'.*\n'))) {
|
||||
endComment = _scanner.lastMatch.end - 1;
|
||||
} else {
|
||||
eof = true;
|
||||
endComment = _src.length;
|
||||
}
|
||||
|
||||
_spans.add(_HighlightSpan(
|
||||
_HighlightType.comment,
|
||||
startComment,
|
||||
endComment,
|
||||
));
|
||||
|
||||
if (eof)
|
||||
break;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Raw r"String"
|
||||
if (_scanner.scan(RegExp(r'r".*"'))) {
|
||||
_spans.add(_HighlightSpan(
|
||||
_HighlightType.string,
|
||||
_scanner.lastMatch.start,
|
||||
_scanner.lastMatch.end,
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Raw r'String'
|
||||
if (_scanner.scan(RegExp(r"r'.*'"))) {
|
||||
_spans.add(_HighlightSpan(
|
||||
_HighlightType.string,
|
||||
_scanner.lastMatch.start,
|
||||
_scanner.lastMatch.end,
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Multiline """String"""
|
||||
if (_scanner.scan(RegExp(r'"""(?:[^"\\]|\\(.|\n))*"""'))) {
|
||||
_spans.add(_HighlightSpan(
|
||||
_HighlightType.string,
|
||||
_scanner.lastMatch.start,
|
||||
_scanner.lastMatch.end,
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Multiline '''String'''
|
||||
if (_scanner.scan(RegExp(r"'''(?:[^'\\]|\\(.|\n))*'''"))) {
|
||||
_spans.add(_HighlightSpan(
|
||||
_HighlightType.string,
|
||||
_scanner.lastMatch.start,
|
||||
_scanner.lastMatch.end,
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
// "String"
|
||||
if (_scanner.scan(RegExp(r'"(?:[^"\\]|\\.)*"'))) {
|
||||
_spans.add(_HighlightSpan(
|
||||
_HighlightType.string,
|
||||
_scanner.lastMatch.start,
|
||||
_scanner.lastMatch.end,
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 'String'
|
||||
if (_scanner.scan(RegExp(r"'(?:[^'\\]|\\.)*'"))) {
|
||||
_spans.add(_HighlightSpan(
|
||||
_HighlightType.string,
|
||||
_scanner.lastMatch.start,
|
||||
_scanner.lastMatch.end,
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Double
|
||||
if (_scanner.scan(RegExp(r'\d+\.\d+'))) {
|
||||
_spans.add(_HighlightSpan(
|
||||
_HighlightType.number,
|
||||
_scanner.lastMatch.start,
|
||||
_scanner.lastMatch.end,
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Integer
|
||||
if (_scanner.scan(RegExp(r'\d+'))) {
|
||||
_spans.add(_HighlightSpan(
|
||||
_HighlightType.number,
|
||||
_scanner.lastMatch.start,
|
||||
_scanner.lastMatch.end)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Punctuation
|
||||
if (_scanner.scan(RegExp(r'[\[\]{}().!=<>&\|\?\+\-\*/%\^~;:,]'))) {
|
||||
_spans.add(_HighlightSpan(
|
||||
_HighlightType.punctuation,
|
||||
_scanner.lastMatch.start,
|
||||
_scanner.lastMatch.end,
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Meta data
|
||||
if (_scanner.scan(RegExp(r'@\w+'))) {
|
||||
_spans.add(_HighlightSpan(
|
||||
_HighlightType.keyword,
|
||||
_scanner.lastMatch.start,
|
||||
_scanner.lastMatch.end,
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Words
|
||||
if (_scanner.scan(RegExp(r'\w+'))) {
|
||||
_HighlightType type;
|
||||
|
||||
String word = _scanner.lastMatch[0];
|
||||
if (word.startsWith('_'))
|
||||
word = word.substring(1);
|
||||
|
||||
if (_keywords.contains(word))
|
||||
type = _HighlightType.keyword;
|
||||
else if (_builtInTypes.contains(word))
|
||||
type = _HighlightType.keyword;
|
||||
else if (_firstLetterIsUpperCase(word))
|
||||
type = _HighlightType.klass;
|
||||
else if (word.length >= 2 && word.startsWith('k') && _firstLetterIsUpperCase(word.substring(1)))
|
||||
type = _HighlightType.constant;
|
||||
|
||||
if (type != null) {
|
||||
_spans.add(_HighlightSpan(
|
||||
type,
|
||||
_scanner.lastMatch.start,
|
||||
_scanner.lastMatch.end,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this loop did anything
|
||||
if (lastLoopPosition == _scanner.position) {
|
||||
// Failed to parse this file, abort gracefully
|
||||
return false;
|
||||
}
|
||||
lastLoopPosition = _scanner.position;
|
||||
}
|
||||
|
||||
_simplify();
|
||||
return true;
|
||||
}
|
||||
|
||||
void _simplify() {
|
||||
for (int i = _spans.length - 2; i >= 0; i -= 1) {
|
||||
if (_spans[i].type == _spans[i + 1].type && _spans[i].end == _spans[i + 1].start) {
|
||||
_spans[i] = _HighlightSpan(
|
||||
_spans[i].type,
|
||||
_spans[i].start,
|
||||
_spans[i + 1].end,
|
||||
);
|
||||
_spans.removeAt(i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool _firstLetterIsUpperCase(String str) {
|
||||
if (str.isNotEmpty) {
|
||||
final String first = str.substring(0, 1);
|
||||
return first == first.toUpperCase();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
enum _HighlightType {
|
||||
number,
|
||||
comment,
|
||||
keyword,
|
||||
string,
|
||||
punctuation,
|
||||
klass,
|
||||
constant
|
||||
}
|
||||
|
||||
class _HighlightSpan {
|
||||
_HighlightSpan(this.type, this.start, this.end);
|
||||
final _HighlightType type;
|
||||
final int start;
|
||||
final int end;
|
||||
|
||||
String textForSpan(String src) {
|
||||
return src.substring(start, end);
|
||||
}
|
||||
|
||||
TextStyle textStyle(SyntaxHighlighterStyle style) {
|
||||
if (type == _HighlightType.number)
|
||||
return style.numberStyle;
|
||||
else if (type == _HighlightType.comment)
|
||||
return style.commentStyle;
|
||||
else if (type == _HighlightType.keyword)
|
||||
return style.keywordStyle;
|
||||
else if (type == _HighlightType.string)
|
||||
return style.stringStyle;
|
||||
else if (type == _HighlightType.punctuation)
|
||||
return style.punctuationStyle;
|
||||
else if (type == _HighlightType.klass)
|
||||
return style.classStyle;
|
||||
else if (type == _HighlightType.constant)
|
||||
return style.constantStyle;
|
||||
else
|
||||
return style.baseStyle;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue