| // Copyright 2017 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:math' as math; |
| |
| import 'package:lib.app.dart/app.dart'; |
| import 'package:lib.mediaplayer.flutter/media_player_controller.dart'; |
| import 'package:lib.ui.flutter/child_view.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:lib.widgets/model.dart'; |
| import 'package:lib.widgets/widgets.dart'; |
| |
| final Uri _kVideoUri = Uri.parse( |
| 'https://storage.googleapis.com/' |
| 'fuchsia/assets/video/656a7250025525ae5a44b43d23c51e38b466d146', |
| ); |
| |
| const double _kElevationStep = 2.0; |
| const double _kAlbumMakerWidth = 672.0; |
| const double _kAlbumMakerImageWidth = 492.0; |
| const double _kAlbumMakerImageMargin = 32.0; |
| const double _kPhotoListWidth = 428.0; |
| const double _kPhotoListHeight = 548.0; |
| const double _kPhotoListPhotoMargin = 4.0; |
| const double _kPhotoListTitleHeight = 96.0; |
| const double _kPhotoListHorizontalMargin = 8.0; |
| const double _kAutoMagicHorizontalOverlap = 56.0; |
| const double _kAutoMagicBottomOffset = 48.0; |
| const double _kAutoMagicSize = 80.0; |
| const double _kAutoMagicIconSize = 40.0; |
| const double _kSearchBoxTopOffset = 40.0; |
| const double _kSearchBoxHeight = 56.0; |
| const double _kSearchBoxWidth = 328.0; |
| const double _kSunBottomOffset = -60.0; |
| const double _kPhotoListCheckInset = 16.0; |
| const double _kPhotoListCheckSize = 24.0; |
| const double _kPhotoListVideoIconSize = 40.0; |
| const double _kPhotoListQuadPictureHeight = 272.0; |
| const double _kPhotoListTitleLeftPadding = 24.0; |
| const double _kPhotoListTitleRightPadding = 48.0; |
| const double _kPhotoListTitleFontSize = 24.0; |
| const double _kPhotoListDescriptionFontSize = 14.0; |
| const double _kSearchBoxIconSize = 24.0; |
| const double _kSearchBoxTextSize = 14.0; |
| const double _kSearchBoxHorizontalMargin = 16.0; |
| const double _kSunRayWidth = 26.0; |
| const double _kSunRayHeight = 17.0; |
| const double _kSunCenterDiameter = 102.0; |
| const double _kSunCenterMargin = 18.0; |
| const double _kSunDiameter = |
| _kSunCenterDiameter + 2.0 * _kSunRayWidth + 2.0 * _kSunCenterMargin; |
| const double _kSunRayOffset = |
| _kSunCenterDiameter / 2.0 + _kSunRayWidth / 2.0 + _kSunCenterMargin; |
| const double _kSqrt2 = 0.707; |
| const double _kVideoPlayerWidth = 840.0; |
| const double _kVideoPlayerHeight = 580.0; |
| const double _kVideoPlayerProgressBarHeight = 3.0; |
| const double _kVideoPlayerTextSize = 14.0; |
| const double _kVideoPlayerIconSize = 48.0; |
| const double _kVideoPlayerTextHorizontalMargin = 24.0; |
| const double _kVideoPlayerTextTopMargin = 24.0; |
| const double _kVideoPlayerButtonHorizontalPadding = 32.0; |
| const double _kVideoPlayerButtonHeight = 92.0; |
| const double _kVideoPlayerButtonWidth = |
| _kVideoPlayerIconSize + 2 * _kVideoPlayerButtonHorizontalPadding; |
| |
| const double _kAlbumMakerElevation = 2 * _kElevationStep; |
| const double _kPhotoListElevation = 4 * _kElevationStep; |
| const double _kAutoMagicElevation = 16 * _kElevationStep; |
| const double _kVideoPlayerElevation = 18 * _kElevationStep; |
| |
| // Relative to _kAlbumMakerElevation |
| const double _kSunRelativeElevation = 5 * _kElevationStep; |
| |
| // Relative to _kPhotoListElevation |
| const double _kPhotoListTitleRelativeElevation = 4 * _kElevationStep; |
| |
| // Relative to _kAlbumMakerElevation |
| const double _kSearchBoxRelativeElevation = 3 * _kElevationStep; |
| |
| final BorderRadius _kAlbumMakerBorderRadius = new BorderRadius.circular(16.0); |
| final BorderRadius _kPhotoListBorderRadius = new BorderRadius.circular(16.0); |
| final BorderRadius _kSearchBoxBorderRadius = new BorderRadius.circular(8.0); |
| const BorderRadius _kVideoPlayerBorderRadius = const BorderRadius.only( |
| bottomLeft: const Radius.circular(16.0), |
| bottomRight: const Radius.circular(16.0), |
| ); |
| |
| const Color _kAutoMagicButtonBackgroundColor = const Color(0xFF4A78C0); |
| final Color _kAlbumMakerBackgroundColor = Colors.grey[50]; |
| final Color _kPhotoListBackgroundColor = Colors.grey[50]; |
| final Color _kPhotoListCheckBackgroundColor = Colors.grey[900]; |
| final Color _kPhotoListCheckIconColor = Colors.grey[100]; |
| final Color _kPhotoListTitleColor = Colors.grey[900]; |
| final Color _kSearchBoxBackgroundColor = Colors.grey[50]; |
| final Color _kSearchBoxTextColor = Colors.grey[600]; |
| const Color _kSunColor = Colors.yellow; |
| |
| const Duration _kVideoSkipAmount = const Duration(seconds: 10); |
| const Duration _kVideoProgressTimeout = const Duration(milliseconds: 250); |
| |
| final StartupContext _startupContext = new StartupContext.fromStartupInfo(); |
| |
| Future<Null> main() async { |
| MediaPlayerController controller = new MediaPlayerController( |
| _startupContext.environmentServices, |
| )..open(_kVideoUri); |
| |
| _VideoModel videoModel = new _VideoModel(controller: controller); |
| |
| runApp( |
| new LayoutBuilder( |
| builder: (BuildContext context, BoxConstraints constraints) => constraints |
| .biggest.width == |
| 0.0 || |
| constraints.biggest.height == 0.0 |
| ? const Offstage() |
| : new Directionality( |
| textDirection: TextDirection.ltr, |
| child: new WindowMediaQuery( |
| child: new GestureDetector( |
| behavior: HitTestBehavior.opaque, |
| onTap: () => videoModel.hideVideo(), |
| child: new Stack( |
| fit: StackFit.expand, |
| children: <Widget>[ |
| // Album Maker. |
| new Positioned( |
| left: 0.0, |
| top: 0.0, |
| bottom: 0.0, |
| width: _kAlbumMakerWidth, |
| child: new _AlbumMaker(), |
| ), |
| |
| // Photo list. |
| new Positioned( |
| right: 0.0, |
| bottom: 0.0, |
| width: _kPhotoListWidth, |
| height: _kPhotoListHeight, |
| child: new _PhotoList( |
| onVideoTapped: () => videoModel.toggleVideo(), |
| ), |
| ), |
| |
| // Auto Magic Button. |
| new Positioned( |
| right: _kPhotoListWidth - _kAutoMagicHorizontalOverlap, |
| bottom: _kAutoMagicBottomOffset, |
| width: _kAutoMagicSize, |
| height: _kAutoMagicSize, |
| child: new _AutoMagicButton(), |
| ), |
| |
| // Video Player. |
| new Center( |
| child: new SizedBox( |
| width: _kVideoPlayerWidth, |
| height: _kVideoPlayerHeight, |
| child: new ScopedModel<_VideoModel>( |
| model: videoModel, |
| child: new LayoutBuilder( |
| builder: (_, BoxConstraints constraints) => |
| (constraints.maxWidth == 0.0 || |
| constraints.maxHeight == 0.0) |
| ? const Offstage() |
| : new ScopedModelDescendant<_VideoModel>( |
| builder: |
| (_, __, _VideoModel videoModel) => |
| new _VideoPlayer( |
| videoModel: videoModel), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| class _VideoModel extends Model { |
| final MediaPlayerController controller; |
| Duration duration = Duration.zero; |
| Duration progress = Duration.zero; |
| bool playing = false; |
| bool showing = false; |
| Timer _progressTimer; |
| |
| _VideoModel({this.controller}) { |
| controller.addListener(() { |
| if (duration != controller.duration) { |
| duration = controller.duration; |
| notifyListeners(); |
| } |
| if (progress != controller.progress) { |
| progress = controller.progress; |
| notifyListeners(); |
| } |
| if (playing != controller.playing) { |
| playing = controller.playing; |
| notifyListeners(); |
| } |
| }); |
| } |
| |
| void toggleVideo() { |
| showing = !showing; |
| if (!showing) { |
| _resetPlayingState(); |
| } else { |
| controller.play(); |
| playing = true; |
| } |
| notifyListeners(); |
| } |
| |
| void hideVideo() { |
| if (showing != false) { |
| showing = false; |
| _resetPlayingState(); |
| notifyListeners(); |
| } |
| } |
| |
| void _resetPlayingState() { |
| controller |
| ..pause() |
| ..seek(Duration.zero); |
| progress = Duration.zero; |
| playing = false; |
| } |
| |
| void togglePlayPause() { |
| if (playing) { |
| controller.pause(); |
| playing = false; |
| } else { |
| controller.play(); |
| playing = true; |
| } |
| notifyListeners(); |
| } |
| |
| void skipBack() { |
| Duration targetSeek = progress - _kVideoSkipAmount; |
| if (targetSeek < Duration.zero) { |
| targetSeek = Duration.zero; |
| } |
| if (progress != targetSeek) { |
| controller.seek(targetSeek); |
| progress = targetSeek; |
| notifyListeners(); |
| } |
| } |
| |
| void skipForward() { |
| Duration targetSeek = progress + _kVideoSkipAmount; |
| if (targetSeek > duration) { |
| targetSeek = duration; |
| } |
| if (progress != targetSeek) { |
| controller.seek(targetSeek); |
| progress = targetSeek; |
| notifyListeners(); |
| } |
| } |
| |
| @override |
| void notifyListeners() { |
| super.notifyListeners(); |
| if (playing && _progressTimer == null) { |
| _progressTimer = new Timer.periodic( |
| _kVideoProgressTimeout, |
| (_) { |
| if (progress != controller.progress) { |
| progress = controller.progress; |
| notifyListeners(); |
| } |
| }, |
| ); |
| } else if (!playing) { |
| _progressTimer?.cancel(); |
| _progressTimer = null; |
| } |
| } |
| } |
| |
| class _VideoPlayer extends StatelessWidget { |
| final _VideoModel videoModel; |
| |
| const _VideoPlayer({this.videoModel}); |
| |
| @override |
| Widget build(BuildContext context) => new Offstage( |
| offstage: !videoModel.showing, |
| child: new PhysicalModel( |
| color: Colors.black, |
| elevation: _kVideoPlayerElevation, |
| borderRadius: _kVideoPlayerBorderRadius, |
| child: new GestureDetector( |
| behavior: HitTestBehavior.opaque, |
| onTap: () { |
| // Do nothing |
| }, |
| child: new Column( |
| children: <Widget>[ |
| new Expanded( |
| child: new _Video(videoModel: videoModel), |
| ), |
| new Container( |
| height: _kVideoPlayerButtonHeight, |
| child: new Stack( |
| children: <Widget>[ |
| new Positioned.fill( |
| child: new _VideoControls( |
| videoModel: videoModel, |
| ), |
| ), |
| new Positioned( |
| top: 0.0, |
| left: 0.0, |
| right: 0.0, |
| height: _kVideoPlayerProgressBarHeight, |
| child: new _VideoProgress( |
| videoModel: videoModel, |
| ), |
| ), |
| ], |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| class _Video extends StatelessWidget { |
| final _VideoModel videoModel; |
| |
| const _Video({this.videoModel}); |
| |
| @override |
| Widget build(BuildContext context) => new GestureDetector( |
| behavior: HitTestBehavior.opaque, |
| onTap: () { |
| if (videoModel.controller.videoViewConnection != null) { |
| videoModel.togglePlayPause(); |
| } |
| }, |
| child: videoModel.controller.videoViewConnection == null |
| ? const Center(child: const FuchsiaSpinner()) |
| : new ChildView( |
| connection: videoModel.controller.videoViewConnection, |
| ), |
| ); |
| } |
| |
| class _VideoControls extends StatelessWidget { |
| final _VideoModel videoModel; |
| |
| const _VideoControls({this.videoModel}); |
| |
| @override |
| Widget build(BuildContext context) => new Container( |
| margin: const EdgeInsets.symmetric( |
| horizontal: _kVideoPlayerTextHorizontalMargin, |
| ), |
| child: new Row( |
| children: <Widget>[ |
| new Expanded( |
| child: new Align( |
| alignment: FractionalOffset.topLeft, |
| child: new _VideoTime(time: videoModel.progress), |
| ), |
| ), |
| new _VideoControlButton( |
| onTap: videoModel.skipBack, |
| icon: Icons.fast_rewind, |
| ), |
| new _VideoControlButton( |
| onTap: videoModel.togglePlayPause, |
| icon: videoModel.playing ? Icons.pause : Icons.play_arrow, |
| ), |
| new _VideoControlButton( |
| onTap: videoModel.skipForward, |
| icon: Icons.fast_forward, |
| ), |
| new Expanded( |
| child: new Align( |
| alignment: FractionalOffset.topRight, |
| child: new _VideoTime(time: videoModel.duration), |
| ), |
| ), |
| ], |
| ), |
| ); |
| } |
| |
| class _VideoTime extends StatelessWidget { |
| final Duration time; |
| |
| const _VideoTime({this.time}); |
| |
| @override |
| Widget build(BuildContext context) => new Container( |
| margin: const EdgeInsets.only( |
| top: _kVideoPlayerTextTopMargin, |
| ), |
| child: new Text( |
| _toTimeString(time), |
| style: const TextStyle( |
| fontSize: _kVideoPlayerTextSize, |
| color: Colors.white, |
| fontFamily: 'RobotoRegular', |
| ), |
| ), |
| ); |
| |
| String _toTimeString(Duration duration) { |
| String secondsString = duration.inSeconds % 60 < 10 |
| ? '0${duration.inSeconds % 60}' |
| : '${duration.inSeconds % 60}'; |
| return '${duration.inMinutes}:$secondsString'; |
| } |
| } |
| |
| class _VideoControlButton extends StatelessWidget { |
| final VoidCallback onTap; |
| final IconData icon; |
| |
| const _VideoControlButton({this.onTap, this.icon}); |
| |
| @override |
| Widget build(BuildContext context) => new GestureDetector( |
| behavior: HitTestBehavior.opaque, |
| onTap: onTap, |
| child: new Container( |
| width: _kVideoPlayerButtonWidth, |
| child: new Center( |
| child: new Icon( |
| icon, |
| size: _kVideoPlayerIconSize, |
| color: Colors.white, |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| class _VideoProgress extends StatelessWidget { |
| final _VideoModel videoModel; |
| |
| const _VideoProgress({this.videoModel}); |
| |
| @override |
| Widget build(BuildContext context) => new Align( |
| alignment: FractionalOffset.centerLeft, |
| child: new FractionallySizedBox( |
| heightFactor: 1.0, |
| widthFactor: videoModel.duration == Duration.zero |
| ? 0.0 |
| : videoModel.progress.inMilliseconds > |
| videoModel.duration.inMilliseconds |
| ? 1.0 |
| : videoModel.progress.inMilliseconds / |
| videoModel.duration.inMilliseconds, |
| child: new Container( |
| color: Colors.white, |
| ), |
| ), |
| ); |
| } |
| |
| class _AutoMagicButton extends StatelessWidget { |
| @override |
| Widget build(BuildContext context) => const PhysicalModel( |
| shape: BoxShape.circle, |
| color: _kAutoMagicButtonBackgroundColor, |
| elevation: _kAutoMagicElevation, |
| child: const Center( |
| child: const Icon( |
| Icons.create, |
| color: Colors.white, |
| size: _kAutoMagicIconSize, |
| ), |
| ), |
| ); |
| } |
| |
| class _AlbumMaker extends StatelessWidget { |
| @override |
| Widget build(BuildContext context) => new PhysicalModel( |
| borderRadius: _kAlbumMakerBorderRadius, |
| color: _kAlbumMakerBackgroundColor, |
| elevation: _kAlbumMakerElevation, |
| child: new Stack( |
| fit: StackFit.expand, |
| children: <Widget>[ |
| // Search box. |
| new Positioned.fill( |
| top: _kSearchBoxTopOffset, |
| child: new Align( |
| alignment: FractionalOffset.topCenter, |
| child: new _SearchBox(), |
| ), |
| ), |
| |
| // Images. |
| new Positioned.fill( |
| top: _kSearchBoxTopOffset + _kSearchBoxHeight / 2.0, |
| child: new Align( |
| alignment: FractionalOffset.topCenter, |
| child: new Container( |
| width: _kAlbumMakerImageWidth, |
| child: new Column(children: <Widget>[ |
| new PhysicalModel( |
| color: _kAlbumMakerBackgroundColor, |
| elevation: _kElevationStep, |
| child: new Image.asset( |
| 'packages/perspective/res/module-a-photos/' |
| '1-sea-withtext.png', |
| ), |
| ), |
| new Container(height: _kAlbumMakerImageMargin), |
| new PhysicalModel( |
| color: _kAlbumMakerBackgroundColor, |
| elevation: _kElevationStep, |
| child: new Image.asset( |
| 'packages/perspective/res/module-a-photos/' |
| '2-pano-withtext.png', |
| ), |
| ), |
| ]), |
| ), |
| ), |
| ), |
| |
| // The sun. |
| const Positioned.fill( |
| bottom: _kSunBottomOffset, |
| child: const Align( |
| alignment: FractionalOffset.bottomCenter, |
| child: const _Sun(elevation: _kSunRelativeElevation), |
| ), |
| ), |
| ], |
| ), |
| ); |
| } |
| |
| class _PhotoList extends StatelessWidget { |
| final VoidCallback onVideoTapped; |
| |
| const _PhotoList({this.onVideoTapped}); |
| |
| @override |
| Widget build(BuildContext context) => new PhysicalModel( |
| borderRadius: _kPhotoListBorderRadius, |
| color: _kPhotoListBackgroundColor, |
| elevation: _kPhotoListElevation, |
| child: new Stack( |
| children: <Widget>[ |
| new Positioned.fill( |
| left: _kPhotoListHorizontalMargin, |
| right: _kPhotoListHorizontalMargin, |
| child: new ListView.builder( |
| physics: const BouncingScrollPhysics(), |
| padding: const EdgeInsets.only( |
| top: _kPhotoListTitleHeight + _kPhotoListPhotoMargin, |
| bottom: _kPhotoListPhotoMargin, |
| ), |
| itemCount: 8, |
| itemBuilder: (BuildContext context, int index) => new Container( |
| margin: const EdgeInsets.symmetric( |
| vertical: _kPhotoListPhotoMargin / 2.0, |
| ), |
| child: _photoBuilder(index), |
| ), |
| ), |
| ), |
| new Positioned( |
| top: 0.0, |
| left: 0.0, |
| right: 0.0, |
| height: _kPhotoListTitleHeight, |
| child: new _PhotoListHeader(), |
| ), |
| ], |
| ), |
| ); |
| |
| Widget _photoBuilder(int index) { |
| switch (index) { |
| case 0: |
| return new Image.asset( |
| 'packages/perspective/res/module-b-gallery-photos/1.png', |
| fit: BoxFit.fitWidth, |
| ); |
| case 1: |
| return new Image.asset( |
| 'packages/perspective/res/module-b-gallery-photos/2.png', |
| fit: BoxFit.fitWidth, |
| ); |
| case 2: |
| return new Stack( |
| children: <Widget>[ |
| new Image.asset( |
| 'packages/perspective/res/module-b-gallery-photos/3.png', |
| fit: BoxFit.fitWidth, |
| ), |
| new Positioned( |
| left: _kPhotoListCheckInset, |
| bottom: _kPhotoListCheckInset, |
| width: _kPhotoListCheckSize, |
| height: _kPhotoListCheckSize, |
| child: new PhysicalModel( |
| shape: BoxShape.circle, |
| color: _kPhotoListCheckBackgroundColor, |
| child: new Icon( |
| Icons.check, |
| color: _kPhotoListCheckIconColor, |
| size: _kPhotoListCheckSize, |
| ), |
| ), |
| ) |
| ], |
| ); |
| case 3: |
| return new GestureDetector( |
| onTap: onVideoTapped, |
| child: new Stack( |
| children: <Widget>[ |
| new Image.asset( |
| 'packages/perspective/res/module-b-gallery-photos/4.png', |
| fit: BoxFit.fitWidth, |
| ), |
| const Positioned.fill( |
| child: const Center( |
| child: const Icon( |
| Icons.play_circle_filled, |
| color: Colors.white, |
| size: _kPhotoListVideoIconSize, |
| ), |
| ), |
| ) |
| ], |
| ), |
| ); |
| case 4: |
| return new Container( |
| height: _kPhotoListQuadPictureHeight, |
| child: new Row( |
| children: <Widget>[ |
| new Flexible( |
| child: new Column( |
| children: <Widget>[ |
| new Expanded( |
| child: new Image.asset( |
| 'packages/perspective/res/module-b-gallery-photos/' |
| '5.png', |
| fit: BoxFit.fitWidth, |
| ), |
| ), |
| new Container(height: _kPhotoListPhotoMargin), |
| new Expanded( |
| child: new Image.asset( |
| 'packages/perspective/res/module-b-gallery-photos/' |
| '7.png', |
| fit: BoxFit.fitWidth, |
| ), |
| ) |
| ], |
| ), |
| ), |
| new Container(width: _kPhotoListPhotoMargin), |
| new Flexible( |
| child: new Column( |
| children: <Widget>[ |
| new Expanded( |
| child: new Image.asset( |
| 'packages/perspective/res/module-b-gallery-photos/' |
| '6.png', |
| fit: BoxFit.fitWidth, |
| ), |
| ), |
| new Container(height: _kPhotoListPhotoMargin), |
| new Expanded( |
| child: new Image.asset( |
| 'packages/perspective/res/module-b-gallery-photos/' |
| '8.png', |
| fit: BoxFit.fitWidth, |
| ), |
| ) |
| ], |
| ), |
| ), |
| ], |
| ), |
| ); |
| case 5: |
| return new Image.asset( |
| 'packages/perspective/res/module-b-gallery-photos/9.png', |
| fit: BoxFit.fitWidth, |
| ); |
| case 6: |
| return new Image.asset( |
| 'packages/perspective/res/module-b-gallery-photos/10.png', |
| fit: BoxFit.fitWidth, |
| ); |
| case 7: |
| default: |
| return new Image.asset( |
| 'packages/perspective/res/module-b-gallery-photos/11.png', |
| fit: BoxFit.fitWidth, |
| ); |
| } |
| } |
| } |
| |
| class _PhotoListHeader extends StatelessWidget { |
| @override |
| Widget build(BuildContext context) => new PhysicalModel( |
| elevation: _kPhotoListTitleRelativeElevation, |
| color: _kPhotoListTitleColor, |
| child: new Stack( |
| children: <Widget>[ |
| const Align( |
| alignment: FractionalOffset.centerLeft, |
| child: const Padding( |
| padding: const EdgeInsets.only( |
| left: _kPhotoListTitleLeftPadding, |
| ), |
| child: const Text( |
| 'Capture.', |
| style: const TextStyle( |
| color: Colors.white, |
| fontSize: _kPhotoListTitleFontSize, |
| fontFamily: 'RobotoMedium', |
| ), |
| ), |
| ), |
| ), |
| new Align( |
| alignment: FractionalOffset.centerRight, |
| child: new Padding( |
| padding: const EdgeInsets.only( |
| right: _kPhotoListTitleRightPadding, |
| ), |
| child: new Column( |
| mainAxisSize: MainAxisSize.min, |
| children: const <Widget>[ |
| const Text( |
| 'French Polynesia', |
| style: const TextStyle( |
| color: Colors.white, |
| fontSize: _kPhotoListDescriptionFontSize, |
| fontFamily: 'Roboto', |
| fontWeight: FontWeight.bold, |
| ), |
| ), |
| const Text( |
| 'June 2017', |
| style: const TextStyle( |
| color: Colors.white, |
| fontSize: _kPhotoListDescriptionFontSize, |
| fontFamily: 'RobotoRegular', |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ], |
| ), |
| ); |
| } |
| |
| class _SearchBox extends StatelessWidget { |
| @override |
| Widget build(BuildContext context) => new PhysicalModel( |
| borderRadius: _kSearchBoxBorderRadius, |
| color: _kSearchBoxBackgroundColor, |
| elevation: _kSearchBoxRelativeElevation, |
| child: new Container( |
| width: _kSearchBoxWidth, |
| height: _kSearchBoxHeight, |
| child: new Padding( |
| padding: const EdgeInsets.symmetric( |
| horizontal: _kSearchBoxHorizontalMargin, |
| ), |
| child: new Stack( |
| children: <Widget>[ |
| new Align( |
| alignment: FractionalOffset.centerLeft, |
| child: new Row( |
| children: <Widget>[ |
| new Icon( |
| Icons.menu, |
| size: _kSearchBoxIconSize, |
| color: _kSearchBoxTextColor, |
| ), |
| new Container(width: _kSearchBoxHorizontalMargin), |
| new Text( |
| 'Story Book', |
| style: new TextStyle( |
| fontFamily: 'RobotoRegular', |
| fontSize: _kSearchBoxTextSize, |
| color: _kSearchBoxTextColor, |
| ), |
| ) |
| ], |
| ), |
| ), |
| new Align( |
| alignment: FractionalOffset.centerRight, |
| child: new Icon( |
| Icons.search, |
| size: _kSearchBoxIconSize, |
| color: _kSearchBoxTextColor, |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| class _SunRay extends StatelessWidget { |
| final double elevation; |
| |
| const _SunRay({this.elevation}); |
| |
| @override |
| Widget build(BuildContext context) => new PhysicalModel( |
| color: _kSunColor, |
| elevation: elevation, |
| child: const SizedBox( |
| width: _kSunRayWidth, |
| height: _kSunRayHeight, |
| ), |
| ); |
| } |
| |
| class _Sun extends StatelessWidget { |
| final double elevation; |
| |
| const _Sun({this.elevation}); |
| |
| @override |
| Widget build(BuildContext context) => new SizedBox( |
| width: _kSunDiameter, |
| height: _kSunDiameter, |
| child: new Stack( |
| children: <Widget>[ |
| // Top ray. |
| new Align( |
| alignment: FractionalOffset.center, |
| child: new Transform( |
| alignment: FractionalOffset.center, |
| transform: new Matrix4.translationValues( |
| 0.0, |
| _kSunRayOffset, |
| 0.0, |
| ), |
| child: new Transform( |
| alignment: FractionalOffset.center, |
| transform: new Matrix4.rotationZ(math.pi / 2.0), |
| child: new _SunRay(elevation: elevation), |
| ), |
| ), |
| ), |
| |
| // Bottom ray. |
| new Align( |
| alignment: FractionalOffset.center, |
| child: new Transform( |
| alignment: FractionalOffset.center, |
| transform: new Matrix4.translationValues( |
| 0.0, |
| -_kSunRayOffset, |
| 0.0, |
| ), |
| child: new Transform( |
| alignment: FractionalOffset.center, |
| transform: new Matrix4.rotationZ(math.pi / 2.0), |
| child: new _SunRay(elevation: elevation), |
| ), |
| ), |
| ), |
| |
| // Right ray. |
| new Align( |
| alignment: FractionalOffset.center, |
| child: new Transform( |
| alignment: FractionalOffset.center, |
| transform: new Matrix4.translationValues( |
| _kSunRayOffset, |
| 0.0, |
| 0.0, |
| ), |
| child: new _SunRay(elevation: elevation), |
| ), |
| ), |
| |
| // Left ray. |
| new Align( |
| alignment: FractionalOffset.center, |
| child: new Transform( |
| alignment: FractionalOffset.center, |
| transform: new Matrix4.translationValues( |
| -_kSunRayOffset, |
| 0.0, |
| 0.0, |
| ), |
| child: new _SunRay(elevation: elevation), |
| ), |
| ), |
| |
| // Sun. |
| new Align( |
| alignment: FractionalOffset.center, |
| child: new PhysicalModel( |
| color: _kSunColor, |
| elevation: elevation, |
| shape: BoxShape.circle, |
| child: const SizedBox( |
| width: _kSunCenterDiameter, |
| height: _kSunCenterDiameter, |
| ), |
| ), |
| ), |
| |
| // Bottom right ray. |
| new Align( |
| alignment: FractionalOffset.center, |
| child: new Transform( |
| alignment: FractionalOffset.center, |
| transform: new Matrix4.translationValues( |
| _kSunRayOffset * _kSqrt2, |
| _kSunRayOffset * _kSqrt2, |
| 0.0, |
| ), |
| child: new Transform( |
| alignment: FractionalOffset.center, |
| transform: new Matrix4.rotationZ(math.pi / 4.0), |
| child: new _SunRay(elevation: elevation), |
| ), |
| ), |
| ), |
| |
| // Top left ray. |
| new Align( |
| alignment: FractionalOffset.center, |
| child: new Transform( |
| alignment: FractionalOffset.center, |
| transform: new Matrix4.translationValues( |
| -_kSunRayOffset * _kSqrt2, |
| -_kSunRayOffset * _kSqrt2, |
| 0.0, |
| ), |
| child: new Transform( |
| alignment: FractionalOffset.center, |
| transform: new Matrix4.rotationZ(math.pi / 4.0), |
| child: new _SunRay(elevation: elevation), |
| ), |
| ), |
| ), |
| |
| // Top right ray. |
| new Align( |
| alignment: FractionalOffset.center, |
| child: new Transform( |
| alignment: FractionalOffset.center, |
| transform: new Matrix4.translationValues( |
| _kSunRayOffset * _kSqrt2, |
| -_kSunRayOffset * _kSqrt2, |
| 0.0, |
| ), |
| child: new Transform( |
| alignment: FractionalOffset.center, |
| transform: new Matrix4.rotationZ(3.0 * math.pi / 4.0), |
| child: new _SunRay(elevation: elevation), |
| ), |
| ), |
| ), |
| |
| // Top right ray. |
| new Align( |
| alignment: FractionalOffset.center, |
| child: new Transform( |
| alignment: FractionalOffset.center, |
| transform: new Matrix4.translationValues( |
| -_kSunRayOffset * _kSqrt2, |
| _kSunRayOffset * _kSqrt2, |
| 0.0, |
| ), |
| child: new Transform( |
| alignment: FractionalOffset.center, |
| transform: new Matrix4.rotationZ(3.0 * math.pi / 4.0), |
| child: new _SunRay(elevation: elevation), |
| ), |
| ), |
| ), |
| ], |
| ), |
| ); |
| } |