mirror of
https://github.com/flutter/samples.git
synced 2026-05-16 20:08:51 +00:00
[Gallery] Fix directory structure (#312)
This commit is contained in:
67
gallery/lib/studies/crane/app.dart
Normal file
67
gallery/lib/studies/crane/app.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright 2019 The Flutter team. All rights 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:gallery/data/gallery_options.dart';
|
||||
import 'package:gallery/l10n/gallery_localizations.dart';
|
||||
import 'package:gallery/studies/crane/backdrop.dart';
|
||||
import 'package:gallery/studies/crane/eat_form.dart';
|
||||
import 'package:gallery/studies/crane/fly_form.dart';
|
||||
import 'package:gallery/studies/crane/sleep_form.dart';
|
||||
import 'package:gallery/studies/crane/theme.dart';
|
||||
|
||||
class CraneApp extends StatefulWidget {
|
||||
const CraneApp({Key key, this.navigatorKey}) : super(key: key);
|
||||
|
||||
final GlobalKey<NavigatorState> navigatorKey;
|
||||
|
||||
@override
|
||||
_CraneAppState createState() => _CraneAppState();
|
||||
}
|
||||
|
||||
class _CraneAppState extends State<CraneApp> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
navigatorKey: widget.navigatorKey,
|
||||
title: 'Crane',
|
||||
debugShowCheckedModeBanner: false,
|
||||
localizationsDelegates: GalleryLocalizations.localizationsDelegates,
|
||||
supportedLocales: GalleryLocalizations.supportedLocales,
|
||||
locale: GalleryOptions.of(context).locale,
|
||||
initialRoute: '/',
|
||||
onGenerateRoute: _getRoute,
|
||||
theme: craneTheme.copyWith(
|
||||
platform: GalleryOptions.of(context).platform,
|
||||
),
|
||||
darkTheme: craneTheme.copyWith(
|
||||
platform: GalleryOptions.of(context).platform,
|
||||
),
|
||||
home: ApplyTextOptions(
|
||||
child: Backdrop(
|
||||
frontLayer: Container(),
|
||||
backLayerItems: [
|
||||
FlyForm(index: 0),
|
||||
SleepForm(index: 1),
|
||||
EatForm(index: 2),
|
||||
],
|
||||
frontTitle: Text('CRANE'),
|
||||
backTitle: Text('MENU'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Route<dynamic> _getRoute(RouteSettings settings) {
|
||||
if (settings.name != '/') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return MaterialPageRoute<void>(
|
||||
settings: settings,
|
||||
builder: (context) => CraneApp(),
|
||||
fullscreenDialog: true,
|
||||
);
|
||||
}
|
||||
281
gallery/lib/studies/crane/backdrop.dart
Normal file
281
gallery/lib/studies/crane/backdrop.dart
Normal file
@@ -0,0 +1,281 @@
|
||||
// Copyright 2019 The Flutter team. 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';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'package:gallery/data/gallery_options.dart';
|
||||
import 'package:gallery/l10n/gallery_localizations.dart';
|
||||
import 'package:gallery/layout/adaptive.dart';
|
||||
import 'package:gallery/studies/crane/border_tab_indicator.dart';
|
||||
import 'package:gallery/studies/crane/backlayer.dart';
|
||||
import 'package:gallery/studies/crane/colors.dart';
|
||||
import 'package:gallery/studies/crane/header_form.dart';
|
||||
import 'package:gallery/studies/crane/item_cards.dart';
|
||||
|
||||
class _FrontLayer extends StatelessWidget {
|
||||
const _FrontLayer({
|
||||
Key key,
|
||||
this.title,
|
||||
this.index,
|
||||
}) : super(key: key);
|
||||
|
||||
final String title;
|
||||
final int index;
|
||||
|
||||
static const frontLayerBorderRadius = 16.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDesktop = isDisplayDesktop(context);
|
||||
final isSmallDesktop = isDisplaySmallDesktop(context);
|
||||
|
||||
return DefaultFocusTraversal(
|
||||
policy: ReadingOrderTraversalPolicy(),
|
||||
child: PhysicalShape(
|
||||
elevation: 16,
|
||||
color: cranePrimaryWhite,
|
||||
clipper: ShapeBorderClipper(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(frontLayerBorderRadius),
|
||||
topRight: Radius.circular(frontLayerBorderRadius),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: ListView(
|
||||
padding: isDesktop
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal:
|
||||
isSmallDesktop ? appPaddingSmall : appPaddingLarge,
|
||||
vertical: 22)
|
||||
: EdgeInsets.all(20),
|
||||
children: [
|
||||
Text(title, style: Theme.of(context).textTheme.subtitle),
|
||||
SizedBox(height: 20),
|
||||
ItemCards(index: index),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
final Widget frontLayer;
|
||||
final List<BackLayerItem> backLayerItems;
|
||||
final Widget frontTitle;
|
||||
final Widget backTitle;
|
||||
|
||||
const Backdrop({
|
||||
@required this.frontLayer,
|
||||
@required this.backLayerItems,
|
||||
@required this.frontTitle,
|
||||
@required this.backTitle,
|
||||
}) : assert(frontLayer != null),
|
||||
assert(backLayerItems != null),
|
||||
assert(frontTitle != null),
|
||||
assert(backTitle != null);
|
||||
|
||||
@override
|
||||
_BackdropState createState() => _BackdropState();
|
||||
}
|
||||
|
||||
class _BackdropState extends State<Backdrop> with TickerProviderStateMixin {
|
||||
TabController _tabController;
|
||||
Animation<Offset> _flyLayerOffset;
|
||||
Animation<Offset> _sleepLayerOffset;
|
||||
Animation<Offset> _eatLayerOffset;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 3, vsync: this);
|
||||
|
||||
// Offsets to create a gap between front layers.
|
||||
_flyLayerOffset = _tabController.animation
|
||||
.drive(Tween<Offset>(begin: Offset(0, 0), end: Offset(-0.05, 0)));
|
||||
|
||||
_sleepLayerOffset = _tabController.animation
|
||||
.drive(Tween<Offset>(begin: Offset(0.05, 0), end: Offset(0, 0)));
|
||||
|
||||
_eatLayerOffset = _tabController.animation
|
||||
.drive(Tween<Offset>(begin: Offset(0.10, 0), end: Offset(0.05, 0)));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleTabs(int tabIndex) {
|
||||
_tabController.animateTo(tabIndex,
|
||||
duration: const Duration(milliseconds: 300));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDesktop = isDisplayDesktop(context);
|
||||
final textScaleFactor = GalleryOptions.of(context).textScaleFactor(context);
|
||||
|
||||
return Material(
|
||||
color: cranePurple800,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 12),
|
||||
child: DefaultFocusTraversal(
|
||||
policy: ReadingOrderTraversalPolicy(),
|
||||
child: Scaffold(
|
||||
backgroundColor: cranePurple800,
|
||||
appBar: AppBar(
|
||||
brightness: Brightness.dark,
|
||||
elevation: 0,
|
||||
titleSpacing: 0,
|
||||
flexibleSpace: CraneAppBar(
|
||||
tabController: _tabController,
|
||||
tabHandler: _handleTabs,
|
||||
),
|
||||
),
|
||||
body: FocusScope(
|
||||
child: Stack(
|
||||
children: [
|
||||
BackLayer(
|
||||
tabController: _tabController,
|
||||
backLayerItems: widget.backLayerItems,
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(
|
||||
top: isDesktop
|
||||
? (isDisplaySmallDesktop(context)
|
||||
? textFieldHeight * 2
|
||||
: textFieldHeight) +
|
||||
20 * textScaleFactor / 2
|
||||
: 175 + 140 * textScaleFactor / 2,
|
||||
),
|
||||
child: TabBarView(
|
||||
physics: isDesktop
|
||||
? NeverScrollableScrollPhysics()
|
||||
: null, // use default TabBarView physics
|
||||
controller: _tabController,
|
||||
children: [
|
||||
SlideTransition(
|
||||
position: _flyLayerOffset,
|
||||
child: _FrontLayer(
|
||||
title: GalleryLocalizations.of(context)
|
||||
.craneFlySubhead,
|
||||
index: 0,
|
||||
),
|
||||
),
|
||||
SlideTransition(
|
||||
position: _sleepLayerOffset,
|
||||
child: _FrontLayer(
|
||||
title: GalleryLocalizations.of(context)
|
||||
.craneSleepSubhead,
|
||||
index: 1,
|
||||
),
|
||||
),
|
||||
SlideTransition(
|
||||
position: _eatLayerOffset,
|
||||
child: _FrontLayer(
|
||||
title: GalleryLocalizations.of(context)
|
||||
.craneEatSubhead,
|
||||
index: 2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CraneAppBar extends StatefulWidget {
|
||||
final Function(int) tabHandler;
|
||||
final TabController tabController;
|
||||
|
||||
const CraneAppBar({Key key, this.tabHandler, this.tabController})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
_CraneAppBarState createState() => _CraneAppBarState();
|
||||
}
|
||||
|
||||
class _CraneAppBarState extends State<CraneAppBar> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDesktop = isDisplayDesktop(context);
|
||||
final isSmallDesktop = isDisplaySmallDesktop(context);
|
||||
final textScaleFactor = GalleryOptions.of(context).textScaleFactor(context);
|
||||
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal:
|
||||
isDesktop && !isSmallDesktop ? appPaddingLarge : appPaddingSmall,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
ExcludeSemantics(
|
||||
child: Image.asset(
|
||||
'assets/crane/logo/logo.png',
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: 24),
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
splashColor: Colors.transparent,
|
||||
),
|
||||
child: TabBar(
|
||||
indicator: BorderTabIndicator(
|
||||
indicatorHeight: isDesktop ? 28 : 32,
|
||||
textScaleFactor: textScaleFactor,
|
||||
),
|
||||
controller: widget.tabController,
|
||||
labelPadding: isDesktop
|
||||
? const EdgeInsets.symmetric(horizontal: 32)
|
||||
: EdgeInsets.zero,
|
||||
isScrollable: isDesktop, // left-align tabs on desktop
|
||||
labelStyle: Theme.of(context).textTheme.button,
|
||||
labelColor: cranePrimaryWhite,
|
||||
unselectedLabelColor: cranePrimaryWhite.withOpacity(.6),
|
||||
onTap: (index) => widget.tabController.animateTo(
|
||||
index,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
),
|
||||
tabs: [
|
||||
Tab(text: GalleryLocalizations.of(context).craneFly),
|
||||
Tab(text: GalleryLocalizations.of(context).craneSleep),
|
||||
Tab(text: GalleryLocalizations.of(context).craneEat),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
49
gallery/lib/studies/crane/backlayer.dart
Normal file
49
gallery/lib/studies/crane/backlayer.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright 2019 The Flutter team. All rights 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';
|
||||
|
||||
abstract class BackLayerItem extends StatefulWidget {
|
||||
final int index;
|
||||
|
||||
BackLayerItem({Key key, this.index}) : super(key: key);
|
||||
}
|
||||
|
||||
class BackLayer extends StatefulWidget {
|
||||
final List<BackLayerItem> backLayerItems;
|
||||
final TabController tabController;
|
||||
|
||||
const BackLayer({Key key, this.backLayerItems, this.tabController})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
_BackLayerState createState() => _BackLayerState();
|
||||
}
|
||||
|
||||
class _BackLayerState extends State<BackLayer> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.tabController.addListener(() => setState(() {}));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tabIndex = widget.tabController.index;
|
||||
return DefaultFocusTraversal(
|
||||
policy: WidgetOrderFocusTraversalPolicy(),
|
||||
child: IndexedStack(
|
||||
index: tabIndex,
|
||||
children: [
|
||||
for (BackLayerItem backLayerItem in widget.backLayerItems)
|
||||
Focus(
|
||||
canRequestFocus: backLayerItem.index == tabIndex,
|
||||
debugLabel: 'backLayerItem: $backLayerItem',
|
||||
child: backLayerItem,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
52
gallery/lib/studies/crane/border_tab_indicator.dart
Normal file
52
gallery/lib/studies/crane/border_tab_indicator.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright 2019 The Flutter team. All rights 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/widgets.dart';
|
||||
|
||||
class BorderTabIndicator extends Decoration {
|
||||
BorderTabIndicator({this.indicatorHeight, this.textScaleFactor}) : super();
|
||||
|
||||
final double indicatorHeight;
|
||||
final double textScaleFactor;
|
||||
|
||||
@override
|
||||
_BorderPainter createBoxPainter([VoidCallback onChanged]) {
|
||||
return _BorderPainter(
|
||||
this, indicatorHeight, this.textScaleFactor, onChanged);
|
||||
}
|
||||
}
|
||||
|
||||
class _BorderPainter extends BoxPainter {
|
||||
_BorderPainter(
|
||||
this.decoration,
|
||||
this.indicatorHeight,
|
||||
this.textScaleFactor,
|
||||
VoidCallback onChanged,
|
||||
) : assert(decoration != null),
|
||||
assert(indicatorHeight >= 0),
|
||||
super(onChanged);
|
||||
|
||||
final BorderTabIndicator decoration;
|
||||
final double indicatorHeight;
|
||||
final double textScaleFactor;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
|
||||
assert(configuration != null);
|
||||
assert(configuration.size != null);
|
||||
final horizontalInset = 16 - 4 * textScaleFactor;
|
||||
final rect = Offset(offset.dx + horizontalInset,
|
||||
(configuration.size.height / 2) - indicatorHeight / 2 - 1) &
|
||||
Size(configuration.size.width - 2 * horizontalInset, indicatorHeight);
|
||||
final paint = Paint();
|
||||
paint.color = Colors.white;
|
||||
paint.style = PaintingStyle.stroke;
|
||||
paint.strokeWidth = 2;
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(rect, Radius.circular(56)),
|
||||
paint,
|
||||
);
|
||||
}
|
||||
}
|
||||
20
gallery/lib/studies/crane/colors.dart
Normal file
20
gallery/lib/studies/crane/colors.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright 2019 The Flutter team. All rights 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 cranePurple700 = Color(0xFF720D5D);
|
||||
const cranePurple800 = Color(0xFF5D1049);
|
||||
const cranePurple900 = Color(0xFF4E0D3A);
|
||||
|
||||
const craneRed700 = Color(0xFFE30425);
|
||||
|
||||
const craneWhite60 = Color(0x99FFFFFF);
|
||||
const cranePrimaryWhite = Color(0xFFFFFFFF);
|
||||
const craneErrorOrange = Color(0xFFFF9100);
|
||||
|
||||
const craneAlpha = Color(0x00FFFFFF);
|
||||
|
||||
const craneGrey = Color(0xFF747474);
|
||||
const craneBlack = Color(0XFF1E252D);
|
||||
51
gallery/lib/studies/crane/eat_form.dart
Normal file
51
gallery/lib/studies/crane/eat_form.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright 2019 The Flutter team. All rights 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:gallery/l10n/gallery_localizations.dart';
|
||||
import 'package:gallery/studies/crane/backlayer.dart';
|
||||
import 'package:gallery/studies/crane/header_form.dart';
|
||||
|
||||
class EatForm extends BackLayerItem {
|
||||
EatForm({int index}) : super(index: index);
|
||||
|
||||
@override
|
||||
_EatFormState createState() => _EatFormState();
|
||||
}
|
||||
|
||||
class _EatFormState extends State<EatForm> {
|
||||
final dinerController = TextEditingController();
|
||||
final dateController = TextEditingController();
|
||||
final timeController = TextEditingController();
|
||||
final locationController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HeaderForm(
|
||||
fields: <HeaderFormField>[
|
||||
HeaderFormField(
|
||||
iconData: Icons.person,
|
||||
title: GalleryLocalizations.of(context).craneFormDiners,
|
||||
textController: dinerController,
|
||||
),
|
||||
HeaderFormField(
|
||||
iconData: Icons.date_range,
|
||||
title: GalleryLocalizations.of(context).craneFormDate,
|
||||
textController: dateController,
|
||||
),
|
||||
HeaderFormField(
|
||||
iconData: Icons.access_time,
|
||||
title: GalleryLocalizations.of(context).craneFormTime,
|
||||
textController: timeController,
|
||||
),
|
||||
HeaderFormField(
|
||||
iconData: Icons.restaurant_menu,
|
||||
title: GalleryLocalizations.of(context).craneFormLocation,
|
||||
textController: locationController,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
51
gallery/lib/studies/crane/fly_form.dart
Normal file
51
gallery/lib/studies/crane/fly_form.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright 2019 The Flutter team. All rights 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:gallery/l10n/gallery_localizations.dart';
|
||||
import 'package:gallery/studies/crane/backlayer.dart';
|
||||
import 'package:gallery/studies/crane/header_form.dart';
|
||||
|
||||
class FlyForm extends BackLayerItem {
|
||||
FlyForm({int index}) : super(index: index);
|
||||
|
||||
@override
|
||||
_FlyFormState createState() => _FlyFormState();
|
||||
}
|
||||
|
||||
class _FlyFormState extends State<FlyForm> {
|
||||
final travelerController = TextEditingController();
|
||||
final countryDestinationController = TextEditingController();
|
||||
final destinationController = TextEditingController();
|
||||
final dateController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HeaderForm(
|
||||
fields: <HeaderFormField>[
|
||||
HeaderFormField(
|
||||
iconData: Icons.person,
|
||||
title: GalleryLocalizations.of(context).craneFormTravelers,
|
||||
textController: travelerController,
|
||||
),
|
||||
HeaderFormField(
|
||||
iconData: Icons.place,
|
||||
title: GalleryLocalizations.of(context).craneFormOrigin,
|
||||
textController: countryDestinationController,
|
||||
),
|
||||
HeaderFormField(
|
||||
iconData: Icons.airplanemode_active,
|
||||
title: GalleryLocalizations.of(context).craneFormDestination,
|
||||
textController: destinationController,
|
||||
),
|
||||
HeaderFormField(
|
||||
iconData: Icons.date_range,
|
||||
title: GalleryLocalizations.of(context).craneFormDates,
|
||||
textController: dateController,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
102
gallery/lib/studies/crane/header_form.dart
Normal file
102
gallery/lib/studies/crane/header_form.dart
Normal file
@@ -0,0 +1,102 @@
|
||||
// Copyright 2019 The Flutter team. All rights 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:gallery/layout/adaptive.dart';
|
||||
import 'package:gallery/studies/crane/colors.dart';
|
||||
|
||||
const textFieldHeight = 60.0;
|
||||
const appPaddingLarge = 120.0;
|
||||
const appPaddingSmall = 24.0;
|
||||
|
||||
class HeaderFormField {
|
||||
final IconData iconData;
|
||||
final String title;
|
||||
final TextEditingController textController;
|
||||
|
||||
const HeaderFormField({this.iconData, this.title, this.textController});
|
||||
}
|
||||
|
||||
class HeaderForm extends StatelessWidget {
|
||||
final List<HeaderFormField> fields;
|
||||
|
||||
const HeaderForm({Key key, this.fields}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDesktop = isDisplayDesktop(context);
|
||||
final isSmallDesktop = isDisplaySmallDesktop(context);
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal:
|
||||
isDesktop && !isSmallDesktop ? appPaddingLarge : appPaddingSmall,
|
||||
),
|
||||
child: isDesktop
|
||||
? LayoutBuilder(builder: (context, constraints) {
|
||||
var crossAxisCount = isSmallDesktop ? 2 : 4;
|
||||
if (fields.length < crossAxisCount) {
|
||||
crossAxisCount = fields.length;
|
||||
}
|
||||
final itemWidth = constraints.maxWidth / crossAxisCount;
|
||||
return GridView.count(
|
||||
crossAxisCount: crossAxisCount,
|
||||
childAspectRatio: itemWidth / textFieldHeight,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
for (final field in fields)
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 16),
|
||||
child: _HeaderTextField(field: field),
|
||||
)
|
||||
],
|
||||
);
|
||||
})
|
||||
: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final field in fields)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: _HeaderTextField(field: field),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HeaderTextField extends StatelessWidget {
|
||||
final HeaderFormField field;
|
||||
|
||||
_HeaderTextField({this.field});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextField(
|
||||
controller: field.textController,
|
||||
cursorColor: Theme.of(context).colorScheme.secondary,
|
||||
style: Theme.of(context).textTheme.body2.copyWith(color: Colors.white),
|
||||
onTap: () {},
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderSide: BorderSide(
|
||||
width: 0,
|
||||
style: BorderStyle.none,
|
||||
),
|
||||
),
|
||||
contentPadding: EdgeInsets.all(16),
|
||||
fillColor: cranePurple700,
|
||||
filled: true,
|
||||
hintText: field.title,
|
||||
hasFloatingPlaceholder: false,
|
||||
prefixIcon: Icon(
|
||||
field.iconData,
|
||||
size: 24,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
179
gallery/lib/studies/crane/item_cards.dart
Normal file
179
gallery/lib/studies/crane/item_cards.dart
Normal file
@@ -0,0 +1,179 @@
|
||||
// Copyright 2019 The Flutter team. All rights 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 'package:gallery/layout/adaptive.dart';
|
||||
import 'package:gallery/layout/highlight_focus.dart';
|
||||
import 'package:gallery/studies/crane/model/data.dart';
|
||||
import 'package:gallery/studies/crane/model/destination.dart';
|
||||
|
||||
class ItemCards extends StatefulWidget {
|
||||
final int index;
|
||||
|
||||
const ItemCards({Key key, this.index}) : super(key: key);
|
||||
|
||||
static const totalColumns = 4;
|
||||
|
||||
@override
|
||||
_ItemCardsState createState() => _ItemCardsState();
|
||||
}
|
||||
|
||||
class _ItemCardsState extends State<ItemCards> {
|
||||
List<Destination> flyDestinations;
|
||||
List<Destination> sleepDestinations;
|
||||
List<Destination> eatDestinations;
|
||||
|
||||
List<Widget> _buildDestinationCards({int listIndex}) {
|
||||
final List<Destination> destinations = [
|
||||
if (listIndex == 0) ...flyDestinations,
|
||||
if (listIndex == 1) ...sleepDestinations,
|
||||
if (listIndex == 2) ...eatDestinations,
|
||||
];
|
||||
|
||||
return destinations
|
||||
.map(
|
||||
(d) => HighlightFocus(
|
||||
debugLabel: 'DestinationCard: ${d.destination}',
|
||||
highlightColor: Colors.red.withOpacity(0.5),
|
||||
onPressed: () {},
|
||||
child: RepaintBoundary(
|
||||
child: _DestinationCard(destination: d),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
// We use didChangeDependencies because the initialization involves an
|
||||
// InheritedWidget (for localization). However, we don't need to get
|
||||
// destinations again when, say, resizing the window.
|
||||
if (flyDestinations == null) {
|
||||
flyDestinations = getFlyDestinations(context);
|
||||
sleepDestinations = getSleepDestinations(context);
|
||||
eatDestinations = getEatDestinations(context);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDesktop = isDisplayDesktop(context);
|
||||
final List<Widget> destinationCards =
|
||||
_buildDestinationCards(listIndex: widget.index);
|
||||
|
||||
if (isDesktop) {
|
||||
var columns = List<List<Widget>>(ItemCards.totalColumns);
|
||||
for (var i = 0; i < destinationCards.length; i++) {
|
||||
final col = i % ItemCards.totalColumns;
|
||||
|
||||
if (columns[col] == null) {
|
||||
columns[col] = List<Widget>();
|
||||
}
|
||||
|
||||
columns[col].add(
|
||||
// TODO: determine why this is isn't always respected
|
||||
Semantics(
|
||||
sortKey: OrdinalSortKey(i.toDouble(), name: 'destination'),
|
||||
child: destinationCards[i],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (var column in columns)
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 16),
|
||||
child: Column(
|
||||
children: column,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Column(children: destinationCards);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _DestinationCard extends StatelessWidget {
|
||||
_DestinationCard({this.destination}) : assert(destination != null);
|
||||
final Destination destination;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final imageWidget = Semantics(
|
||||
child: ExcludeSemantics(
|
||||
child: Image.asset(
|
||||
destination.assetName,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
label: destination.assetSemanticLabel,
|
||||
);
|
||||
|
||||
final isDesktop = isDisplayDesktop(context);
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
if (isDesktop) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 40),
|
||||
child: Semantics(
|
||||
container: true,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
child: imageWidget,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20, bottom: 10),
|
||||
child: Text(
|
||||
destination.destination,
|
||||
style: textTheme.subhead,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
destination.subtitle(context),
|
||||
semanticsLabel: destination.subtitleSemantics(context),
|
||||
style: textTheme.subtitle,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: EdgeInsetsDirectional.only(end: 8),
|
||||
leading: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
child: SizedBox(
|
||||
height: 60,
|
||||
width: 60,
|
||||
child: imageWidget,
|
||||
),
|
||||
),
|
||||
title: Text(destination.destination, style: textTheme.subhead),
|
||||
subtitle: Text(
|
||||
destination.subtitle(context),
|
||||
semanticsLabel: destination.subtitleSemantics(context),
|
||||
style: textTheme.subtitle,
|
||||
),
|
||||
),
|
||||
Divider(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
294
gallery/lib/studies/crane/model/data.dart
Normal file
294
gallery/lib/studies/crane/model/data.dart
Normal file
@@ -0,0 +1,294 @@
|
||||
// Copyright 2019 The Flutter team. All rights 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:gallery/l10n/gallery_localizations.dart';
|
||||
import 'package:gallery/studies/crane/model/destination.dart';
|
||||
|
||||
// TODO: localize durations
|
||||
|
||||
List<FlyDestination> getFlyDestinations(BuildContext context) =>
|
||||
<FlyDestination>[
|
||||
FlyDestination(
|
||||
id: 0,
|
||||
destination: GalleryLocalizations.of(context).craneFly0,
|
||||
stops: 1,
|
||||
duration: Duration(hours: 6, minutes: 15),
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneFly0SemanticLabel,
|
||||
),
|
||||
FlyDestination(
|
||||
id: 1,
|
||||
destination: GalleryLocalizations.of(context).craneFly1,
|
||||
stops: 0,
|
||||
duration: Duration(hours: 13, minutes: 30),
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneFly1SemanticLabel,
|
||||
),
|
||||
FlyDestination(
|
||||
id: 2,
|
||||
destination: GalleryLocalizations.of(context).craneFly2,
|
||||
stops: 0,
|
||||
duration: Duration(hours: 5, minutes: 16),
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneFly2SemanticLabel,
|
||||
),
|
||||
FlyDestination(
|
||||
id: 3,
|
||||
destination: GalleryLocalizations.of(context).craneFly3,
|
||||
stops: 2,
|
||||
duration: Duration(hours: 19, minutes: 40),
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneFly3SemanticLabel,
|
||||
),
|
||||
FlyDestination(
|
||||
id: 4,
|
||||
destination: GalleryLocalizations.of(context).craneFly4,
|
||||
stops: 0,
|
||||
duration: Duration(hours: 8, minutes: 24),
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneFly4SemanticLabel,
|
||||
),
|
||||
FlyDestination(
|
||||
id: 5,
|
||||
destination: GalleryLocalizations.of(context).craneFly5,
|
||||
stops: 1,
|
||||
duration: Duration(hours: 14, minutes: 12),
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneFly5SemanticLabel,
|
||||
),
|
||||
FlyDestination(
|
||||
id: 6,
|
||||
destination: GalleryLocalizations.of(context).craneFly6,
|
||||
stops: 0,
|
||||
duration: Duration(hours: 5, minutes: 24),
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneFly6SemanticLabel,
|
||||
),
|
||||
FlyDestination(
|
||||
id: 7,
|
||||
destination: GalleryLocalizations.of(context).craneFly7,
|
||||
stops: 1,
|
||||
duration: Duration(hours: 5, minutes: 43),
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneFly7SemanticLabel,
|
||||
),
|
||||
FlyDestination(
|
||||
id: 8,
|
||||
destination: GalleryLocalizations.of(context).craneFly8,
|
||||
stops: 0,
|
||||
duration: Duration(hours: 8, minutes: 25),
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneFly8SemanticLabel,
|
||||
),
|
||||
FlyDestination(
|
||||
id: 9,
|
||||
destination: GalleryLocalizations.of(context).craneFly9,
|
||||
stops: 1,
|
||||
duration: Duration(hours: 15, minutes: 52),
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneFly9SemanticLabel,
|
||||
),
|
||||
FlyDestination(
|
||||
id: 10,
|
||||
destination: GalleryLocalizations.of(context).craneFly10,
|
||||
stops: 0,
|
||||
duration: Duration(hours: 5, minutes: 57),
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneFly10SemanticLabel,
|
||||
),
|
||||
FlyDestination(
|
||||
id: 11,
|
||||
destination: GalleryLocalizations.of(context).craneFly11,
|
||||
stops: 1,
|
||||
duration: Duration(hours: 13, minutes: 24),
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneFly11SemanticLabel,
|
||||
),
|
||||
FlyDestination(
|
||||
id: 12,
|
||||
destination: GalleryLocalizations.of(context).craneFly12,
|
||||
stops: 2,
|
||||
duration: Duration(hours: 10, minutes: 20),
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneFly12SemanticLabel,
|
||||
),
|
||||
FlyDestination(
|
||||
id: 13,
|
||||
destination: GalleryLocalizations.of(context).craneFly13,
|
||||
stops: 0,
|
||||
duration: Duration(hours: 7, minutes: 15),
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneFly13SemanticLabel,
|
||||
),
|
||||
];
|
||||
|
||||
List<SleepDestination> getSleepDestinations(BuildContext context) =>
|
||||
<SleepDestination>[
|
||||
SleepDestination(
|
||||
id: 0,
|
||||
destination: GalleryLocalizations.of(context).craneSleep0,
|
||||
total: 2241,
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneSleep0SemanticLabel,
|
||||
),
|
||||
SleepDestination(
|
||||
id: 1,
|
||||
destination: GalleryLocalizations.of(context).craneSleep1,
|
||||
total: 876,
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneSleep1SemanticLabel,
|
||||
),
|
||||
SleepDestination(
|
||||
id: 2,
|
||||
destination: GalleryLocalizations.of(context).craneSleep2,
|
||||
total: 1286,
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneSleep2SemanticLabel,
|
||||
),
|
||||
SleepDestination(
|
||||
id: 3,
|
||||
destination: GalleryLocalizations.of(context).craneSleep3,
|
||||
total: 496,
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneSleep3SemanticLabel,
|
||||
),
|
||||
SleepDestination(
|
||||
id: 4,
|
||||
destination: GalleryLocalizations.of(context).craneSleep4,
|
||||
total: 390,
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneSleep4SemanticLabel,
|
||||
),
|
||||
SleepDestination(
|
||||
id: 5,
|
||||
destination: GalleryLocalizations.of(context).craneSleep5,
|
||||
total: 876,
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneSleep5SemanticLabel,
|
||||
),
|
||||
SleepDestination(
|
||||
id: 6,
|
||||
destination: GalleryLocalizations.of(context).craneSleep6,
|
||||
total: 989,
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneSleep6SemanticLabel,
|
||||
),
|
||||
SleepDestination(
|
||||
id: 7,
|
||||
destination: GalleryLocalizations.of(context).craneSleep7,
|
||||
total: 306,
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneSleep7SemanticLabel,
|
||||
),
|
||||
SleepDestination(
|
||||
id: 8,
|
||||
destination: GalleryLocalizations.of(context).craneSleep8,
|
||||
total: 385,
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneSleep8SemanticLabel,
|
||||
),
|
||||
SleepDestination(
|
||||
id: 9,
|
||||
destination: GalleryLocalizations.of(context).craneSleep9,
|
||||
total: 989,
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneSleep9SemanticLabel,
|
||||
),
|
||||
SleepDestination(
|
||||
id: 10,
|
||||
destination: GalleryLocalizations.of(context).craneSleep10,
|
||||
total: 1380,
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneSleep10SemanticLabel,
|
||||
),
|
||||
SleepDestination(
|
||||
id: 11,
|
||||
destination: GalleryLocalizations.of(context).craneSleep11,
|
||||
total: 1109,
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneSleep11SemanticLabel,
|
||||
),
|
||||
];
|
||||
|
||||
List<EatDestination> getEatDestinations(BuildContext context) =>
|
||||
<EatDestination>[
|
||||
EatDestination(
|
||||
id: 0,
|
||||
destination: GalleryLocalizations.of(context).craneEat0,
|
||||
total: 354,
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneEat0SemanticLabel,
|
||||
),
|
||||
EatDestination(
|
||||
id: 1,
|
||||
destination: GalleryLocalizations.of(context).craneEat1,
|
||||
total: 623,
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneEat1SemanticLabel,
|
||||
),
|
||||
EatDestination(
|
||||
id: 2,
|
||||
destination: GalleryLocalizations.of(context).craneEat2,
|
||||
total: 124,
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneEat2SemanticLabel,
|
||||
),
|
||||
EatDestination(
|
||||
id: 3,
|
||||
destination: GalleryLocalizations.of(context).craneEat3,
|
||||
total: 495,
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneEat3SemanticLabel,
|
||||
),
|
||||
EatDestination(
|
||||
id: 4,
|
||||
destination: GalleryLocalizations.of(context).craneEat4,
|
||||
total: 683,
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneEat4SemanticLabel,
|
||||
),
|
||||
EatDestination(
|
||||
id: 5,
|
||||
destination: GalleryLocalizations.of(context).craneEat5,
|
||||
total: 786,
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneEat5SemanticLabel,
|
||||
),
|
||||
EatDestination(
|
||||
id: 6,
|
||||
destination: GalleryLocalizations.of(context).craneEat6,
|
||||
total: 323,
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneEat6SemanticLabel,
|
||||
),
|
||||
EatDestination(
|
||||
id: 7,
|
||||
destination: GalleryLocalizations.of(context).craneEat7,
|
||||
total: 285,
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneEat7SemanticLabel,
|
||||
),
|
||||
EatDestination(
|
||||
id: 8,
|
||||
destination: GalleryLocalizations.of(context).craneEat8,
|
||||
total: 323,
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneEat8SemanticLabel,
|
||||
),
|
||||
EatDestination(
|
||||
id: 9,
|
||||
destination: GalleryLocalizations.of(context).craneEat9,
|
||||
total: 1406,
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneEat9SemanticLabel,
|
||||
),
|
||||
EatDestination(
|
||||
id: 10,
|
||||
destination: GalleryLocalizations.of(context).craneEat10,
|
||||
total: 849,
|
||||
assetSemanticLabel:
|
||||
GalleryLocalizations.of(context).craneEat10SemanticLabel,
|
||||
),
|
||||
];
|
||||
127
gallery/lib/studies/crane/model/destination.dart
Normal file
127
gallery/lib/studies/crane/model/destination.dart
Normal file
@@ -0,0 +1,127 @@
|
||||
// Copyright 2019 The Flutter team. All rights 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:gallery/data/gallery_options.dart';
|
||||
|
||||
import 'package:gallery/studies/crane/model/formatters.dart';
|
||||
|
||||
import '../../../l10n/gallery_localizations.dart';
|
||||
|
||||
abstract class Destination {
|
||||
const Destination({
|
||||
@required this.id,
|
||||
@required this.destination,
|
||||
@required this.assetSemanticLabel,
|
||||
}) : assert(id != null),
|
||||
assert(destination != null);
|
||||
|
||||
final int id;
|
||||
final String destination;
|
||||
final String assetSemanticLabel;
|
||||
|
||||
String get assetName;
|
||||
|
||||
String subtitle(BuildContext context);
|
||||
String subtitleSemantics(BuildContext context) => subtitle(context);
|
||||
|
||||
@override
|
||||
String toString() => '$destination (id=$id)';
|
||||
}
|
||||
|
||||
class FlyDestination extends Destination {
|
||||
const FlyDestination({
|
||||
@required int id,
|
||||
@required String destination,
|
||||
@required String assetSemanticLabel,
|
||||
@required this.stops,
|
||||
this.duration,
|
||||
}) : assert(stops != null),
|
||||
assert(destination != null),
|
||||
super(
|
||||
id: id,
|
||||
destination: destination,
|
||||
assetSemanticLabel: assetSemanticLabel,
|
||||
);
|
||||
|
||||
final int stops;
|
||||
final Duration duration;
|
||||
|
||||
String get assetName => 'assets/crane/destinations/fly_$id.jpg';
|
||||
|
||||
String subtitle(BuildContext context) {
|
||||
final stopsText = GalleryLocalizations.of(context).craneFlyStops(stops);
|
||||
|
||||
if (duration == null) {
|
||||
return stopsText;
|
||||
} else {
|
||||
final textDirection = GalleryOptions.of(context).textDirection();
|
||||
final durationText =
|
||||
formattedDuration(context, duration, abbreviated: true);
|
||||
return textDirection == TextDirection.ltr
|
||||
? '$stopsText · $durationText'
|
||||
: '$durationText · $stopsText';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String subtitleSemantics(BuildContext context) {
|
||||
final stopsText = GalleryLocalizations.of(context).craneFlyStops(stops);
|
||||
|
||||
if (duration == null) {
|
||||
return stopsText;
|
||||
} else {
|
||||
final durationText =
|
||||
formattedDuration(context, duration, abbreviated: false);
|
||||
return '$stopsText, $durationText';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SleepDestination extends Destination {
|
||||
const SleepDestination({
|
||||
@required int id,
|
||||
@required String destination,
|
||||
@required String assetSemanticLabel,
|
||||
@required this.total,
|
||||
}) : assert(total != null),
|
||||
assert(destination != null),
|
||||
super(
|
||||
id: id,
|
||||
destination: destination,
|
||||
assetSemanticLabel: assetSemanticLabel,
|
||||
);
|
||||
|
||||
final int total;
|
||||
|
||||
String get assetName => 'assets/crane/destinations/sleep_$id.jpg';
|
||||
|
||||
String subtitle(BuildContext context) {
|
||||
return GalleryLocalizations.of(context).craneSleepProperties(total);
|
||||
}
|
||||
}
|
||||
|
||||
class EatDestination extends Destination {
|
||||
const EatDestination({
|
||||
@required int id,
|
||||
@required String destination,
|
||||
@required String assetSemanticLabel,
|
||||
@required this.total,
|
||||
}) : assert(total != null),
|
||||
assert(destination != null),
|
||||
super(
|
||||
id: id,
|
||||
destination: destination,
|
||||
assetSemanticLabel: assetSemanticLabel,
|
||||
);
|
||||
|
||||
final int total;
|
||||
|
||||
String get assetName => 'assets/crane/destinations/eat_$id.jpg';
|
||||
|
||||
String subtitle(BuildContext context) {
|
||||
return GalleryLocalizations.of(context).craneEatRestaurants(total);
|
||||
}
|
||||
}
|
||||
17
gallery/lib/studies/crane/model/formatters.dart
Normal file
17
gallery/lib/studies/crane/model/formatters.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright 2019 The Flutter team. All rights 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:gallery/l10n/gallery_localizations.dart';
|
||||
|
||||
// Duration of time (e.g. 16h 12m)
|
||||
String formattedDuration(BuildContext context, Duration duration,
|
||||
{bool abbreviated}) {
|
||||
final hoursShortForm =
|
||||
GalleryLocalizations.of(context).craneHours(duration.inHours.toInt());
|
||||
final minutesShortForm =
|
||||
GalleryLocalizations.of(context).craneMinutes(duration.inMinutes % 60);
|
||||
return GalleryLocalizations.of(context)
|
||||
.craneFlightDuration(hoursShortForm, minutesShortForm);
|
||||
}
|
||||
45
gallery/lib/studies/crane/sleep_form.dart
Normal file
45
gallery/lib/studies/crane/sleep_form.dart
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright 2019 The Flutter team. All rights 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:gallery/l10n/gallery_localizations.dart';
|
||||
|
||||
import 'package:gallery/studies/crane/backlayer.dart';
|
||||
import 'package:gallery/studies/crane/header_form.dart';
|
||||
|
||||
class SleepForm extends BackLayerItem {
|
||||
SleepForm({int index}) : super(index: index);
|
||||
|
||||
@override
|
||||
_SleepFormState createState() => _SleepFormState();
|
||||
}
|
||||
|
||||
class _SleepFormState extends State<SleepForm> {
|
||||
final travelerController = TextEditingController();
|
||||
final dateController = TextEditingController();
|
||||
final locationController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HeaderForm(
|
||||
fields: <HeaderFormField>[
|
||||
HeaderFormField(
|
||||
iconData: Icons.person,
|
||||
title: GalleryLocalizations.of(context).craneFormTravelers,
|
||||
textController: travelerController,
|
||||
),
|
||||
HeaderFormField(
|
||||
iconData: Icons.date_range,
|
||||
title: GalleryLocalizations.of(context).craneFormDates,
|
||||
textController: dateController,
|
||||
),
|
||||
HeaderFormField(
|
||||
iconData: Icons.hotel,
|
||||
title: GalleryLocalizations.of(context).craneFormLocation,
|
||||
textController: locationController,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
106
gallery/lib/studies/crane/theme.dart
Normal file
106
gallery/lib/studies/crane/theme.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
// Copyright 2019 The Flutter team. All rights 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:google_fonts/google_fonts.dart';
|
||||
|
||||
import 'package:gallery/studies/crane/colors.dart';
|
||||
|
||||
final ThemeData craneTheme = _buildCraneTheme();
|
||||
|
||||
IconThemeData _customIconTheme(IconThemeData original, Color color) {
|
||||
return original.copyWith(color: color);
|
||||
}
|
||||
|
||||
ThemeData _buildCraneTheme() {
|
||||
final ThemeData base = ThemeData.light();
|
||||
|
||||
return base.copyWith(
|
||||
colorScheme: ColorScheme.light().copyWith(
|
||||
primary: cranePurple800,
|
||||
secondary: craneRed700,
|
||||
),
|
||||
accentColor: cranePurple700,
|
||||
primaryColor: cranePurple800,
|
||||
buttonColor: craneRed700,
|
||||
hintColor: craneWhite60,
|
||||
indicatorColor: cranePrimaryWhite,
|
||||
scaffoldBackgroundColor: cranePrimaryWhite,
|
||||
cardColor: cranePrimaryWhite,
|
||||
textSelectionColor: cranePurple700,
|
||||
errorColor: craneErrorOrange,
|
||||
highlightColor: Colors.transparent,
|
||||
buttonTheme: ButtonThemeData(
|
||||
textTheme: ButtonTextTheme.accent,
|
||||
),
|
||||
textTheme: _buildCraneTextTheme(base.textTheme),
|
||||
primaryTextTheme: _buildCraneTextTheme(base.primaryTextTheme),
|
||||
accentTextTheme: _buildCraneTextTheme(base.accentTextTheme),
|
||||
iconTheme: _customIconTheme(base.iconTheme, craneWhite60),
|
||||
primaryIconTheme: _customIconTheme(base.iconTheme, cranePrimaryWhite),
|
||||
);
|
||||
}
|
||||
|
||||
TextTheme _buildCraneTextTheme(TextTheme base) {
|
||||
return GoogleFonts.ralewayTextTheme(
|
||||
base.copyWith(
|
||||
display4: base.display4.copyWith(
|
||||
fontWeight: FontWeight.w300,
|
||||
fontSize: 96,
|
||||
),
|
||||
display3: base.display3.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 60,
|
||||
),
|
||||
display2: base.display2.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 48,
|
||||
),
|
||||
display1: base.display1.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 34,
|
||||
),
|
||||
headline: base.headline.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 24,
|
||||
),
|
||||
title: base.title.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 20,
|
||||
),
|
||||
subhead: base.subhead.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
subtitle: base.subtitle.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
color: craneGrey,
|
||||
),
|
||||
body2: base.body2.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16,
|
||||
),
|
||||
body1: base.body1.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
button: base.button.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13,
|
||||
letterSpacing: 0.8,
|
||||
),
|
||||
caption: base.caption.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 12,
|
||||
color: craneGrey,
|
||||
),
|
||||
overline: base.overline.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user