From 1a6ed678939f6b026267326021d43b87f0797381 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sun, 28 Jul 2019 18:25:12 +0900 Subject: [PATCH] split fullscreen page widgets --- lib/image_fullscreen_page.dart | 201 ------------------ lib/main.dart | 2 +- lib/thumbnail_collection.dart | 8 +- .../common/draggable_scrollbar.dart | 0 lib/{ => widgets}/common/fake_app_bar.dart | 0 lib/{ => widgets}/common/outlined_text.dart | 0 lib/widgets/fullscreen/image_page.dart | 184 ++++++++++++++++ lib/widgets/fullscreen/info_page.dart | 53 +++++ .../fullscreen/overlay.dart} | 0 9 files changed, 242 insertions(+), 206 deletions(-) delete mode 100644 lib/image_fullscreen_page.dart rename lib/{ => widgets}/common/draggable_scrollbar.dart (100%) rename lib/{ => widgets}/common/fake_app_bar.dart (100%) rename lib/{ => widgets}/common/outlined_text.dart (100%) create mode 100644 lib/widgets/fullscreen/image_page.dart create mode 100644 lib/widgets/fullscreen/info_page.dart rename lib/{image_fullscreen_overlay.dart => widgets/fullscreen/overlay.dart} (100%) diff --git a/lib/image_fullscreen_page.dart b/lib/image_fullscreen_page.dart deleted file mode 100644 index a99c44fcf..000000000 --- a/lib/image_fullscreen_page.dart +++ /dev/null @@ -1,201 +0,0 @@ -import 'dart:io'; -import 'dart:math'; - -import 'package:aves/image_fullscreen_overlay.dart'; -import 'package:aves/model/image_entry.dart'; -import 'package:aves/utils/file_utils.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:photo_view/photo_view.dart'; -import 'package:photo_view/photo_view_gallery.dart'; - -class ImageFullscreenPage extends StatefulWidget { - final List entries; - final String initialUri; - - const ImageFullscreenPage({ - Key key, - this.entries, - this.initialUri, - }) : super(key: key); - - @override - ImageFullscreenPageState createState() => ImageFullscreenPageState(); -} - -class ImageFullscreenPageState extends State with SingleTickerProviderStateMixin { - int _currentPage; - PageController _pageController; - ValueNotifier _overlayVisible = ValueNotifier(false); - AnimationController _overlayAnimationController; - Animation _topOverlayOffset, _bottomOverlayOffset; - - List get entries => widget.entries; - - @override - void initState() { - super.initState(); - final index = entries.indexWhere((entry) => entry.uri == widget.initialUri); - _currentPage = max(0, index); - _pageController = PageController(initialPage: _currentPage); - _overlayAnimationController = AnimationController( - duration: Duration(milliseconds: 250), - vsync: this, - ); - _topOverlayOffset = Tween(begin: Offset(0, 0), end: Offset(0, -1)).animate(CurvedAnimation(parent: _overlayAnimationController, curve: Curves.easeOutQuart, reverseCurve: Curves.easeInQuart)); - _bottomOverlayOffset = Tween(begin: Offset(0, 0), end: Offset(0, 1)).animate(CurvedAnimation(parent: _overlayAnimationController, curve: Curves.easeOutQuart, reverseCurve: Curves.easeInQuart)); - _overlayVisible.addListener(onOverlayVisibleChange); - } - - @override - void dispose() { - _overlayVisible.removeListener(onOverlayVisibleChange); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return PageView( - scrollDirection: Axis.vertical, - children: [ - ClipRect( - child: Scaffold( - backgroundColor: Colors.black, - body: Stack( - children: [ - PhotoViewGallery.builder( - itemCount: entries.length, - builder: (context, index) { - final entry = entries[index]; - return PhotoViewGalleryPageOptions( - imageProvider: FileImage(File(entry.path)), - heroTag: entry.uri, - minScale: PhotoViewComputedScale.contained, - initialScale: PhotoViewComputedScale.contained, - onTapUp: (tapContext, details, value) => _overlayVisible.value = !_overlayVisible.value, - ); - }, - loadingChild: Center( - child: CircularProgressIndicator(), - ), - pageController: _pageController, - onPageChanged: (index) { - debugPrint('onPageChanged: index=$index'); - setState(() => _currentPage = index); - }, - transitionOnUserGestures: true, - scrollPhysics: BouncingScrollPhysics(), - ), - if (_currentPage != null) ...[ - SlideTransition( - position: _topOverlayOffset, - child: FullscreenTopOverlay( - entries: entries, - index: _currentPage, - ), - ), - Positioned( - bottom: 0, - child: SlideTransition( - position: _bottomOverlayOffset, - child: FullscreenBottomOverlay( - entries: entries, - index: _currentPage, - ), - ), - ) - ] - ], - ), - resizeToAvoidBottomInset: false, - ), - ), - InfoPage(entry: entries[_currentPage]), - ], -// Hero( -// tag: uri, -// child: Stack( -// children: [ -// Center( -// child: widget.thumbnail == null -// ? CircularProgressIndicator() -// : Image.memory( -// widget.thumbnail, -// width: requestWidth, -// height: requestHeight, -// fit: BoxFit.contain, -// ), -// ), -// Center( -// child: FadeInImage( -// placeholder: MemoryImage(kTransparentImage), -// image: FileImage(File(path)), -// fadeOutDuration: Duration(milliseconds: 1), -// fadeInDuration: Duration(milliseconds: 200), -// width: requestWidth, -// height: requestHeight, -// fit: BoxFit.contain, -// ), -// ), -// ], -// ), -// ), - ); - } - - onOverlayVisibleChange() { - if (_overlayVisible.value) - _overlayAnimationController.forward(); - else - _overlayAnimationController.reverse(); - } -} - -class InfoPage extends StatelessWidget { - final ImageEntry entry; - - const InfoPage({this.entry}); - - @override - Widget build(BuildContext context) { - final date = entry.getBestDate(); - return Scaffold( - appBar: AppBar( - title: Text('Info'), - ), - body: Padding( - padding: EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - InfoRow('Title', entry.title), - InfoRow('Date', '${DateFormat.yMMMd().format(date)} – ${DateFormat.Hm().format(date)}'), - InfoRow('Size', formatFilesize(entry.sizeBytes)), - InfoRow('Resolution', '${entry.width} × ${entry.height} (${entry.getMegaPixels()} MP)'), - InfoRow('Path', entry.path), - ], - ), - ), - ); - } -} - -class InfoRow extends StatelessWidget { - final String label, value; - - const InfoRow(this.label, this.value); - - @override - Widget build(BuildContext context) { - return Wrap( - children: [ - Text( - label, - style: TextStyle(color: Colors.white70), - ), - SizedBox(width: 8), - Text(value), - ], - ); - } -} diff --git a/lib/main.dart b/lib/main.dart index f1fef01fb..001d989e6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,7 @@ -import 'package:aves/common/fake_app_bar.dart'; import 'package:aves/model/image_decode_service.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/thumbnail_collection.dart'; +import 'package:aves/widgets/common/fake_app_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/lib/thumbnail_collection.dart b/lib/thumbnail_collection.dart index 544b1d1af..7e39db4fd 100644 --- a/lib/thumbnail_collection.dart +++ b/lib/thumbnail_collection.dart @@ -1,9 +1,9 @@ -import 'package:aves/common/draggable_scrollbar.dart'; -import 'package:aves/common/outlined_text.dart'; -import 'package:aves/image_fullscreen_page.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/thumbnail.dart'; import 'package:aves/utils/date_utils.dart'; +import 'package:aves/widgets/common/draggable_scrollbar.dart'; +import 'package:aves/widgets/common/outlined_text.dart'; +import 'package:aves/widgets/fullscreen/image_page.dart'; import "package:collection/collection.dart"; import 'package:flutter/material.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart'; @@ -103,7 +103,7 @@ class SectionSliver extends StatelessWidget { return Navigator.push( context, MaterialPageRoute( - builder: (context) => ImageFullscreenPage( + builder: (context) => FullscreenPage( entries: entries, initialUri: entry.uri, ), diff --git a/lib/common/draggable_scrollbar.dart b/lib/widgets/common/draggable_scrollbar.dart similarity index 100% rename from lib/common/draggable_scrollbar.dart rename to lib/widgets/common/draggable_scrollbar.dart diff --git a/lib/common/fake_app_bar.dart b/lib/widgets/common/fake_app_bar.dart similarity index 100% rename from lib/common/fake_app_bar.dart rename to lib/widgets/common/fake_app_bar.dart diff --git a/lib/common/outlined_text.dart b/lib/widgets/common/outlined_text.dart similarity index 100% rename from lib/common/outlined_text.dart rename to lib/widgets/common/outlined_text.dart diff --git a/lib/widgets/fullscreen/image_page.dart b/lib/widgets/fullscreen/image_page.dart new file mode 100644 index 000000000..370cce2c2 --- /dev/null +++ b/lib/widgets/fullscreen/image_page.dart @@ -0,0 +1,184 @@ +import 'dart:io'; +import 'dart:math'; + +import 'package:aves/model/image_entry.dart'; +import 'package:aves/widgets/fullscreen/info_page.dart'; +import 'package:aves/widgets/fullscreen/overlay.dart'; +import 'package:flutter/material.dart'; +import 'package:photo_view/photo_view.dart'; +import 'package:photo_view/photo_view_gallery.dart'; + +class FullscreenPage extends StatefulWidget { + final List entries; + final String initialUri; + + const FullscreenPage({ + Key key, + this.entries, + this.initialUri, + }) : super(key: key); + + @override + FullscreenPageState createState() => FullscreenPageState(); +} + +class FullscreenPageState extends State with SingleTickerProviderStateMixin { + int _currentHorizontalPage, _currentVerticalPage = 0; + PageController _horizontalPager, _verticalPager; + ValueNotifier _overlayVisible = ValueNotifier(false); + AnimationController _overlayAnimationController; + Animation _topOverlayOffset, _bottomOverlayOffset; + + List get entries => widget.entries; + + @override + void initState() { + super.initState(); + final index = entries.indexWhere((entry) => entry.uri == widget.initialUri); + _currentHorizontalPage = max(0, index); + _horizontalPager = PageController(initialPage: _currentHorizontalPage); + _verticalPager = PageController(initialPage: _currentVerticalPage); + _overlayAnimationController = AnimationController( + duration: Duration(milliseconds: 250), + vsync: this, + ); + _topOverlayOffset = Tween(begin: Offset(0, 0), end: Offset(0, -1)).animate(CurvedAnimation(parent: _overlayAnimationController, curve: Curves.easeOutQuart, reverseCurve: Curves.easeInQuart)); + _bottomOverlayOffset = Tween(begin: Offset(0, 0), end: Offset(0, 1)).animate(CurvedAnimation(parent: _overlayAnimationController, curve: Curves.easeOutQuart, reverseCurve: Curves.easeInQuart)); + _overlayVisible.addListener(onOverlayVisibleChange); + } + + @override + void dispose() { + _overlayVisible.removeListener(onOverlayVisibleChange); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + body: Stack( + children: [ + PageView( + scrollDirection: Axis.vertical, + controller: _verticalPager, + onPageChanged: (page) => setState(() => _currentVerticalPage = page), + children: [ + ImagePage( + entries: entries, + pageController: _horizontalPager, + onTap: () => _overlayVisible.value = !_overlayVisible.value, + onPageChanged: (page) => setState(() => _currentHorizontalPage = page), + ), + InfoPage( + entry: entries[_currentHorizontalPage], + ), + ], + ), + if (_currentHorizontalPage != null && _currentVerticalPage == 0) ...[ + SlideTransition( + position: _topOverlayOffset, + child: FullscreenTopOverlay( + entries: entries, + index: _currentHorizontalPage, + ), + ), + Positioned( + bottom: 0, + child: SlideTransition( + position: _bottomOverlayOffset, + child: FullscreenBottomOverlay( + entries: entries, + index: _currentHorizontalPage, + ), + ), + ) + ] + ], + ), + resizeToAvoidBottomInset: false, +// Hero( +// tag: uri, +// child: Stack( +// children: [ +// Center( +// child: widget.thumbnail == null +// ? CircularProgressIndicator() +// : Image.memory( +// widget.thumbnail, +// width: requestWidth, +// height: requestHeight, +// fit: BoxFit.contain, +// ), +// ), +// Center( +// child: FadeInImage( +// placeholder: MemoryImage(kTransparentImage), +// image: FileImage(File(path)), +// fadeOutDuration: Duration(milliseconds: 1), +// fadeInDuration: Duration(milliseconds: 200), +// width: requestWidth, +// height: requestHeight, +// fit: BoxFit.contain, +// ), +// ), +// ], +// ), +// ), + ); + } + + onOverlayVisibleChange() { + if (_overlayVisible.value) + _overlayAnimationController.forward(); + else + _overlayAnimationController.reverse(); + } +} + +class ImagePage extends StatefulWidget { + final List entries; + final PageController pageController; + final VoidCallback onTap; + final ValueChanged onPageChanged; + + const ImagePage({ + this.entries, + this.pageController, + this.onTap, + this.onPageChanged, + }); + + @override + State createState() => ImagePageState(); +} + +class ImagePageState extends State with AutomaticKeepAliveClientMixin { + @override + Widget build(BuildContext context) { + super.build(context); + return PhotoViewGallery.builder( + itemCount: widget.entries.length, + builder: (context, index) { + final entry = widget.entries[index]; + return PhotoViewGalleryPageOptions( + imageProvider: FileImage(File(entry.path)), + heroTag: entry.uri, + minScale: PhotoViewComputedScale.contained, + initialScale: PhotoViewComputedScale.contained, + onTapUp: (tapContext, details, value) => widget.onTap?.call(), + ); + }, + loadingChild: Center( + child: CircularProgressIndicator(), + ), + pageController: widget.pageController, + onPageChanged: widget.onPageChanged, + transitionOnUserGestures: true, + scrollPhysics: BouncingScrollPhysics(), + ); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/widgets/fullscreen/info_page.dart b/lib/widgets/fullscreen/info_page.dart new file mode 100644 index 000000000..1426000ae --- /dev/null +++ b/lib/widgets/fullscreen/info_page.dart @@ -0,0 +1,53 @@ +import 'package:aves/model/image_entry.dart'; +import 'package:aves/utils/file_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +class InfoPage extends StatelessWidget { + final ImageEntry entry; + + const InfoPage({this.entry}); + + @override + Widget build(BuildContext context) { + final date = entry.getBestDate(); + return Scaffold( + appBar: AppBar( + title: Text('Info'), + ), + body: Padding( + padding: EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InfoRow('Title', entry.title), + InfoRow('Date', '${DateFormat.yMMMd().format(date)} – ${DateFormat.Hm().format(date)}'), + InfoRow('Size', formatFilesize(entry.sizeBytes)), + InfoRow('Resolution', '${entry.width} × ${entry.height} (${entry.getMegaPixels()} MP)'), + InfoRow('Path', entry.path), + ], + ), + ), + ); + } +} + +class InfoRow extends StatelessWidget { + final String label, value; + + const InfoRow(this.label, this.value); + + @override + Widget build(BuildContext context) { + return Wrap( + children: [ + Text( + label, + style: TextStyle(color: Colors.white70), + ), + SizedBox(width: 8), + Text(value), + ], + ); + } +} diff --git a/lib/image_fullscreen_overlay.dart b/lib/widgets/fullscreen/overlay.dart similarity index 100% rename from lib/image_fullscreen_overlay.dart rename to lib/widgets/fullscreen/overlay.dart