mirror of
https://github.com/flutter/samples.git
synced 2025-11-10 14:58:34 +00:00
[Gallery] Fix directory structure (#312)
This commit is contained in:
134
gallery/lib/pages/about.dart
Normal file
134
gallery/lib/pages/about.dart
Normal file
@@ -0,0 +1,134 @@
|
||||
// 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/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:package_info/package_info.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'package:gallery/l10n/gallery_localizations.dart';
|
||||
|
||||
void showAboutDialog({
|
||||
@required BuildContext context,
|
||||
}) {
|
||||
assert(context != null);
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return _AboutDialog();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> getVersionNumber() async {
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
return packageInfo.version;
|
||||
}
|
||||
|
||||
class _AboutDialog extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final bodyTextStyle = textTheme.body2.apply(color: colorScheme.onPrimary);
|
||||
|
||||
final name = 'Flutter Gallery'; // Don't need to localize.
|
||||
final legalese = '© 2019 The Flutter team'; // Don't need to localize.
|
||||
final samplesRepo =
|
||||
GalleryLocalizations.of(context).aboutFlutterSamplesRepo;
|
||||
final seeSource =
|
||||
GalleryLocalizations.of(context).aboutDialogDescription(samplesRepo);
|
||||
final samplesRepoIndex = seeSource.indexOf(samplesRepo);
|
||||
final samplesRepoIndexEnd = samplesRepoIndex + samplesRepo.length;
|
||||
final seeSourceFirst = seeSource.substring(0, samplesRepoIndex);
|
||||
final seeSourceSecond = seeSource.substring(samplesRepoIndexEnd);
|
||||
|
||||
return AlertDialog(
|
||||
backgroundColor: colorScheme.background,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
content: Container(
|
||||
constraints: BoxConstraints(maxWidth: 400),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: getVersionNumber(),
|
||||
builder: (context, snapshot) => Text(
|
||||
snapshot.hasData ? '$name ${snapshot.data}' : '$name',
|
||||
style: textTheme.display1.apply(color: colorScheme.onPrimary),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
style: bodyTextStyle,
|
||||
text: seeSourceFirst,
|
||||
),
|
||||
TextSpan(
|
||||
style: bodyTextStyle.copyWith(
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
text: samplesRepo,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () async {
|
||||
final url = 'https://github.com/flutter/samples/';
|
||||
if (await canLaunch(url)) {
|
||||
await launch(
|
||||
url,
|
||||
forceSafariVC: false,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
TextSpan(
|
||||
style: bodyTextStyle,
|
||||
text: seeSourceSecond,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 18),
|
||||
Text(
|
||||
legalese,
|
||||
style: bodyTextStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
FlatButton(
|
||||
textColor: colorScheme.primary,
|
||||
child: Text(
|
||||
MaterialLocalizations.of(context).viewLicensesButtonLabel,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(MaterialPageRoute<void>(
|
||||
builder: (context) => Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
textTheme:
|
||||
Typography(platform: Theme.of(context).platform).black,
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
),
|
||||
child: LicensePage(
|
||||
applicationName: name,
|
||||
applicationLegalese: legalese,
|
||||
),
|
||||
),
|
||||
));
|
||||
},
|
||||
),
|
||||
FlatButton(
|
||||
textColor: colorScheme.primary,
|
||||
child: Text(MaterialLocalizations.of(context).closeButtonLabel),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
378
gallery/lib/pages/backdrop.dart
Normal file
378
gallery/lib/pages/backdrop.dart
Normal file
@@ -0,0 +1,378 @@
|
||||
// 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:flare_dart/math/mat2d.dart';
|
||||
import 'package:flare_flutter/flare.dart';
|
||||
import 'package:flare_flutter/flare_actor.dart';
|
||||
import 'package:flare_flutter/flare_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:gallery/constants.dart';
|
||||
import 'package:gallery/data/gallery_options.dart';
|
||||
import 'package:gallery/l10n/gallery_localizations.dart';
|
||||
import 'package:gallery/layout/adaptive.dart';
|
||||
import 'package:gallery/pages/home.dart';
|
||||
import 'package:gallery/pages/settings.dart';
|
||||
|
||||
class AnimatedBackdrop extends StatefulWidget {
|
||||
@override
|
||||
_AnimatedBackdropState createState() => _AnimatedBackdropState();
|
||||
}
|
||||
|
||||
class _AnimatedBackdropState extends State<AnimatedBackdrop>
|
||||
with SingleTickerProviderStateMixin {
|
||||
AnimationController backdropController;
|
||||
ValueNotifier<bool> isSettingsOpenNotifier;
|
||||
Animation<double> openSettingsAnimation;
|
||||
Animation<double> staggerSettingsItemsAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
backdropController = AnimationController(
|
||||
duration: Duration(milliseconds: 200),
|
||||
vsync: this,
|
||||
)..addListener(() {
|
||||
setState(() {
|
||||
// The state that has changed here is the animation.
|
||||
});
|
||||
});
|
||||
isSettingsOpenNotifier = ValueNotifier(false);
|
||||
openSettingsAnimation = CurvedAnimation(
|
||||
parent: backdropController,
|
||||
curve: Interval(
|
||||
0.0,
|
||||
0.4,
|
||||
curve: Curves.ease,
|
||||
),
|
||||
);
|
||||
staggerSettingsItemsAnimation = CurvedAnimation(
|
||||
parent: backdropController,
|
||||
curve: Interval(
|
||||
0.5,
|
||||
1.0,
|
||||
curve: Curves.easeIn,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
backdropController.dispose();
|
||||
isSettingsOpenNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Backdrop(
|
||||
controller: backdropController,
|
||||
isSettingsOpenNotifier: isSettingsOpenNotifier,
|
||||
openSettingsAnimation: openSettingsAnimation,
|
||||
frontLayer: SettingsPage(
|
||||
openSettingsAnimation: openSettingsAnimation,
|
||||
staggerSettingsItemsAnimation: staggerSettingsItemsAnimation,
|
||||
isSettingsOpenNotifier: isSettingsOpenNotifier,
|
||||
),
|
||||
backLayer: HomePage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Backdrop extends StatefulWidget {
|
||||
final Widget frontLayer;
|
||||
final Widget backLayer;
|
||||
final AnimationController controller;
|
||||
final Animation<double> openSettingsAnimation;
|
||||
final ValueNotifier<bool> isSettingsOpenNotifier;
|
||||
|
||||
Backdrop({
|
||||
Key key,
|
||||
@required this.frontLayer,
|
||||
@required this.backLayer,
|
||||
@required this.controller,
|
||||
@required this.openSettingsAnimation,
|
||||
@required this.isSettingsOpenNotifier,
|
||||
}) : assert(frontLayer != null),
|
||||
assert(backLayer != null),
|
||||
assert(controller != null),
|
||||
assert(isSettingsOpenNotifier != null),
|
||||
assert(openSettingsAnimation != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
_BackdropState createState() => _BackdropState();
|
||||
}
|
||||
|
||||
class _BackdropState extends State<Backdrop>
|
||||
with SingleTickerProviderStateMixin, FlareController {
|
||||
FlareAnimationLayer _animationLayer;
|
||||
FlutterActorArtboard _artboard;
|
||||
|
||||
double settingsButtonWidth = 64;
|
||||
double settingsButtonHeightDesktop = 56;
|
||||
double settingsButtonHeightMobile = 40;
|
||||
|
||||
FocusNode frontLayerFocusNode;
|
||||
FocusNode backLayerFocusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
frontLayerFocusNode = FocusNode();
|
||||
backLayerFocusNode = FocusNode();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
frontLayerFocusNode.dispose();
|
||||
backLayerFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initialize(FlutterActorArtboard artboard) {
|
||||
_artboard = artboard;
|
||||
initAnimationLayer();
|
||||
}
|
||||
|
||||
@override
|
||||
void setViewTransform(Mat2D viewTransform) {
|
||||
// This is a necessary override for the [FlareController] mixin.
|
||||
}
|
||||
|
||||
@override
|
||||
bool advance(FlutterActorArtboard artboard, double elapsed) {
|
||||
if (_animationLayer != null) {
|
||||
FlareAnimationLayer layer = _animationLayer;
|
||||
layer.time = widget.controller.value * layer.duration;
|
||||
layer.animation.apply(layer.time, _artboard, 1);
|
||||
if (layer.isDone || layer.time == 0) {
|
||||
_animationLayer = null;
|
||||
}
|
||||
}
|
||||
return _animationLayer != null;
|
||||
}
|
||||
|
||||
void initAnimationLayer() {
|
||||
if (_artboard != null) {
|
||||
final animationName = "Animations";
|
||||
ActorAnimation animation = _artboard.getAnimation(animationName);
|
||||
_animationLayer = FlareAnimationLayer()
|
||||
..name = animationName
|
||||
..animation = animation;
|
||||
}
|
||||
}
|
||||
|
||||
void toggleSettings() {
|
||||
// Animate the settings panel to open or close.
|
||||
widget.controller
|
||||
.fling(velocity: widget.isSettingsOpenNotifier.value ? -1 : 1);
|
||||
setState(() {
|
||||
widget.isSettingsOpenNotifier.value =
|
||||
!widget.isSettingsOpenNotifier.value;
|
||||
});
|
||||
// Animate the settings icon.
|
||||
initAnimationLayer();
|
||||
isActive.value = true;
|
||||
}
|
||||
|
||||
Animation<RelativeRect> _getPanelAnimation(BoxConstraints constraints) {
|
||||
final double height = constraints.biggest.height;
|
||||
final double top = height - galleryHeaderHeight;
|
||||
final double bottom = -galleryHeaderHeight;
|
||||
return RelativeRectTween(
|
||||
begin: RelativeRect.fromLTRB(0, 0, 0, 0),
|
||||
end: RelativeRect.fromLTRB(0, top, 0, bottom),
|
||||
).animate(CurvedAnimation(
|
||||
parent: widget.openSettingsAnimation,
|
||||
curve: Curves.linear,
|
||||
));
|
||||
}
|
||||
|
||||
Widget _galleryHeader() {
|
||||
return ExcludeSemantics(
|
||||
excluding: widget.isSettingsOpenNotifier.value,
|
||||
child: Semantics(
|
||||
sortKey: OrdinalSortKey(
|
||||
GalleryOptions.of(context).textDirection() == TextDirection.ltr
|
||||
? 1.0
|
||||
: 2.0,
|
||||
name: 'header',
|
||||
),
|
||||
label: GalleryLocalizations.of(context).homeHeaderGallery,
|
||||
child: Container(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
|
||||
final isDesktop = isDisplayDesktop(context);
|
||||
final safeAreaTopPadding = MediaQuery.of(context).padding.top;
|
||||
|
||||
final Widget frontLayer = ExcludeSemantics(
|
||||
child: DefaultFocusTraversal(
|
||||
policy: WidgetOrderFocusTraversalPolicy(),
|
||||
child: Focus(
|
||||
skipTraversal: !widget.isSettingsOpenNotifier.value,
|
||||
child: widget.frontLayer,
|
||||
),
|
||||
),
|
||||
excluding: !widget.isSettingsOpenNotifier.value,
|
||||
);
|
||||
final Widget backLayer = ExcludeSemantics(
|
||||
child: widget.backLayer,
|
||||
excluding: widget.isSettingsOpenNotifier.value,
|
||||
);
|
||||
|
||||
return DefaultFocusTraversal(
|
||||
child: InheritedBackdropFocusNodes(
|
||||
frontLayerFocusNode: frontLayerFocusNode,
|
||||
backLayerFocusNode: backLayerFocusNode,
|
||||
child: Container(
|
||||
child: Stack(
|
||||
children: [
|
||||
if (!isDesktop) ...[
|
||||
_galleryHeader(),
|
||||
frontLayer,
|
||||
PositionedTransition(
|
||||
rect: _getPanelAnimation(constraints),
|
||||
child: backLayer,
|
||||
),
|
||||
],
|
||||
if (isDesktop) ...[
|
||||
_galleryHeader(),
|
||||
backLayer,
|
||||
if (widget.isSettingsOpenNotifier.value) ...[
|
||||
ExcludeSemantics(
|
||||
child: ModalBarrier(
|
||||
dismissible: true,
|
||||
),
|
||||
),
|
||||
Semantics(
|
||||
label: GalleryLocalizations.of(context)
|
||||
.settingsButtonCloseLabel,
|
||||
child: GestureDetector(
|
||||
onTap: toggleSettings,
|
||||
),
|
||||
)
|
||||
],
|
||||
ScaleTransition(
|
||||
alignment: Directionality.of(context) == TextDirection.ltr
|
||||
? Alignment.topRight
|
||||
: Alignment.topLeft,
|
||||
scale: CurvedAnimation(
|
||||
parent: isDesktop
|
||||
? widget.controller
|
||||
: widget.openSettingsAnimation,
|
||||
curve: Curves.easeIn,
|
||||
reverseCurve: Curves.easeOut,
|
||||
),
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.topEnd,
|
||||
child: Material(
|
||||
elevation: 7,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
color: Theme.of(context).colorScheme.secondaryVariant,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 560,
|
||||
maxWidth: desktopSettingsWidth,
|
||||
minWidth: desktopSettingsWidth,
|
||||
),
|
||||
child: frontLayer,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
Align(
|
||||
alignment: AlignmentDirectional.topEnd,
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: widget.isSettingsOpenNotifier.value
|
||||
? GalleryLocalizations.of(context)
|
||||
.settingsButtonCloseLabel
|
||||
: GalleryLocalizations.of(context).settingsButtonLabel,
|
||||
child: SizedBox(
|
||||
width: settingsButtonWidth,
|
||||
height: isDesktop
|
||||
? settingsButtonHeightDesktop
|
||||
: settingsButtonHeightMobile + safeAreaTopPadding,
|
||||
child: Material(
|
||||
borderRadius: BorderRadiusDirectional.only(
|
||||
bottomStart: Radius.circular(10),
|
||||
),
|
||||
color: widget.isSettingsOpenNotifier.value &
|
||||
!widget.controller.isAnimating
|
||||
? Colors.transparent
|
||||
: Theme.of(context).colorScheme.secondaryVariant,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: toggleSettings,
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.only(
|
||||
start: 3, end: 18),
|
||||
child: Focus(
|
||||
onFocusChange: (hasFocus) {
|
||||
if (!hasFocus) {
|
||||
FocusScope.of(context).requestFocus(
|
||||
(widget.isSettingsOpenNotifier.value)
|
||||
? frontLayerFocusNode
|
||||
: backLayerFocusNode);
|
||||
}
|
||||
},
|
||||
child: FlareActor(
|
||||
Theme.of(context).colorScheme.brightness ==
|
||||
Brightness.light
|
||||
? 'assets/icons/settings/settings_light.flr'
|
||||
: 'assets/icons/settings/settings_dark.flr',
|
||||
alignment: Directionality.of(context) ==
|
||||
TextDirection.ltr
|
||||
? Alignment.bottomLeft
|
||||
: Alignment.bottomRight,
|
||||
fit: BoxFit.contain,
|
||||
controller: this,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: _buildStack,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InheritedBackdropFocusNodes extends InheritedWidget {
|
||||
InheritedBackdropFocusNodes({
|
||||
@required Widget child,
|
||||
@required this.frontLayerFocusNode,
|
||||
@required this.backLayerFocusNode,
|
||||
}) : assert(child != null),
|
||||
super(child: child);
|
||||
|
||||
final FocusNode frontLayerFocusNode;
|
||||
final FocusNode backLayerFocusNode;
|
||||
|
||||
static InheritedBackdropFocusNodes of(BuildContext context) =>
|
||||
context.dependOnInheritedWidgetOfExactType();
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(InheritedWidget oldWidget) => true;
|
||||
}
|
||||
308
gallery/lib/pages/category_list_item.dart
Normal file
308
gallery/lib/pages/category_list_item.dart
Normal file
@@ -0,0 +1,308 @@
|
||||
// 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/demos.dart';
|
||||
import 'package:gallery/layout/adaptive.dart';
|
||||
import 'package:gallery/pages/demo.dart';
|
||||
|
||||
class CategoryListItem extends StatefulWidget {
|
||||
const CategoryListItem({
|
||||
Key key,
|
||||
this.title,
|
||||
this.imageString,
|
||||
this.demos = const [],
|
||||
}) : super(key: key);
|
||||
|
||||
final String title;
|
||||
final String imageString;
|
||||
final List<GalleryDemo> demos;
|
||||
|
||||
@override
|
||||
_CategoryListItemState createState() => _CategoryListItemState();
|
||||
}
|
||||
|
||||
class _CategoryListItemState extends State<CategoryListItem>
|
||||
with SingleTickerProviderStateMixin {
|
||||
static final Animatable<double> _easeInTween =
|
||||
CurveTween(curve: Curves.easeIn);
|
||||
static const _expandDuration = Duration(milliseconds: 200);
|
||||
AnimationController _controller;
|
||||
Animation<double> _childrenHeightFactor;
|
||||
Animation<double> _headerChevronOpacity;
|
||||
Animation<double> _headerHeight;
|
||||
Animation<EdgeInsetsGeometry> _headerMargin;
|
||||
Animation<EdgeInsetsGeometry> _headerImagePadding;
|
||||
Animation<EdgeInsetsGeometry> _childrenPadding;
|
||||
Animation<BorderRadius> _headerBorderRadius;
|
||||
bool _isExpanded = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(duration: _expandDuration, vsync: this);
|
||||
_childrenHeightFactor = _controller.drive(_easeInTween);
|
||||
_headerChevronOpacity = _controller.drive(_easeInTween);
|
||||
_headerHeight = Tween<double>(
|
||||
begin: 80,
|
||||
end: 96,
|
||||
).animate(_controller);
|
||||
_headerMargin = EdgeInsetsGeometryTween(
|
||||
begin: EdgeInsets.fromLTRB(32, 8, 32, 8),
|
||||
end: EdgeInsets.zero,
|
||||
).animate(_controller);
|
||||
_headerImagePadding = EdgeInsetsGeometryTween(
|
||||
begin: EdgeInsets.all(8),
|
||||
end: EdgeInsetsDirectional.fromSTEB(16, 8, 8, 8),
|
||||
).animate(_controller);
|
||||
_childrenPadding = EdgeInsetsGeometryTween(
|
||||
begin: EdgeInsets.symmetric(horizontal: 32),
|
||||
end: EdgeInsets.zero,
|
||||
).animate(_controller);
|
||||
_headerBorderRadius = BorderRadiusTween(
|
||||
begin: BorderRadius.circular(10),
|
||||
end: BorderRadius.zero,
|
||||
).animate(_controller);
|
||||
|
||||
if (_isExpanded) {
|
||||
_controller.value = 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleTap() {
|
||||
setState(() {
|
||||
_isExpanded = !_isExpanded;
|
||||
if (_isExpanded) {
|
||||
_controller.forward();
|
||||
} else {
|
||||
_controller.reverse().then<void>((value) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
// Rebuild.
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildHeaderWithChildren(BuildContext context, Widget child) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_CategoryHeader(
|
||||
margin: _headerMargin.value,
|
||||
imagePadding: _headerImagePadding.value,
|
||||
borderRadius: _headerBorderRadius.value,
|
||||
height: _headerHeight.value,
|
||||
chevronOpacity: _headerChevronOpacity.value,
|
||||
imageString: widget.imageString,
|
||||
title: widget.title,
|
||||
onTap: _handleTap,
|
||||
),
|
||||
Padding(
|
||||
padding: _childrenPadding.value,
|
||||
child: ClipRect(
|
||||
child: Align(
|
||||
heightFactor: _childrenHeightFactor.value,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool closed = !_isExpanded && _controller.isDismissed;
|
||||
return AnimatedBuilder(
|
||||
animation: _controller.view,
|
||||
builder: _buildHeaderWithChildren,
|
||||
child: closed ? null : _ExpandedCategoryDemos(demos: widget.demos),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CategoryHeader extends StatelessWidget {
|
||||
const _CategoryHeader({
|
||||
Key key,
|
||||
this.margin,
|
||||
this.imagePadding,
|
||||
this.borderRadius,
|
||||
this.height,
|
||||
this.chevronOpacity,
|
||||
this.imageString,
|
||||
this.title,
|
||||
this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
final EdgeInsetsGeometry margin;
|
||||
final EdgeInsetsGeometry imagePadding;
|
||||
final double height;
|
||||
final BorderRadiusGeometry borderRadius;
|
||||
final String imageString;
|
||||
final String title;
|
||||
final double chevronOpacity;
|
||||
final GestureTapCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
margin: margin,
|
||||
child: Material(
|
||||
shape: RoundedRectangleBorder(borderRadius: borderRadius),
|
||||
color: colorScheme.onBackground,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: imagePadding,
|
||||
child: ExcludeSemantics(
|
||||
child: Image.asset(
|
||||
imageString,
|
||||
width: 64,
|
||||
height: 64,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: 8),
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.headline.apply(
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Opacity(
|
||||
opacity: chevronOpacity,
|
||||
child: chevronOpacity != 0
|
||||
? Padding(
|
||||
padding: const EdgeInsetsDirectional.only(
|
||||
start: 8,
|
||||
end: 32,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_up,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExpandedCategoryDemos extends StatelessWidget {
|
||||
const _ExpandedCategoryDemos({
|
||||
Key key,
|
||||
this.demos,
|
||||
}) : super(key: key);
|
||||
|
||||
final List<GalleryDemo> demos;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
for (final demo in demos)
|
||||
CategoryDemoItem(
|
||||
demo: demo,
|
||||
),
|
||||
const SizedBox(height: 12), // Extra space below.
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CategoryDemoItem extends StatelessWidget {
|
||||
const CategoryDemoItem({Key key, this.demo}) : super(key: key);
|
||||
|
||||
final GalleryDemo demo;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: MergeSemantics(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.push<void>(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => DemoPage(demo: demo)),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: EdgeInsetsDirectional.only(
|
||||
start: 32,
|
||||
top: 20,
|
||||
end: isDisplayDesktop(context) ? 16 : 8,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
demo.icon,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
SizedBox(width: 40),
|
||||
Flexible(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
demo.title,
|
||||
style: textTheme.subhead
|
||||
.apply(color: colorScheme.onSurface),
|
||||
),
|
||||
Text(
|
||||
demo.subtitle,
|
||||
style: textTheme.overline.apply(
|
||||
color: colorScheme.onSurface.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
Divider(
|
||||
thickness: 1,
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
764
gallery/lib/pages/demo.dart
Normal file
764
gallery/lib/pages/demo.dart
Normal file
@@ -0,0 +1,764 @@
|
||||
// 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:io' show Platform;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:gallery/codeviewer/code_displayer.dart';
|
||||
import 'package:gallery/codeviewer/code_style.dart';
|
||||
import 'package:gallery/constants.dart';
|
||||
import 'package:gallery/data/demos.dart';
|
||||
import 'package:gallery/data/gallery_options.dart';
|
||||
import 'package:gallery/feature_discovery/feature_discovery.dart';
|
||||
import 'package:gallery/l10n/gallery_localizations.dart';
|
||||
import 'package:gallery/layout/adaptive.dart';
|
||||
import 'package:gallery/pages/splash.dart';
|
||||
import 'package:gallery/themes/gallery_theme_data.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
const _demoViewedCountKey = 'demoViewedCountKey';
|
||||
|
||||
enum _DemoState {
|
||||
normal,
|
||||
options,
|
||||
info,
|
||||
code,
|
||||
fullscreen,
|
||||
}
|
||||
|
||||
class DemoPage extends StatefulWidget {
|
||||
const DemoPage({
|
||||
Key key,
|
||||
@required this.demo,
|
||||
}) : super(key: key);
|
||||
|
||||
final GalleryDemo demo;
|
||||
|
||||
@override
|
||||
_DemoPageState createState() => _DemoPageState();
|
||||
}
|
||||
|
||||
class _DemoPageState extends State<DemoPage> with TickerProviderStateMixin {
|
||||
_DemoState _state = _DemoState.normal;
|
||||
int _configIndex = 0;
|
||||
bool _isDesktop;
|
||||
bool _showFeatureHighlight = true;
|
||||
int _demoViewedCount;
|
||||
|
||||
AnimationController _codeBackgroundColorController;
|
||||
|
||||
GalleryDemoConfiguration get _currentConfig {
|
||||
return widget.demo.configurations[_configIndex];
|
||||
}
|
||||
|
||||
bool get _hasOptions => widget.demo.configurations.length > 1;
|
||||
|
||||
bool get _isSupportedSharedPreferencesPlatform =>
|
||||
!kIsWeb && (Platform.isAndroid || Platform.isIOS);
|
||||
|
||||
// Only show the feature highlight on Android/iOS in mobile layout and only on
|
||||
// the first and forth time the demo page is viewed.
|
||||
bool _showFeatureHighlightForPlatform(BuildContext context) {
|
||||
return _showFeatureHighlight &&
|
||||
_isSupportedSharedPreferencesPlatform &&
|
||||
!isDisplayDesktop(context) &&
|
||||
(_demoViewedCount != null &&
|
||||
(_demoViewedCount == 0 || _demoViewedCount == 3));
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_codeBackgroundColorController = AnimationController(
|
||||
vsync: this,
|
||||
duration: Duration(milliseconds: 300),
|
||||
);
|
||||
SharedPreferences.getInstance().then((preferences) {
|
||||
setState(() {
|
||||
_demoViewedCount = preferences.getInt(_demoViewedCountKey) ?? 0;
|
||||
preferences.setInt(_demoViewedCountKey, _demoViewedCount + 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_codeBackgroundColorController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (_isDesktop == null) {
|
||||
_isDesktop = isDisplayDesktop(context);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets state and updates the background color for code.
|
||||
void setStateAndUpdate(VoidCallback callback) {
|
||||
setState(() {
|
||||
callback();
|
||||
if (_state == _DemoState.code) {
|
||||
_codeBackgroundColorController.forward();
|
||||
} else {
|
||||
_codeBackgroundColorController.reverse();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _handleTap(_DemoState newState) {
|
||||
// Do not allow normal state for desktop.
|
||||
if (_state == newState && isDisplayDesktop(context)) {
|
||||
if (_state == _DemoState.fullscreen) {
|
||||
setStateAndUpdate(() {
|
||||
_state = _hasOptions ? _DemoState.options : _DemoState.info;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setStateAndUpdate(() {
|
||||
_state = _state == newState ? _DemoState.normal : newState;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _showDocumentation(BuildContext context) async {
|
||||
final url = _currentConfig.documentationUrl;
|
||||
if (url == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (await canLaunch(url)) {
|
||||
await launch(url);
|
||||
} else {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return SimpleDialog(
|
||||
title: Text(GalleryLocalizations.of(context).demoInvalidURL),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(url),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _resolveState(BuildContext context) {
|
||||
final isDesktop = isDisplayDesktop(context);
|
||||
if (_state == _DemoState.fullscreen && !isDesktop) {
|
||||
// Do not allow fullscreen state for mobile.
|
||||
_state = _DemoState.normal;
|
||||
} else if (_state == _DemoState.normal && isDesktop) {
|
||||
// Do not allow normal state for desktop.
|
||||
_state = _hasOptions ? _DemoState.options : _DemoState.info;
|
||||
} else if (isDesktop != this._isDesktop) {
|
||||
this._isDesktop = isDesktop;
|
||||
// When going from desktop to mobile, return to normal state.
|
||||
if (!isDesktop) {
|
||||
_state = _DemoState.normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
bool isDesktop = isDisplayDesktop(context);
|
||||
_resolveState(context);
|
||||
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final iconColor = colorScheme.onSurface;
|
||||
final selectedIconColor = colorScheme.primary;
|
||||
final appBarPadding = isDesktop ? 20.0 : 0.0;
|
||||
|
||||
final appBar = AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
leading: Padding(
|
||||
padding: EdgeInsetsDirectional.only(start: appBarPadding),
|
||||
child: IconButton(
|
||||
icon: const BackButtonIcon(),
|
||||
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
||||
onPressed: () {
|
||||
Navigator.maybePop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
if (_hasOptions)
|
||||
IconButton(
|
||||
icon: FeatureDiscovery(
|
||||
title: GalleryLocalizations.of(context).demoOptionsFeatureTitle,
|
||||
description: GalleryLocalizations.of(context)
|
||||
.demoOptionsFeatureDescription,
|
||||
showOverlay: _showFeatureHighlightForPlatform(context),
|
||||
color: colorScheme.primary,
|
||||
onDismiss: () {
|
||||
setState(() {
|
||||
_showFeatureHighlight = false;
|
||||
});
|
||||
},
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_showFeatureHighlight = false;
|
||||
});
|
||||
},
|
||||
child: Icon(
|
||||
Icons.tune,
|
||||
color: _state == _DemoState.options ||
|
||||
_showFeatureHighlightForPlatform(context)
|
||||
? selectedIconColor
|
||||
: iconColor,
|
||||
),
|
||||
),
|
||||
tooltip: GalleryLocalizations.of(context).demoOptionsTooltip,
|
||||
onPressed: () => _handleTap(_DemoState.options),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.info),
|
||||
tooltip: GalleryLocalizations.of(context).demoInfoTooltip,
|
||||
color: _state == _DemoState.info ? selectedIconColor : iconColor,
|
||||
onPressed: () => _handleTap(_DemoState.info),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.code),
|
||||
tooltip: GalleryLocalizations.of(context).demoCodeTooltip,
|
||||
color: _state == _DemoState.code ? selectedIconColor : iconColor,
|
||||
onPressed: () => _handleTap(_DemoState.code),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.library_books),
|
||||
tooltip: GalleryLocalizations.of(context).demoDocumentationTooltip,
|
||||
color: iconColor,
|
||||
onPressed: () => _showDocumentation(context),
|
||||
),
|
||||
if (isDesktop)
|
||||
IconButton(
|
||||
icon: Icon(Icons.fullscreen),
|
||||
tooltip: GalleryLocalizations.of(context).demoFullscreenTooltip,
|
||||
color:
|
||||
_state == _DemoState.fullscreen ? selectedIconColor : iconColor,
|
||||
onPressed: () => _handleTap(_DemoState.fullscreen),
|
||||
),
|
||||
SizedBox(width: appBarPadding),
|
||||
],
|
||||
);
|
||||
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
final bottomSafeArea = mediaQuery.padding.bottom;
|
||||
final contentHeight = mediaQuery.size.height -
|
||||
mediaQuery.padding.top -
|
||||
mediaQuery.padding.bottom -
|
||||
appBar.preferredSize.height;
|
||||
final maxSectionHeight = isDesktop ? contentHeight : contentHeight - 64;
|
||||
final horizontalPadding = isDesktop ? mediaQuery.size.width * 0.12 : 0.0;
|
||||
final maxSectionWidth = 420.0;
|
||||
|
||||
Widget section;
|
||||
switch (_state) {
|
||||
case _DemoState.options:
|
||||
section = _DemoSectionOptions(
|
||||
maxHeight: maxSectionHeight,
|
||||
maxWidth: maxSectionWidth,
|
||||
configurations: widget.demo.configurations,
|
||||
configIndex: _configIndex,
|
||||
onConfigChanged: (index) {
|
||||
setStateAndUpdate(() {
|
||||
_configIndex = index;
|
||||
if (!isDesktop) {
|
||||
_state = _DemoState.normal;
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
break;
|
||||
case _DemoState.info:
|
||||
section = _DemoSectionInfo(
|
||||
maxHeight: maxSectionHeight,
|
||||
maxWidth: maxSectionWidth,
|
||||
title: _currentConfig.title,
|
||||
description: _currentConfig.description,
|
||||
);
|
||||
break;
|
||||
case _DemoState.code:
|
||||
final TextStyle codeTheme = GoogleFonts.robotoMono(
|
||||
fontSize: 12 * GalleryOptions.of(context).textScaleFactor(context),
|
||||
);
|
||||
section = CodeStyle(
|
||||
baseStyle: codeTheme.copyWith(color: Color(0xFFFAFBFB)),
|
||||
numberStyle: codeTheme.copyWith(color: Color(0xFFBD93F9)),
|
||||
commentStyle: codeTheme.copyWith(color: Color(0xFF808080)),
|
||||
keywordStyle: codeTheme.copyWith(color: Color(0xFF1CDEC9)),
|
||||
stringStyle: codeTheme.copyWith(color: Color(0xFFFFA65C)),
|
||||
punctuationStyle: codeTheme.copyWith(color: Color(0xFF8BE9FD)),
|
||||
classStyle: codeTheme.copyWith(color: Color(0xFFD65BAD)),
|
||||
constantStyle: codeTheme.copyWith(color: Color(0xFFFF8383)),
|
||||
child: _DemoSectionCode(
|
||||
maxHeight: maxSectionHeight,
|
||||
codeWidget: CodeDisplayPage(
|
||||
_currentConfig.code,
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
section = Container();
|
||||
break;
|
||||
}
|
||||
|
||||
Widget body;
|
||||
Widget demoContent = DemoContent(
|
||||
height: contentHeight,
|
||||
buildRoute: _currentConfig.buildRoute,
|
||||
);
|
||||
if (isDesktop) {
|
||||
final isFullScreen = _state == _DemoState.fullscreen;
|
||||
final Widget sectionAndDemo = Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!isFullScreen) Expanded(child: section),
|
||||
SizedBox(width: !isFullScreen ? 48.0 : 0),
|
||||
Expanded(child: demoContent),
|
||||
],
|
||||
);
|
||||
|
||||
body = SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 56),
|
||||
child: sectionAndDemo,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
section = AnimatedSize(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
alignment: Alignment.topCenter,
|
||||
curve: Curves.easeIn,
|
||||
child: section,
|
||||
);
|
||||
|
||||
// Add a tap gesture to collapse the currently opened section.
|
||||
demoContent = Semantics(
|
||||
label: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (_state != _DemoState.normal) {
|
||||
setStateAndUpdate(() {
|
||||
_state = _DemoState.normal;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Semantics(
|
||||
excludeSemantics: _state != _DemoState.normal,
|
||||
child: demoContent,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
body = SafeArea(
|
||||
bottom: false,
|
||||
child: ListView(
|
||||
// Use a non-scrollable ListView to enable animation of shifting the
|
||||
// demo offscreen.
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
section,
|
||||
demoContent,
|
||||
// Fake the safe area to ensure the animation looks correct.
|
||||
SizedBox(height: bottomSafeArea),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget page;
|
||||
|
||||
if (isDesktop) {
|
||||
page = AnimatedBuilder(
|
||||
animation: _codeBackgroundColorController,
|
||||
builder: (context, child) {
|
||||
Brightness themeBrightness;
|
||||
|
||||
switch (GalleryOptions.of(context).themeMode) {
|
||||
case ThemeMode.system:
|
||||
themeBrightness = MediaQuery.of(context).platformBrightness;
|
||||
break;
|
||||
case ThemeMode.light:
|
||||
themeBrightness = Brightness.light;
|
||||
break;
|
||||
case ThemeMode.dark:
|
||||
themeBrightness = Brightness.dark;
|
||||
break;
|
||||
}
|
||||
|
||||
Widget contents = Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
|
||||
child: ApplyTextOptions(
|
||||
child: Scaffold(
|
||||
appBar: appBar,
|
||||
body: body,
|
||||
backgroundColor: Colors.transparent,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (themeBrightness == Brightness.light) {
|
||||
// If it is currently in light mode, add a
|
||||
// dark background for code.
|
||||
Widget codeBackground = Container(
|
||||
padding: EdgeInsets.only(top: 56),
|
||||
child: Container(
|
||||
color: ColorTween(
|
||||
begin: Colors.transparent,
|
||||
end: GalleryThemeData.darkThemeData.canvasColor,
|
||||
).animate(_codeBackgroundColorController).value,
|
||||
),
|
||||
);
|
||||
|
||||
contents = Stack(
|
||||
children: [
|
||||
codeBackground,
|
||||
contents,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
color: colorScheme.background,
|
||||
child: contents,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
page = Container(
|
||||
color: colorScheme.background,
|
||||
child: ApplyTextOptions(
|
||||
child: Scaffold(
|
||||
appBar: appBar,
|
||||
body: body,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Add the splash page functionality for desktop.
|
||||
if (isDesktop) {
|
||||
page = MediaQuery.removePadding(
|
||||
removeTop: true,
|
||||
context: context,
|
||||
child: SplashPage(
|
||||
isAnimated: false,
|
||||
child: page,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return FeatureDiscoveryController(page);
|
||||
}
|
||||
}
|
||||
|
||||
class _DemoSectionOptions extends StatelessWidget {
|
||||
const _DemoSectionOptions({
|
||||
Key key,
|
||||
this.maxHeight,
|
||||
this.maxWidth,
|
||||
this.configurations,
|
||||
this.configIndex,
|
||||
this.onConfigChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
final double maxHeight;
|
||||
final double maxWidth;
|
||||
final List<GalleryDemoConfiguration> configurations;
|
||||
final int configIndex;
|
||||
final ValueChanged<int> onConfigChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Align(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxHeight: maxHeight, maxWidth: maxWidth),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(
|
||||
start: 24,
|
||||
top: 12,
|
||||
end: 24,
|
||||
),
|
||||
child: Text(
|
||||
GalleryLocalizations.of(context).demoOptionsTooltip,
|
||||
style: textTheme.display1.apply(
|
||||
color: colorScheme.onSurface,
|
||||
fontSizeDelta:
|
||||
isDisplayDesktop(context) ? desktopDisplay1FontDelta : 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
Divider(
|
||||
thickness: 1,
|
||||
height: 16,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
Flexible(
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
for (final configuration in configurations)
|
||||
_DemoSectionOptionsItem(
|
||||
title: configuration.title,
|
||||
isSelected: configuration == configurations[configIndex],
|
||||
onTap: () {
|
||||
onConfigChanged(configurations.indexOf(configuration));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DemoSectionOptionsItem extends StatelessWidget {
|
||||
const _DemoSectionOptionsItem({
|
||||
Key key,
|
||||
this.title,
|
||||
this.isSelected,
|
||||
this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
final String title;
|
||||
final bool isSelected;
|
||||
final GestureTapCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Material(
|
||||
color: isSelected ? colorScheme.surface : null,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(minWidth: double.infinity),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.body1.apply(
|
||||
color:
|
||||
isSelected ? colorScheme.primary : colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DemoSectionInfo extends StatelessWidget {
|
||||
const _DemoSectionInfo({
|
||||
Key key,
|
||||
this.maxHeight,
|
||||
this.maxWidth,
|
||||
this.title,
|
||||
this.description,
|
||||
}) : super(key: key);
|
||||
|
||||
final double maxHeight;
|
||||
final double maxWidth;
|
||||
final String title;
|
||||
final String description;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Align(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
child: Container(
|
||||
padding: const EdgeInsetsDirectional.only(
|
||||
start: 24,
|
||||
top: 12,
|
||||
end: 24,
|
||||
bottom: 32,
|
||||
),
|
||||
constraints: BoxConstraints(maxHeight: maxHeight, maxWidth: maxWidth),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: textTheme.display1.apply(
|
||||
color: colorScheme.onSurface,
|
||||
fontSizeDelta:
|
||||
isDisplayDesktop(context) ? desktopDisplay1FontDelta : 0,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Text(
|
||||
description,
|
||||
style: textTheme.body1.apply(color: colorScheme.onSurface),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DemoContent extends StatelessWidget {
|
||||
const DemoContent({
|
||||
Key key,
|
||||
@required this.height,
|
||||
@required this.buildRoute,
|
||||
}) : super(key: key);
|
||||
|
||||
final double height;
|
||||
final WidgetBuilder buildRoute;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16),
|
||||
height: height,
|
||||
child: Material(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(10.0),
|
||||
bottom: Radius.circular(2.0),
|
||||
),
|
||||
child: DemoWrapper(child: Builder(builder: buildRoute)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DemoSectionCode extends StatelessWidget {
|
||||
const _DemoSectionCode({
|
||||
Key key,
|
||||
this.maxHeight,
|
||||
this.codeWidget,
|
||||
}) : super(key: key);
|
||||
|
||||
final double maxHeight;
|
||||
final Widget codeWidget;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDesktop = isDisplayDesktop(context);
|
||||
|
||||
return Theme(
|
||||
data: GalleryThemeData.darkThemeData,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: 16),
|
||||
child: Container(
|
||||
color: isDesktop ? null : GalleryThemeData.darkThemeData.canvasColor,
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
height: maxHeight,
|
||||
child: codeWidget,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CodeDisplayPage extends StatelessWidget {
|
||||
const CodeDisplayPage(this.code);
|
||||
|
||||
final CodeDisplayer code;
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
final isDesktop = isDisplayDesktop(context);
|
||||
|
||||
final TextSpan _richTextCode = code(context);
|
||||
final String _plainTextCode = _richTextCode.toPlainText();
|
||||
|
||||
void _showSnackBarOnCopySuccess(dynamic result) {
|
||||
Scaffold.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
GalleryLocalizations.of(context)
|
||||
.demoCodeViewerCopiedToClipboardMessage,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showSnackBarOnCopyFailure(Object exception) {
|
||||
Scaffold.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
GalleryLocalizations.of(context)
|
||||
.demoCodeViewerFailedToCopyToClipboardMessage(exception),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: isDesktop
|
||||
? EdgeInsets.only(bottom: 8)
|
||||
: EdgeInsets.symmetric(vertical: 8),
|
||||
child: FlatButton(
|
||||
color: Colors.white.withOpacity(0.15),
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(4)),
|
||||
),
|
||||
onPressed: () async {
|
||||
// The future will not complete on web, thus no
|
||||
// Snackbar will be shown, see https://github.com/flutter/flutter/issues/49349.
|
||||
await Clipboard.setData(ClipboardData(text: _plainTextCode))
|
||||
.then(_showSnackBarOnCopySuccess)
|
||||
.catchError(_showSnackBarOnCopyFailure);
|
||||
},
|
||||
child: Text(
|
||||
GalleryLocalizations.of(context).demoCodeViewerCopyAll,
|
||||
style: Theme.of(context).textTheme.button.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: RichText(
|
||||
textDirection: TextDirection.ltr,
|
||||
text: _richTextCode,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
944
gallery/lib/pages/home.dart
Normal file
944
gallery/lib/pages/home.dart
Normal file
@@ -0,0 +1,944 @@
|
||||
// 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:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/semantics.dart';
|
||||
import 'package:gallery/constants.dart';
|
||||
import 'package:gallery/data/demos.dart';
|
||||
import 'package:gallery/data/gallery_options.dart';
|
||||
import 'package:gallery/l10n/gallery_localizations.dart';
|
||||
import 'package:gallery/layout/adaptive.dart';
|
||||
import 'package:gallery/pages/backdrop.dart';
|
||||
import 'package:gallery/pages/category_list_item.dart';
|
||||
import 'package:gallery/pages/settings.dart';
|
||||
import 'package:gallery/pages/splash.dart';
|
||||
import 'package:gallery/studies/crane/app.dart';
|
||||
import 'package:gallery/studies/crane/colors.dart';
|
||||
import 'package:gallery/studies/fortnightly/app.dart';
|
||||
import 'package:gallery/studies/rally/app.dart';
|
||||
import 'package:gallery/studies/rally/colors.dart';
|
||||
import 'package:gallery/studies/shrine/app.dart';
|
||||
import 'package:gallery/studies/shrine/colors.dart';
|
||||
import 'package:gallery/studies/starter/app.dart';
|
||||
|
||||
const _horizontalPadding = 32.0;
|
||||
const _carouselItemMargin = 8.0;
|
||||
const _horizontalDesktopPadding = 81.0;
|
||||
const _carouselHeightMin = 200.0 + 2 * _carouselItemMargin;
|
||||
|
||||
const shrineTitle = 'Shrine';
|
||||
const rallyTitle = 'Rally';
|
||||
const craneTitle = 'Crane';
|
||||
const homeCategoryMaterial = 'MATERIAL';
|
||||
const homeCategoryCupertino = 'CUPERTINO';
|
||||
|
||||
class ToggleSplashNotification extends Notification {}
|
||||
|
||||
class NavigatorKeys {
|
||||
static final shrine = GlobalKey<NavigatorState>();
|
||||
static final rally = GlobalKey<NavigatorState>();
|
||||
static final crane = GlobalKey<NavigatorState>();
|
||||
static final fortnightly = GlobalKey<NavigatorState>();
|
||||
static final starter = GlobalKey<NavigatorState>();
|
||||
}
|
||||
|
||||
class HomePage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var carouselHeight = _carouselHeight(.7, context);
|
||||
final isDesktop = isDisplayDesktop(context);
|
||||
final carouselCards = <_CarouselCard>[
|
||||
_CarouselCard(
|
||||
title: shrineTitle,
|
||||
subtitle: GalleryLocalizations.of(context).shrineDescription,
|
||||
asset: 'assets/studies/shrine_card.png',
|
||||
assetDark: 'assets/studies/shrine_card_dark.png',
|
||||
textColor: shrineBrown900,
|
||||
study: ShrineApp(navigatorKey: NavigatorKeys.shrine),
|
||||
navigatorKey: NavigatorKeys.shrine,
|
||||
),
|
||||
_CarouselCard(
|
||||
title: rallyTitle,
|
||||
subtitle: GalleryLocalizations.of(context).rallyDescription,
|
||||
textColor: RallyColors.accountColors[0],
|
||||
asset: 'assets/studies/rally_card.png',
|
||||
assetDark: 'assets/studies/rally_card_dark.png',
|
||||
study: RallyApp(navigatorKey: NavigatorKeys.rally),
|
||||
navigatorKey: NavigatorKeys.rally,
|
||||
),
|
||||
_CarouselCard(
|
||||
title: craneTitle,
|
||||
subtitle: GalleryLocalizations.of(context).craneDescription,
|
||||
asset: 'assets/studies/crane_card.png',
|
||||
assetDark: 'assets/studies/crane_card_dark.png',
|
||||
textColor: cranePurple700,
|
||||
study: CraneApp(navigatorKey: NavigatorKeys.crane),
|
||||
navigatorKey: NavigatorKeys.crane,
|
||||
),
|
||||
_CarouselCard(
|
||||
title: fortnightlyTitle,
|
||||
subtitle: GalleryLocalizations.of(context).fortnightlyDescription,
|
||||
// TODO: Provide asset for study banner.
|
||||
study: FortnightlyApp(navigatorKey: NavigatorKeys.fortnightly),
|
||||
navigatorKey: NavigatorKeys.fortnightly,
|
||||
),
|
||||
_CarouselCard(
|
||||
title: GalleryLocalizations.of(context).starterAppTitle,
|
||||
subtitle: GalleryLocalizations.of(context).starterAppDescription,
|
||||
asset: 'assets/studies/starter_card.png',
|
||||
assetDark: 'assets/studies/starter_card_dark.png',
|
||||
textColor: Colors.black,
|
||||
study: StarterApp(navigatorKey: NavigatorKeys.starter),
|
||||
navigatorKey: NavigatorKeys.starter,
|
||||
),
|
||||
];
|
||||
|
||||
if (isDesktop) {
|
||||
final desktopCategoryItems = <_DesktopCategoryItem>[
|
||||
_DesktopCategoryItem(
|
||||
title: homeCategoryMaterial,
|
||||
imageString: 'assets/icons/material/material.png',
|
||||
demos: materialDemos(context),
|
||||
),
|
||||
_DesktopCategoryItem(
|
||||
title: homeCategoryCupertino,
|
||||
imageString: 'assets/icons/cupertino/cupertino.png',
|
||||
demos: cupertinoDemos(context),
|
||||
),
|
||||
_DesktopCategoryItem(
|
||||
title: GalleryLocalizations.of(context).homeCategoryReference,
|
||||
imageString: 'assets/icons/reference/reference.png',
|
||||
demos: referenceDemos(context),
|
||||
),
|
||||
];
|
||||
|
||||
return Scaffold(
|
||||
body: ListView(
|
||||
padding: EdgeInsetsDirectional.only(
|
||||
start: _horizontalDesktopPadding,
|
||||
top: isDesktop ? firstHeaderDesktopTopPadding : 21,
|
||||
end: _horizontalDesktopPadding,
|
||||
),
|
||||
children: [
|
||||
ExcludeSemantics(child: _GalleryHeader()),
|
||||
|
||||
/// TODO: When Focus widget becomes better remove dummy Focus
|
||||
/// variable.
|
||||
|
||||
/// This [Focus] widget grabs focus from the settingsIcon,
|
||||
/// when settings isn't open.
|
||||
/// The container following the Focus widget isn't wrapped with
|
||||
/// Focus because anytime FocusScope.of(context).requestFocus() the
|
||||
/// focused widget will be skipped. We want to be able to focus on
|
||||
/// the container which is why we created this Focus variable.
|
||||
Focus(
|
||||
focusNode:
|
||||
InheritedBackdropFocusNodes.of(context).backLayerFocusNode,
|
||||
child: SizedBox(),
|
||||
),
|
||||
Container(
|
||||
height: carouselHeight,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: spaceBetween(30, carouselCards),
|
||||
),
|
||||
),
|
||||
_CategoriesHeader(),
|
||||
Container(
|
||||
height: 585,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: spaceBetween(28, desktopCategoryItems),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(
|
||||
bottom: 81,
|
||||
top: 109,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
Theme.of(context).colorScheme.brightness == Brightness.dark
|
||||
? 'assets/logo/flutter_logo.png'
|
||||
: 'assets/logo/flutter_logo_color.png',
|
||||
excludeFromSemantics: true,
|
||||
),
|
||||
Expanded(
|
||||
child: Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
alignment: WrapAlignment.end,
|
||||
children: [
|
||||
SettingsAbout(),
|
||||
SettingsFeedback(),
|
||||
SettingsAttribution(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Scaffold(
|
||||
body: _AnimatedHomePage(
|
||||
isSplashPageAnimationFinished:
|
||||
SplashPageAnimation.of(context).isFinished,
|
||||
carouselCards: carouselCards,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> spaceBetween(double paddingBetween, List<Widget> children) {
|
||||
return [
|
||||
for (int index = 0; index < children.length; index++) ...[
|
||||
Flexible(
|
||||
child: children[index],
|
||||
),
|
||||
if (index < children.length - 1) SizedBox(width: paddingBetween),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class _GalleryHeader extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Header(
|
||||
color: Theme.of(context).colorScheme.primaryVariant,
|
||||
text: GalleryLocalizations.of(context).homeHeaderGallery,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CategoriesHeader extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Header(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
text: GalleryLocalizations.of(context).homeHeaderCategories,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Header extends StatelessWidget {
|
||||
const Header({this.color, this.text});
|
||||
|
||||
final Color color;
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: isDisplayDesktop(context) ? 63 : 15,
|
||||
bottom: isDisplayDesktop(context) ? 21 : 11,
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.display1.apply(
|
||||
color: color,
|
||||
fontSizeDelta:
|
||||
isDisplayDesktop(context) ? desktopDisplay1FontDelta : 0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AnimatedHomePage extends StatefulWidget {
|
||||
const _AnimatedHomePage({
|
||||
Key key,
|
||||
@required this.carouselCards,
|
||||
@required this.isSplashPageAnimationFinished,
|
||||
}) : super(key: key);
|
||||
|
||||
final List<Widget> carouselCards;
|
||||
final bool isSplashPageAnimationFinished;
|
||||
|
||||
@override
|
||||
_AnimatedHomePageState createState() => _AnimatedHomePageState();
|
||||
}
|
||||
|
||||
class _AnimatedHomePageState extends State<_AnimatedHomePage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
AnimationController _animationController;
|
||||
Timer _launchTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: Duration(milliseconds: 800),
|
||||
);
|
||||
|
||||
if (widget.isSplashPageAnimationFinished) {
|
||||
// To avoid the animation from running when changing the window size from
|
||||
// desktop to mobile, we do not animate our widget if the
|
||||
// splash page animation is finished on initState.
|
||||
_animationController.value = 1.0;
|
||||
} else {
|
||||
// Start our animation halfway through the splash page animation.
|
||||
_launchTimer = Timer(
|
||||
const Duration(
|
||||
milliseconds: splashPageAnimationDurationInMilliseconds ~/ 2,
|
||||
),
|
||||
() {
|
||||
_animationController.forward();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
_animationController.dispose();
|
||||
_launchTimer?.cancel();
|
||||
_launchTimer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
ListView(
|
||||
children: [
|
||||
SizedBox(height: 8),
|
||||
Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: _horizontalPadding),
|
||||
child: ExcludeSemantics(child: _GalleryHeader()),
|
||||
),
|
||||
_Carousel(
|
||||
children: widget.carouselCards,
|
||||
animationController: _animationController,
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: _horizontalPadding),
|
||||
child: _CategoriesHeader(),
|
||||
),
|
||||
_AnimatedCategoryItem(
|
||||
startDelayFraction: 0.00,
|
||||
controller: _animationController,
|
||||
child: CategoryListItem(
|
||||
title: homeCategoryMaterial,
|
||||
imageString: 'assets/icons/material/material.png',
|
||||
demos: materialDemos(context),
|
||||
),
|
||||
),
|
||||
_AnimatedCategoryItem(
|
||||
startDelayFraction: 0.05,
|
||||
controller: _animationController,
|
||||
child: CategoryListItem(
|
||||
title: homeCategoryCupertino,
|
||||
imageString: 'assets/icons/cupertino/cupertino.png',
|
||||
demos: cupertinoDemos(context),
|
||||
),
|
||||
),
|
||||
_AnimatedCategoryItem(
|
||||
startDelayFraction: 0.10,
|
||||
controller: _animationController,
|
||||
child: CategoryListItem(
|
||||
title: GalleryLocalizations.of(context).homeCategoryReference,
|
||||
imageString: 'assets/icons/reference/reference.png',
|
||||
demos: referenceDemos(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: GestureDetector(
|
||||
onVerticalDragEnd: (details) {
|
||||
if (details.velocity.pixelsPerSecond.dy > 200) {
|
||||
ToggleSplashNotification()..dispatch(context);
|
||||
}
|
||||
},
|
||||
child: SafeArea(
|
||||
child: Container(
|
||||
height: 40,
|
||||
// If we don't set the color, gestures are not detected.
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DesktopCategoryItem extends StatelessWidget {
|
||||
const _DesktopCategoryItem({
|
||||
this.title,
|
||||
this.imageString,
|
||||
this.demos,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String imageString;
|
||||
final List<GalleryDemo> demos;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Material(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
color: colorScheme.surface,
|
||||
child: Semantics(
|
||||
container: true,
|
||||
child: DefaultFocusTraversal(
|
||||
policy: WidgetOrderFocusTraversalPolicy(),
|
||||
child: Column(
|
||||
children: [
|
||||
_DesktopCategoryHeader(
|
||||
title: title,
|
||||
imageString: imageString,
|
||||
),
|
||||
Divider(
|
||||
height: 2,
|
||||
thickness: 2,
|
||||
color: colorScheme.background,
|
||||
),
|
||||
Flexible(
|
||||
// Remove ListView top padding as it is already accounted for.
|
||||
child: MediaQuery.removePadding(
|
||||
removeTop: true,
|
||||
context: context,
|
||||
child: ListView(
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
for (GalleryDemo demo in demos)
|
||||
CategoryDemoItem(
|
||||
demo: demo,
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DesktopCategoryHeader extends StatelessWidget {
|
||||
const _DesktopCategoryHeader({
|
||||
this.title,
|
||||
this.imageString,
|
||||
});
|
||||
final String title;
|
||||
final String imageString;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ColorScheme colorScheme = Theme.of(context).colorScheme;
|
||||
return Material(
|
||||
color: colorScheme.onBackground,
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Image.asset(
|
||||
imageString,
|
||||
width: 64,
|
||||
height: 64,
|
||||
excludeFromSemantics: true,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: EdgeInsetsDirectional.only(start: 8),
|
||||
child: Semantics(
|
||||
header: true,
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.headline.apply(
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
maxLines: 4,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Animates the category item to stagger in. The [_AnimatedCategoryItem.startDelayFraction]
|
||||
/// gives a delay in the unit of a fraction of the whole animation duration,
|
||||
/// which is defined in [_AnimatedHomePageState].
|
||||
class _AnimatedCategoryItem extends StatelessWidget {
|
||||
_AnimatedCategoryItem({
|
||||
Key key,
|
||||
double startDelayFraction,
|
||||
@required this.controller,
|
||||
@required this.child,
|
||||
}) : topPaddingAnimation = Tween(
|
||||
begin: 60.0,
|
||||
end: 0.0,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: Interval(
|
||||
0.000 + startDelayFraction,
|
||||
0.400 + startDelayFraction,
|
||||
curve: Curves.ease,
|
||||
),
|
||||
),
|
||||
),
|
||||
super(key: key);
|
||||
|
||||
final Widget child;
|
||||
final AnimationController controller;
|
||||
final Animation<double> topPaddingAnimation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: controller,
|
||||
builder: (context, child) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: topPaddingAnimation.value),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Animates the carousel to come in from the right.
|
||||
class _AnimatedCarousel extends StatelessWidget {
|
||||
_AnimatedCarousel({
|
||||
Key key,
|
||||
@required this.child,
|
||||
@required this.controller,
|
||||
}) : startPositionAnimation = Tween(
|
||||
begin: 1.0,
|
||||
end: 0.0,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: Interval(
|
||||
0.200,
|
||||
0.800,
|
||||
curve: Curves.ease,
|
||||
),
|
||||
),
|
||||
),
|
||||
super(key: key);
|
||||
|
||||
final Widget child;
|
||||
final AnimationController controller;
|
||||
final Animation<double> startPositionAnimation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
return Stack(
|
||||
children: [
|
||||
SizedBox(height: _carouselHeight(.4, context)),
|
||||
AnimatedBuilder(
|
||||
animation: controller,
|
||||
builder: (context, child) {
|
||||
return PositionedDirectional(
|
||||
start: constraints.maxWidth * startPositionAnimation.value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
height: _carouselHeight(.4, context),
|
||||
width: constraints.maxWidth,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Animates a carousel card to come in from the right.
|
||||
class _AnimatedCarouselCard extends StatelessWidget {
|
||||
_AnimatedCarouselCard({
|
||||
Key key,
|
||||
@required this.child,
|
||||
@required this.controller,
|
||||
}) : startPaddingAnimation = Tween(
|
||||
begin: _horizontalPadding,
|
||||
end: 0.0,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: Interval(
|
||||
0.900,
|
||||
1.000,
|
||||
curve: Curves.ease,
|
||||
),
|
||||
),
|
||||
),
|
||||
super(key: key);
|
||||
|
||||
final Widget child;
|
||||
final AnimationController controller;
|
||||
final Animation<double> startPaddingAnimation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: controller,
|
||||
builder: (context, child) {
|
||||
return Padding(
|
||||
padding: EdgeInsetsDirectional.only(
|
||||
start: startPaddingAnimation.value,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Carousel extends StatefulWidget {
|
||||
const _Carousel({
|
||||
Key key,
|
||||
this.children,
|
||||
this.animationController,
|
||||
}) : super(key: key);
|
||||
|
||||
final List<Widget> children;
|
||||
final AnimationController animationController;
|
||||
|
||||
@override
|
||||
_CarouselState createState() => _CarouselState();
|
||||
}
|
||||
|
||||
class _CarouselState extends State<_Carousel>
|
||||
with SingleTickerProviderStateMixin {
|
||||
PageController _controller;
|
||||
int _currentPage = 0;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (_controller == null) {
|
||||
// The viewPortFraction is calculated as the width of the device minus the
|
||||
// padding.
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final padding = (_horizontalPadding * 2) - (_carouselItemMargin * 2);
|
||||
_controller = PageController(
|
||||
initialPage: _currentPage,
|
||||
viewportFraction: (width - padding) / width,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget builder(int index) {
|
||||
final carouselCard = AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
double value;
|
||||
if (_controller.position.haveDimensions) {
|
||||
value = _controller.page - index;
|
||||
} else {
|
||||
// If haveDimensions is false, use _currentPage to calculate value.
|
||||
value = (_currentPage - index).toDouble();
|
||||
}
|
||||
// We want the peeking cards to be 160 in height and 0.38 helps
|
||||
// achieve that.
|
||||
value = (1 - (value.abs() * .38)).clamp(0, 1).toDouble();
|
||||
value = Curves.easeOut.transform(value);
|
||||
|
||||
return Center(
|
||||
child: Transform(
|
||||
transform: Matrix4.diagonal3Values(1.0, value, 1.0),
|
||||
alignment: Alignment.center,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: widget.children[index],
|
||||
);
|
||||
|
||||
// We only want the second card to be animated.
|
||||
if (index == 1) {
|
||||
return _AnimatedCarouselCard(
|
||||
child: carouselCard,
|
||||
controller: widget.animationController,
|
||||
);
|
||||
} else {
|
||||
return carouselCard;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _AnimatedCarousel(
|
||||
child: PageView.builder(
|
||||
onPageChanged: (value) {
|
||||
setState(() {
|
||||
_currentPage = value;
|
||||
});
|
||||
},
|
||||
controller: _controller,
|
||||
itemCount: widget.children.length,
|
||||
itemBuilder: (context, index) => builder(index),
|
||||
),
|
||||
controller: widget.animationController,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CarouselCard extends StatelessWidget {
|
||||
const _CarouselCard({
|
||||
Key key,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.asset,
|
||||
this.assetDark,
|
||||
this.textColor,
|
||||
this.study,
|
||||
this.navigatorKey,
|
||||
}) : super(key: key);
|
||||
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final String asset;
|
||||
final String assetDark;
|
||||
final Color textColor;
|
||||
final Widget study;
|
||||
final GlobalKey<NavigatorState> navigatorKey;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
// TODO: Update with newer assets when we have them. For now, always use the
|
||||
// dark assets.
|
||||
// Theme.of(context).colorScheme.brightness == Brightness.dark;
|
||||
final isDark = true;
|
||||
final asset = isDark ? assetDark : this.asset;
|
||||
final textColor = isDark ? Colors.white.withOpacity(0.87) : this.textColor;
|
||||
|
||||
return Container(
|
||||
margin:
|
||||
EdgeInsets.all(isDisplayDesktop(context) ? 0 : _carouselItemMargin),
|
||||
child: Material(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
color: Colors.grey,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).push<void>(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _StudyWrapper(
|
||||
study: study,
|
||||
navigatorKey: navigatorKey,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (asset != null)
|
||||
Ink.image(
|
||||
image: AssetImage(asset),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(16, 0, 16, 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: textTheme.caption.apply(color: textColor),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.visible,
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: textTheme.overline.apply(color: textColor),
|
||||
maxLines: 5,
|
||||
overflow: TextOverflow.visible,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
double _carouselHeight(double scaleFactor, BuildContext context) => math.max(
|
||||
_carouselHeightMin *
|
||||
GalleryOptions.of(context).textScaleFactor(context) *
|
||||
scaleFactor,
|
||||
_carouselHeightMin);
|
||||
|
||||
/// Wrap the studies with this to display a back button and allow the user to
|
||||
/// exit them at any time.
|
||||
class _StudyWrapper extends StatefulWidget {
|
||||
const _StudyWrapper({
|
||||
Key key,
|
||||
this.study,
|
||||
this.navigatorKey,
|
||||
}) : super(key: key);
|
||||
|
||||
final Widget study;
|
||||
final GlobalKey<NavigatorState> navigatorKey;
|
||||
|
||||
@override
|
||||
_StudyWrapperState createState() => _StudyWrapperState();
|
||||
}
|
||||
|
||||
class _StudyWrapperState extends State<_StudyWrapper> {
|
||||
FocusNode backButtonFocusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
backButtonFocusNode = FocusNode();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
backButtonFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
return ApplyTextOptions(
|
||||
child: DefaultFocusTraversal(
|
||||
policy: StudyWrapperFocusTraversalPolicy(
|
||||
backButtonFocusNode: backButtonFocusNode,
|
||||
studyNavigatorKey: widget.navigatorKey,
|
||||
),
|
||||
child: InheritedFocusNodes(
|
||||
backButtonFocusNode: backButtonFocusNode,
|
||||
child: Stack(
|
||||
children: [
|
||||
Semantics(
|
||||
sortKey: const OrdinalSortKey(1),
|
||||
child: widget.study,
|
||||
),
|
||||
Align(
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Semantics(
|
||||
sortKey: const OrdinalSortKey(0),
|
||||
label: GalleryLocalizations.of(context).backToGallery,
|
||||
button: true,
|
||||
excludeSemantics: true,
|
||||
child: FloatingActionButton.extended(
|
||||
focusNode: backButtonFocusNode,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
icon: IconTheme(
|
||||
data: IconThemeData(color: colorScheme.onPrimary),
|
||||
child: BackButtonIcon(),
|
||||
),
|
||||
label: Text(
|
||||
MaterialLocalizations.of(context).backButtonTooltip,
|
||||
style: textTheme.button
|
||||
.apply(color: colorScheme.onPrimary),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InheritedFocusNodes extends InheritedWidget {
|
||||
const InheritedFocusNodes({
|
||||
Key key,
|
||||
@required Widget child,
|
||||
@required this.backButtonFocusNode,
|
||||
}) : assert(child != null),
|
||||
super(key: key, child: child);
|
||||
|
||||
final FocusNode backButtonFocusNode;
|
||||
|
||||
static InheritedFocusNodes of(BuildContext context) =>
|
||||
context.dependOnInheritedWidgetOfExactType();
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(InheritedFocusNodes old) => true;
|
||||
}
|
||||
|
||||
class StudyWrapperFocusTraversalPolicy extends WidgetOrderFocusTraversalPolicy {
|
||||
StudyWrapperFocusTraversalPolicy({
|
||||
@required this.backButtonFocusNode,
|
||||
@required this.studyNavigatorKey,
|
||||
});
|
||||
|
||||
final FocusNode backButtonFocusNode;
|
||||
final GlobalKey<NavigatorState> studyNavigatorKey;
|
||||
|
||||
FocusNode _firstFocusNode() {
|
||||
return studyNavigatorKey.currentState.focusScopeNode.traversalDescendants
|
||||
.toList()
|
||||
.first;
|
||||
}
|
||||
|
||||
@override
|
||||
bool previous(FocusNode currentNode) {
|
||||
if (currentNode == backButtonFocusNode) {
|
||||
return super.previous(_firstFocusNode());
|
||||
} else {
|
||||
return super.previous(currentNode);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool next(FocusNode currentNode) {
|
||||
if (currentNode == backButtonFocusNode) {
|
||||
_firstFocusNode().requestFocus();
|
||||
return true;
|
||||
} else {
|
||||
return super.next(currentNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
507
gallery/lib/pages/settings.dart
Normal file
507
gallery/lib/pages/settings.dart
Normal file
@@ -0,0 +1,507 @@
|
||||
// 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:collection';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
|
||||
import 'package:gallery/constants.dart';
|
||||
import 'package:gallery/data/gallery_options.dart';
|
||||
import 'package:gallery/l10n/gallery_localizations.dart';
|
||||
import 'package:gallery/layout/adaptive.dart';
|
||||
import 'package:gallery/pages/about.dart' as about;
|
||||
import 'package:gallery/pages/backdrop.dart';
|
||||
import 'package:gallery/pages/home.dart';
|
||||
import 'package:gallery/pages/settings_list_item.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
enum _ExpandableSetting {
|
||||
textScale,
|
||||
textDirection,
|
||||
locale,
|
||||
platform,
|
||||
theme,
|
||||
}
|
||||
|
||||
class SettingsPage extends StatefulWidget {
|
||||
SettingsPage({
|
||||
Key key,
|
||||
@required this.openSettingsAnimation,
|
||||
@required this.staggerSettingsItemsAnimation,
|
||||
@required this.isSettingsOpenNotifier,
|
||||
}) : super(key: key);
|
||||
|
||||
final Animation<double> openSettingsAnimation;
|
||||
final Animation<double> staggerSettingsItemsAnimation;
|
||||
final ValueNotifier<bool> isSettingsOpenNotifier;
|
||||
|
||||
@override
|
||||
_SettingsPageState createState() => _SettingsPageState();
|
||||
}
|
||||
|
||||
class _SettingsPageState extends State<SettingsPage> {
|
||||
_ExpandableSetting expandedSettingId;
|
||||
Map<String, String> _localeNativeNames;
|
||||
|
||||
void onTapSetting(_ExpandableSetting settingId) {
|
||||
setState(() {
|
||||
if (expandedSettingId == settingId) {
|
||||
expandedSettingId = null;
|
||||
} else {
|
||||
expandedSettingId = settingId;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
LocaleNamesLocalizationsDelegate().allNativeNames().then(
|
||||
(data) => setState(
|
||||
() {
|
||||
_localeNativeNames = data;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// When closing settings, also shrink expanded setting.
|
||||
widget.isSettingsOpenNotifier.addListener(() {
|
||||
if (!widget.isSettingsOpenNotifier.value) {
|
||||
setState(() {
|
||||
expandedSettingId = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Given a [Locale], returns a [DisplayOption] with its native name for a
|
||||
/// title and its name in the currently selected locale for a subtitle. If the
|
||||
/// native name can't be determined, it is omitted. If the locale can't be
|
||||
/// determined, the locale code is used.
|
||||
DisplayOption _getLocaleDisplayOption(BuildContext context, Locale locale) {
|
||||
// TODO: gsw, fil, and es_419 aren't in flutter_localized_countries' dataset
|
||||
final localeCode = locale.toString();
|
||||
final localeName = LocaleNames.of(context).nameOf(localeCode);
|
||||
if (localeName != null) {
|
||||
final localeNativeName =
|
||||
_localeNativeNames != null ? _localeNativeNames[localeCode] : null;
|
||||
return localeNativeName != null
|
||||
? DisplayOption(localeNativeName, subtitle: localeName)
|
||||
: DisplayOption(localeName);
|
||||
} else {
|
||||
switch (localeCode) {
|
||||
case 'gsw':
|
||||
return DisplayOption('Schwiizertüütsch', subtitle: 'Swiss German');
|
||||
case 'fil':
|
||||
return DisplayOption('Filipino', subtitle: 'Filipino');
|
||||
case 'es_419':
|
||||
return DisplayOption(
|
||||
'español (Latinoamérica)',
|
||||
subtitle: 'Spanish (Latin America)',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return DisplayOption(localeCode);
|
||||
}
|
||||
|
||||
/// Create a sorted — by native name – map of supported locales to their
|
||||
/// intended display string, with a system option as the first element.
|
||||
LinkedHashMap<Locale, DisplayOption> _getLocaleOptions() {
|
||||
var localeOptions = LinkedHashMap.of({
|
||||
systemLocaleOption: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsSystemDefault +
|
||||
(deviceLocale != null
|
||||
? ' - ${_getLocaleDisplayOption(context, deviceLocale).title}'
|
||||
: ''),
|
||||
),
|
||||
});
|
||||
var supportedLocales =
|
||||
List<Locale>.from(GalleryLocalizations.supportedLocales);
|
||||
supportedLocales.removeWhere((locale) => locale == deviceLocale);
|
||||
|
||||
final displayLocales = Map<Locale, DisplayOption>.fromIterable(
|
||||
supportedLocales,
|
||||
value: (dynamic locale) =>
|
||||
_getLocaleDisplayOption(context, locale as Locale),
|
||||
).entries.toList()
|
||||
..sort((l1, l2) => compareAsciiUpperCase(l1.value.title, l2.value.title));
|
||||
|
||||
localeOptions.addAll(LinkedHashMap.fromEntries(displayLocales));
|
||||
return localeOptions;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final options = GalleryOptions.of(context);
|
||||
final isDesktop = isDisplayDesktop(context);
|
||||
|
||||
final settingsListItems = [
|
||||
SettingsListItem<double>(
|
||||
title: GalleryLocalizations.of(context).settingsTextScaling,
|
||||
selectedOption: options.textScaleFactor(
|
||||
context,
|
||||
useSentinel: true,
|
||||
),
|
||||
options: LinkedHashMap.of({
|
||||
systemTextScaleFactorOption: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsSystemDefault,
|
||||
),
|
||||
0.8: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsTextScalingSmall,
|
||||
),
|
||||
1.0: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsTextScalingNormal,
|
||||
),
|
||||
2.0: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsTextScalingLarge,
|
||||
),
|
||||
3.0: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsTextScalingHuge,
|
||||
),
|
||||
}),
|
||||
onOptionChanged: (newTextScale) => GalleryOptions.update(
|
||||
context,
|
||||
options.copyWith(textScaleFactor: newTextScale),
|
||||
),
|
||||
onTapSetting: () => onTapSetting(_ExpandableSetting.textScale),
|
||||
isExpanded: expandedSettingId == _ExpandableSetting.textScale,
|
||||
),
|
||||
SettingsListItem<CustomTextDirection>(
|
||||
title: GalleryLocalizations.of(context).settingsTextDirection,
|
||||
selectedOption: options.customTextDirection,
|
||||
options: LinkedHashMap.of({
|
||||
CustomTextDirection.localeBased: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsTextDirectionLocaleBased,
|
||||
),
|
||||
CustomTextDirection.ltr: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsTextDirectionLTR,
|
||||
),
|
||||
CustomTextDirection.rtl: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsTextDirectionRTL,
|
||||
),
|
||||
}),
|
||||
onOptionChanged: (newTextDirection) => GalleryOptions.update(
|
||||
context,
|
||||
options.copyWith(customTextDirection: newTextDirection),
|
||||
),
|
||||
onTapSetting: () => onTapSetting(_ExpandableSetting.textDirection),
|
||||
isExpanded: expandedSettingId == _ExpandableSetting.textDirection,
|
||||
),
|
||||
SettingsListItem<Locale>(
|
||||
title: GalleryLocalizations.of(context).settingsLocale,
|
||||
selectedOption: options.locale == deviceLocale
|
||||
? systemLocaleOption
|
||||
: options.locale,
|
||||
options: _getLocaleOptions(),
|
||||
onOptionChanged: (newLocale) {
|
||||
if (newLocale == systemLocaleOption) {
|
||||
newLocale = deviceLocale;
|
||||
}
|
||||
GalleryOptions.update(
|
||||
context,
|
||||
options.copyWith(locale: newLocale),
|
||||
);
|
||||
},
|
||||
onTapSetting: () => onTapSetting(_ExpandableSetting.locale),
|
||||
isExpanded: expandedSettingId == _ExpandableSetting.locale,
|
||||
),
|
||||
SettingsListItem<TargetPlatform>(
|
||||
title: GalleryLocalizations.of(context).settingsPlatformMechanics,
|
||||
selectedOption: options.platform,
|
||||
options: LinkedHashMap.of({
|
||||
TargetPlatform.android: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsPlatformAndroid,
|
||||
),
|
||||
TargetPlatform.iOS: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsPlatformIOS,
|
||||
),
|
||||
}),
|
||||
onOptionChanged: (newPlatform) => GalleryOptions.update(
|
||||
context,
|
||||
options.copyWith(platform: newPlatform),
|
||||
),
|
||||
onTapSetting: () => onTapSetting(_ExpandableSetting.platform),
|
||||
isExpanded: expandedSettingId == _ExpandableSetting.platform,
|
||||
),
|
||||
SettingsListItem<ThemeMode>(
|
||||
title: GalleryLocalizations.of(context).settingsTheme,
|
||||
selectedOption: options.themeMode,
|
||||
options: LinkedHashMap.of({
|
||||
ThemeMode.system: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsSystemDefault,
|
||||
),
|
||||
ThemeMode.dark: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsDarkTheme,
|
||||
),
|
||||
ThemeMode.light: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsLightTheme,
|
||||
),
|
||||
}),
|
||||
onOptionChanged: (newThemeMode) => GalleryOptions.update(
|
||||
context,
|
||||
options.copyWith(themeMode: newThemeMode),
|
||||
),
|
||||
onTapSetting: () => onTapSetting(_ExpandableSetting.theme),
|
||||
isExpanded: expandedSettingId == _ExpandableSetting.theme,
|
||||
),
|
||||
SlowMotionSetting(),
|
||||
];
|
||||
|
||||
return Material(
|
||||
color: colorScheme.secondaryVariant,
|
||||
child: _AnimatedSettingsPage(
|
||||
animation: widget.openSettingsAnimation,
|
||||
child: Padding(
|
||||
padding: isDesktop
|
||||
? EdgeInsets.zero
|
||||
: EdgeInsets.only(
|
||||
bottom: galleryHeaderHeight,
|
||||
),
|
||||
// Remove ListView top padding as it is already accounted for.
|
||||
child: MediaQuery.removePadding(
|
||||
removeTop: isDesktop,
|
||||
context: context,
|
||||
child: ListView(
|
||||
children: [
|
||||
if (isDesktop) SizedBox(height: firstHeaderDesktopTopPadding),
|
||||
Focus(
|
||||
focusNode: InheritedBackdropFocusNodes.of(context)
|
||||
.frontLayerFocusNode,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 32),
|
||||
child: ExcludeSemantics(
|
||||
child: Header(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
text: GalleryLocalizations.of(context).settingsTitle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isDesktop)
|
||||
...settingsListItems
|
||||
else ...[
|
||||
_AnimateSettingsListItems(
|
||||
animation: widget.staggerSettingsItemsAnimation,
|
||||
children: settingsListItems,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Divider(
|
||||
thickness: 2, height: 0, color: colorScheme.background),
|
||||
SizedBox(height: 12),
|
||||
SettingsAbout(),
|
||||
SettingsFeedback(),
|
||||
SizedBox(height: 12),
|
||||
Divider(
|
||||
thickness: 2, height: 0, color: colorScheme.background),
|
||||
SettingsAttribution(),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsAbout extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _SettingsLink(
|
||||
title: GalleryLocalizations.of(context).settingsAbout,
|
||||
icon: Icons.info_outline,
|
||||
onTap: () {
|
||||
about.showAboutDialog(context: context);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsFeedback extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _SettingsLink(
|
||||
title: GalleryLocalizations.of(context).settingsFeedback,
|
||||
icon: Icons.feedback,
|
||||
onTap: () async {
|
||||
final url = 'https://github.com/flutter/flutter/issues/new/choose/';
|
||||
if (await canLaunch(url)) {
|
||||
await launch(
|
||||
url,
|
||||
forceSafariVC: false,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsAttribution extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDesktop = isDisplayDesktop(context);
|
||||
final verticalPadding = isDesktop ? 0.0 : 28.0;
|
||||
return MergeSemantics(
|
||||
child: Padding(
|
||||
padding: EdgeInsetsDirectional.only(
|
||||
start: isDesktop ? 24 : 32,
|
||||
end: isDesktop ? 0 : 32,
|
||||
top: verticalPadding,
|
||||
bottom: verticalPadding,
|
||||
),
|
||||
child: Text(
|
||||
GalleryLocalizations.of(context).settingsAttribution,
|
||||
style: Theme.of(context).textTheme.body2.copyWith(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSecondary,
|
||||
),
|
||||
textAlign: isDesktop ? TextAlign.end : TextAlign.start,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SettingsLink extends StatelessWidget {
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final GestureTapCallback onTap;
|
||||
|
||||
_SettingsLink({this.title, this.icon, this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final isDesktop = isDisplayDesktop(context);
|
||||
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: isDesktop ? 24 : 32,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: colorScheme.onSecondary.withOpacity(0.5),
|
||||
size: 24,
|
||||
),
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.only(
|
||||
start: 16,
|
||||
top: 12,
|
||||
bottom: 12,
|
||||
),
|
||||
child: Text(
|
||||
title,
|
||||
style: textTheme.subtitle.apply(
|
||||
color: colorScheme.onSecondary,
|
||||
),
|
||||
textAlign: isDesktop ? TextAlign.end : TextAlign.start,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Animate the settings page to slide in from above.
|
||||
class _AnimatedSettingsPage extends StatelessWidget {
|
||||
const _AnimatedSettingsPage({
|
||||
Key key,
|
||||
@required this.animation,
|
||||
@required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
final Widget child;
|
||||
final Animation<double> animation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDesktop = isDisplayDesktop(context);
|
||||
|
||||
if (isDesktop) {
|
||||
return child;
|
||||
} else {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
return Stack(
|
||||
children: [
|
||||
PositionedTransition(
|
||||
rect: RelativeRectTween(
|
||||
begin: RelativeRect.fromLTRB(0, -constraints.maxHeight, 0, 0),
|
||||
end: RelativeRect.fromLTRB(0, 0, 0, 0),
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.linear,
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Animate the settings list items to stagger in from above.
|
||||
class _AnimateSettingsListItems extends StatelessWidget {
|
||||
const _AnimateSettingsListItems({
|
||||
Key key,
|
||||
this.animation,
|
||||
this.children,
|
||||
this.topPadding,
|
||||
this.bottomPadding,
|
||||
}) : super(key: key);
|
||||
|
||||
final Animation<double> animation;
|
||||
final List<Widget> children;
|
||||
final double topPadding;
|
||||
final double bottomPadding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final startDividingPadding = 4.0;
|
||||
final topPaddingTween = Tween<double>(
|
||||
begin: 0,
|
||||
end: children.length * startDividingPadding,
|
||||
);
|
||||
final dividerTween = Tween<double>(
|
||||
begin: startDividingPadding,
|
||||
end: 0,
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: topPaddingTween.animate(animation).value),
|
||||
child: Column(
|
||||
children: [
|
||||
for (Widget child in children)
|
||||
AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: dividerTween.animate(animation).value,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
334
gallery/lib/pages/settings_list_item.dart
Normal file
334
gallery/lib/pages/settings_list_item.dart
Normal file
@@ -0,0 +1,334 @@
|
||||
// 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:collection';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gallery/data/gallery_options.dart';
|
||||
import 'package:gallery/l10n/gallery_localizations.dart';
|
||||
|
||||
// Common constants between SlowMotionSetting and SettingsListItem.
|
||||
final settingItemBorderRadius = BorderRadius.circular(10);
|
||||
const settingItemHeaderMargin = EdgeInsetsDirectional.fromSTEB(32, 0, 32, 8);
|
||||
|
||||
class DisplayOption {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
|
||||
DisplayOption(this.title, {this.subtitle});
|
||||
}
|
||||
|
||||
class SlowMotionSetting extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final options = GalleryOptions.of(context);
|
||||
|
||||
return Semantics(
|
||||
container: true,
|
||||
child: Container(
|
||||
margin: settingItemHeaderMargin,
|
||||
child: Material(
|
||||
shape: RoundedRectangleBorder(borderRadius: settingItemBorderRadius),
|
||||
color: colorScheme.secondary,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
GalleryLocalizations.of(context).settingsSlowMotion,
|
||||
style: textTheme.subhead.apply(
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 8),
|
||||
child: Switch(
|
||||
activeColor: colorScheme.primary,
|
||||
value: options.timeDilation != 1.0,
|
||||
onChanged: (isOn) => GalleryOptions.update(
|
||||
context,
|
||||
options.copyWith(timeDilation: isOn ? 5.0 : 1.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsListItem<T> extends StatefulWidget {
|
||||
SettingsListItem({
|
||||
Key key,
|
||||
@required this.title,
|
||||
@required this.options,
|
||||
@required this.selectedOption,
|
||||
@required this.onOptionChanged,
|
||||
@required this.onTapSetting,
|
||||
@required this.isExpanded,
|
||||
}) : super(key: key);
|
||||
|
||||
final String title;
|
||||
final LinkedHashMap<T, DisplayOption> options;
|
||||
final T selectedOption;
|
||||
final ValueChanged<T> onOptionChanged;
|
||||
final Function onTapSetting;
|
||||
final bool isExpanded;
|
||||
|
||||
@override
|
||||
_SettingsListItemState createState() => _SettingsListItemState<T>();
|
||||
}
|
||||
|
||||
class _SettingsListItemState<T> extends State<SettingsListItem<T>>
|
||||
with SingleTickerProviderStateMixin {
|
||||
static final Animatable<double> _easeInTween =
|
||||
CurveTween(curve: Curves.easeIn);
|
||||
static const _expandDuration = Duration(milliseconds: 150);
|
||||
AnimationController _controller;
|
||||
Animation<double> _childrenHeightFactor;
|
||||
Animation<double> _headerChevronRotation;
|
||||
Animation<double> _headerSubtitleHeight;
|
||||
Animation<EdgeInsetsGeometry> _headerMargin;
|
||||
Animation<EdgeInsetsGeometry> _headerPadding;
|
||||
Animation<EdgeInsetsGeometry> _childrenPadding;
|
||||
Animation<BorderRadius> _headerBorderRadius;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(duration: _expandDuration, vsync: this);
|
||||
_childrenHeightFactor = _controller.drive(_easeInTween);
|
||||
_headerChevronRotation =
|
||||
Tween<double>(begin: 0, end: 0.5).animate(_controller);
|
||||
_headerMargin = EdgeInsetsGeometryTween(
|
||||
begin: settingItemHeaderMargin,
|
||||
end: EdgeInsets.zero,
|
||||
).animate(_controller);
|
||||
_headerPadding = EdgeInsetsGeometryTween(
|
||||
begin: EdgeInsetsDirectional.fromSTEB(16, 10, 0, 10),
|
||||
end: EdgeInsetsDirectional.fromSTEB(32, 18, 32, 20),
|
||||
).animate(_controller);
|
||||
_headerSubtitleHeight =
|
||||
_controller.drive(Tween<double>(begin: 1.0, end: 0.0));
|
||||
_childrenPadding = EdgeInsetsGeometryTween(
|
||||
begin: EdgeInsets.symmetric(horizontal: 32),
|
||||
end: EdgeInsets.zero,
|
||||
).animate(_controller);
|
||||
_headerBorderRadius = BorderRadiusTween(
|
||||
begin: settingItemBorderRadius,
|
||||
end: BorderRadius.zero,
|
||||
).animate(_controller);
|
||||
|
||||
if (widget.isExpanded) {
|
||||
_controller.value = 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleExpansion() {
|
||||
if (widget.isExpanded) {
|
||||
_controller.forward();
|
||||
} else {
|
||||
_controller.reverse().then<void>((value) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildHeaderWithChildren(BuildContext context, Widget child) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_CategoryHeader(
|
||||
margin: _headerMargin.value,
|
||||
padding: _headerPadding.value,
|
||||
borderRadius: _headerBorderRadius.value,
|
||||
subtitleHeight: _headerSubtitleHeight,
|
||||
chevronRotation: _headerChevronRotation,
|
||||
title: widget.title,
|
||||
subtitle: widget.options[widget.selectedOption].title ?? '',
|
||||
onTap: () => widget.onTapSetting(),
|
||||
),
|
||||
Padding(
|
||||
padding: _childrenPadding.value,
|
||||
child: ClipRect(
|
||||
child: Align(
|
||||
heightFactor: _childrenHeightFactor.value,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_handleExpansion();
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final optionWidgetsList = <Widget>[];
|
||||
|
||||
widget.options.forEach(
|
||||
(optionValue, optionDisplay) => optionWidgetsList.add(
|
||||
RadioListTile<T>(
|
||||
value: optionValue,
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
optionDisplay.title,
|
||||
style: theme.textTheme.body2.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
if (optionDisplay.subtitle != null)
|
||||
Text(
|
||||
optionDisplay.subtitle,
|
||||
style: theme.textTheme.body2.copyWith(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimary
|
||||
.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
groupValue: widget.selectedOption,
|
||||
onChanged: (newOption) => widget.onOptionChanged(newOption),
|
||||
activeColor: Theme.of(context).colorScheme.primary,
|
||||
dense: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: _controller.view,
|
||||
builder: _buildHeaderWithChildren,
|
||||
child: Container(
|
||||
margin: const EdgeInsetsDirectional.only(start: 24, bottom: 40),
|
||||
decoration: BoxDecoration(
|
||||
border: BorderDirectional(
|
||||
start: BorderSide(
|
||||
width: 2,
|
||||
color: theme.colorScheme.background,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) => optionWidgetsList[index],
|
||||
itemCount: optionWidgetsList.length,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CategoryHeader extends StatelessWidget {
|
||||
const _CategoryHeader({
|
||||
Key key,
|
||||
this.margin,
|
||||
this.padding,
|
||||
this.borderRadius,
|
||||
this.subtitleHeight,
|
||||
this.chevronRotation,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
final EdgeInsetsGeometry margin;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final BorderRadiusGeometry borderRadius;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final Animation<double> subtitleHeight;
|
||||
final Animation<double> chevronRotation;
|
||||
final GestureTapCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
return Container(
|
||||
margin: margin,
|
||||
child: Material(
|
||||
shape: RoundedRectangleBorder(borderRadius: borderRadius),
|
||||
color: colorScheme.secondary,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: textTheme.subhead.apply(
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
SizeTransition(
|
||||
sizeFactor: subtitleHeight,
|
||||
child: Text(
|
||||
subtitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.overline.apply(
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(
|
||||
start: 8,
|
||||
end: 24,
|
||||
),
|
||||
child: RotationTransition(
|
||||
turns: chevronRotation,
|
||||
child: Icon(Icons.arrow_drop_down),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
238
gallery/lib/pages/splash.dart
Normal file
238
gallery/lib/pages/splash.dart
Normal file
@@ -0,0 +1,238 @@
|
||||
// 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:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gallery/constants.dart';
|
||||
import 'package:gallery/layout/adaptive.dart';
|
||||
import 'package:gallery/pages/home.dart';
|
||||
|
||||
const homePeekDesktop = 210.0;
|
||||
const homePeekMobile = 60.0;
|
||||
|
||||
class SplashPageAnimation extends InheritedWidget {
|
||||
const SplashPageAnimation({
|
||||
Key key,
|
||||
@required this.isFinished,
|
||||
@required Widget child,
|
||||
}) : assert(child != null),
|
||||
super(key: key, child: child);
|
||||
|
||||
final bool isFinished;
|
||||
|
||||
static SplashPageAnimation of(BuildContext context) {
|
||||
return context.dependOnInheritedWidgetOfExactType();
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(SplashPageAnimation old) => true;
|
||||
}
|
||||
|
||||
class SplashPage extends StatefulWidget {
|
||||
const SplashPage({
|
||||
Key key,
|
||||
this.isAnimated = true,
|
||||
@required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
final bool isAnimated;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
_SplashPageState createState() => _SplashPageState();
|
||||
}
|
||||
|
||||
class _SplashPageState extends State<SplashPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
AnimationController _controller;
|
||||
Timer _launchTimer;
|
||||
int _effect;
|
||||
final _random = Random();
|
||||
|
||||
// A map of the effect index to its duration. This duration is used to
|
||||
// determine how long to display the splash animation at launch.
|
||||
//
|
||||
// If a new effect is added, this map should be updated.
|
||||
final _effectDurations = {
|
||||
1: 5,
|
||||
2: 4,
|
||||
3: 4,
|
||||
4: 5,
|
||||
5: 5,
|
||||
6: 4,
|
||||
7: 4,
|
||||
8: 4,
|
||||
9: 3,
|
||||
10: 6,
|
||||
};
|
||||
|
||||
bool get _isSplashVisible {
|
||||
return _controller.status == AnimationStatus.completed ||
|
||||
_controller.status == AnimationStatus.forward;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// If the number of included effects changes, this number should be changed.
|
||||
_effect = _random.nextInt(_effectDurations.length) + 1;
|
||||
|
||||
_controller = AnimationController(
|
||||
duration: Duration(
|
||||
milliseconds: splashPageAnimationDurationInMilliseconds,
|
||||
),
|
||||
value: 1,
|
||||
vsync: this)
|
||||
..addListener(() {
|
||||
this.setState(() {});
|
||||
});
|
||||
if (widget.isAnimated) {
|
||||
_launchTimer = Timer(
|
||||
Duration(seconds: _effectDurations[_effect]),
|
||||
() {
|
||||
_controller.fling(velocity: -1);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
_controller.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_launchTimer?.cancel();
|
||||
_launchTimer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Animation<RelativeRect> _getPanelAnimation(
|
||||
BuildContext context,
|
||||
BoxConstraints constraints,
|
||||
) {
|
||||
final double height = constraints.biggest.height -
|
||||
(isDisplayDesktop(context) ? homePeekDesktop : homePeekMobile);
|
||||
return RelativeRectTween(
|
||||
begin: const RelativeRect.fromLTRB(0, 0, 0, 0),
|
||||
end: RelativeRect.fromLTRB(0, height, 0, 0),
|
||||
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return NotificationListener<ToggleSplashNotification>(
|
||||
onNotification: (_) {
|
||||
_controller.forward();
|
||||
return true;
|
||||
},
|
||||
child: SplashPageAnimation(
|
||||
isFinished: _controller.status == AnimationStatus.dismissed,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final Animation<RelativeRect> animation =
|
||||
_getPanelAnimation(context, constraints);
|
||||
Widget frontLayer = widget.child;
|
||||
if (_isSplashVisible) {
|
||||
frontLayer = GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
_controller.reverse();
|
||||
},
|
||||
onVerticalDragEnd: (details) {
|
||||
if (details.velocity.pixelsPerSecond.dy < -200) {
|
||||
_controller.reverse();
|
||||
}
|
||||
},
|
||||
child: IgnorePointer(child: frontLayer),
|
||||
);
|
||||
}
|
||||
|
||||
if (isDisplayDesktop(context)) {
|
||||
frontLayer = Padding(
|
||||
padding: const EdgeInsets.only(top: 136),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(40),
|
||||
),
|
||||
child: frontLayer,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
_SplashBackLayer(
|
||||
isSplashCollapsed: !_isSplashVisible,
|
||||
effect: _effect,
|
||||
onTap: () {
|
||||
_controller.forward();
|
||||
},
|
||||
),
|
||||
PositionedTransition(
|
||||
rect: animation,
|
||||
child: frontLayer,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SplashBackLayer extends StatelessWidget {
|
||||
_SplashBackLayer({
|
||||
Key key,
|
||||
@required this.isSplashCollapsed,
|
||||
this.effect,
|
||||
this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
final bool isSplashCollapsed;
|
||||
final int effect;
|
||||
final GestureTapCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var effectAsset = 'assets/splash_effects/splash_effect_$effect.gif';
|
||||
final flutterLogo = Image.asset('assets/logo/flutter_logo.png');
|
||||
|
||||
Widget child;
|
||||
if (isSplashCollapsed) {
|
||||
child = isDisplayDesktop(context)
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 50),
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: flutterLogo,
|
||||
),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
} else {
|
||||
child = Stack(
|
||||
children: [
|
||||
Center(child: Image.asset(effectAsset)),
|
||||
Center(child: flutterLogo),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return ExcludeSemantics(
|
||||
child: Container(
|
||||
color: Color(0xFF030303), // This is the background color of the gifs.
|
||||
padding: EdgeInsets.only(
|
||||
bottom: isDisplayDesktop(context) ? homePeekDesktop : homePeekMobile,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user