fullscreen: fixed route transition

This commit is contained in:
Thibault Deckers 2019-12-26 22:06:31 +09:00
parent 68766d0e17
commit aafcc1da63
24 changed files with 186 additions and 175 deletions

View file

@ -4,8 +4,8 @@ import 'package:aves/widgets/album/all_collection_drawer.dart';
import 'package:aves/widgets/album/all_collection_page.dart'; import 'package:aves/widgets/album/all_collection_page.dart';
import 'package:aves/widgets/common/fake_app_bar.dart'; import 'package:aves/widgets/common/fake_app_bar.dart';
import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/common/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/media_store_collection_provider.dart'; import 'package:aves/widgets/common/providers/media_store_collection_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pedantic/pedantic.dart'; import 'package:pedantic/pedantic.dart';
@ -64,7 +64,7 @@ class _HomePageState extends State<HomePage> {
// TODO reduce permission check time // TODO reduce permission check time
// TODO TLAD ask android.permission.ACCESS_MEDIA_LOCATION (unredacted EXIF with scoped storage) // TODO TLAD ask android.permission.ACCESS_MEDIA_LOCATION (unredacted EXIF with scoped storage)
final permissions = await PermissionHandler().requestPermissions([ final permissions = await PermissionHandler().requestPermissions([
PermissionGroup.storage PermissionGroup.storage,
]); // 350ms ]); // 350ms
if (permissions[PermissionGroup.storage] != PermissionStatus.granted) { if (permissions[PermissionGroup.storage] != PermissionStatus.granted) {
unawaited(SystemNavigator.pop()); unawaited(SystemNavigator.pop());

View file

@ -60,7 +60,7 @@ class ImageCollection with ChangeNotifier {
break; break;
case SortFactor.size: case SortFactor.size:
sections = Map.fromEntries([ sections = Map.fromEntries([
MapEntry('All', _rawEntries) MapEntry('All', _rawEntries),
]); ]);
break; break;
} }

View file

@ -181,7 +181,7 @@ class ImageEntry {
return { return {
addressDetails.countryName, addressDetails.countryName,
addressDetails.adminArea, addressDetails.adminArea,
addressDetails.locality addressDetails.locality,
}.where((part) => part != null && part.isNotEmpty).join(', '); }.where((part) => part != null && part.isNotEmpty).join(', ');
} }

View file

@ -13,7 +13,7 @@ String _decimal2sexagesimal(final double dec) {
final List<String> tmp = NumberFormat('0.0#####').format(_round(value, decimals: 10)).split('.'); final List<String> tmp = NumberFormat('0.0#####').format(_round(value, decimals: 10)).split('.');
return <int>[ return <int>[
int.parse(tmp[0]).abs(), int.parse(tmp[0]).abs(),
int.parse(tmp[1]) int.parse(tmp[1]),
]; ];
} }
@ -39,6 +39,6 @@ List<String> toDMS(Tuple2<double, double> latLng) {
final lng = latLng.item2; final lng = latLng.item2;
return [ return [
'${_decimal2sexagesimal(lat)} ${lat < 0 ? 'S' : 'N'}', '${_decimal2sexagesimal(lat)} ${lat < 0 ? 'S' : 'N'}',
'${_decimal2sexagesimal(lng)} ${lng < 0 ? 'W' : 'E'}' '${_decimal2sexagesimal(lng)} ${lng < 0 ? 'W' : 'E'}',
]; ];
} }

View file

@ -60,22 +60,22 @@ class AllCollectionDrawer extends StatelessWidget {
Row(children: [ Row(children: [
Icon(Icons.photo_library), Icon(Icons.photo_library),
const SizedBox(width: 4), const SizedBox(width: 4),
Text('${collection.imageCount}') Text('${collection.imageCount}'),
]), ]),
Row(children: [ Row(children: [
Icon(Icons.video_library), Icon(Icons.video_library),
const SizedBox(width: 4), const SizedBox(width: 4),
Text('${collection.videoCount}') Text('${collection.videoCount}'),
]), ]),
Row(children: [ Row(children: [
Icon(Icons.photo_album), Icon(Icons.photo_album),
const SizedBox(width: 4), const SizedBox(width: 4),
Text('${collection.albumCount}') Text('${collection.albumCount}'),
]), ]),
Row(children: [ Row(children: [
Icon(Icons.label), Icon(Icons.label),
const SizedBox(width: 4), const SizedBox(width: 4),
Text('${collection.tagCount}') Text('${collection.tagCount}'),
]), ]),
], ],
), ),

View file

@ -0,0 +1,121 @@
import 'package:aves/model/image_collection.dart';
import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/album/sections.dart';
import 'package:aves/widgets/album/thumbnail.dart';
import 'package:aves/widgets/album/transparent_material_page_route.dart';
import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/fullscreen/fullscreen_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
class SectionSliver extends StatelessWidget {
final ImageCollection collection;
final Map<dynamic, List<ImageEntry>> sections;
final dynamic sectionKey;
final double screenWidth;
const SectionSliver({
Key key,
@required this.collection,
@required this.sections,
@required this.sectionKey,
@required this.screenWidth,
}) : super(key: key);
@override
Widget build(BuildContext context) {
const columnCount = 4;
return SliverStickyHeader(
header: SectionHeader(
collection: collection,
sections: sections,
sectionKey: sectionKey,
),
sliver: SliverGrid(
delegate: SliverChildBuilderDelegate(
// TODO TLAD find out why thumbnails are rebuilt (with `initState`) when:
// - config change (show/hide status bar)
// - navigating away/back
(sliverContext, index) {
final sectionEntries = sections[sectionKey];
if (index >= sectionEntries.length) return null;
final entry = sectionEntries[index];
return GestureDetector(
onTap: () => _showFullscreen(sliverContext, entry),
child: Thumbnail(
entry: entry,
extent: screenWidth / columnCount,
),
);
},
childCount: sections[sectionKey].length,
addAutomaticKeepAlives: false,
addRepaintBoundaries: true,
),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columnCount,
),
),
overlapsContent: false,
);
}
void _showFullscreen(BuildContext context, ImageEntry entry) {
Navigator.push(
context,
TransparentMaterialPageRoute(
pageBuilder: (context, _, __) => FullscreenPage(
collection: collection,
initialUri: entry.uri,
),
),
);
}
}
class SectionHeader extends StatelessWidget {
final ImageCollection collection;
final Map<dynamic, List<ImageEntry>> sections;
final dynamic sectionKey;
const SectionHeader({
Key key,
@required this.collection,
@required this.sections,
@required this.sectionKey,
}) : super(key: key);
@override
Widget build(BuildContext context) {
Widget header = const SizedBox.shrink();
if (collection.sortFactor == SortFactor.date) {
switch (collection.groupFactor) {
case GroupFactor.album:
Widget albumIcon = IconUtils.getAlbumIcon(context, sectionKey as String);
if (albumIcon != null) {
albumIcon = Material(
type: MaterialType.circle,
elevation: 3,
color: Colors.transparent,
shadowColor: Colors.black,
child: albumIcon,
);
}
header = TitleSectionHeader(
leading: albumIcon,
title: collection.getUniqueAlbumName(sectionKey as String, sections.keys.cast<String>()),
);
break;
case GroupFactor.month:
header = MonthSectionHeader(date: sectionKey as DateTime);
break;
case GroupFactor.day:
header = DaySectionHeader(date: sectionKey as DateTime);
break;
}
}
return IgnorePointer(
child: header,
);
}
}

View file

@ -1,7 +1,7 @@
import 'package:aves/model/image_collection.dart'; import 'package:aves/model/image_collection.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/album/thumbnail_collection.dart'; import 'package:aves/widgets/album/thumbnail_collection.dart';
import 'package:aves/widgets/common/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class FilteredCollectionPage extends StatelessWidget { class FilteredCollectionPage extends StatelessWidget {

View file

@ -1,7 +1,7 @@
import 'package:aves/model/image_collection.dart'; import 'package:aves/model/image_collection.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/album/thumbnail_collection.dart'; import 'package:aves/widgets/album/thumbnail_collection.dart';
import 'package:aves/widgets/common/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class ImageSearchDelegate extends SearchDelegate<ImageEntry> { class ImageSearchDelegate extends SearchDelegate<ImageEntry> {

View file

@ -1,5 +1,5 @@
import 'package:aves/utils/time_utils.dart'; import 'package:aves/utils/time_utils.dart';
import 'package:aves/widgets/common/outlined_text.dart'; import 'package:aves/widgets/common/fx/outlined_text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';

View file

@ -1,12 +1,8 @@
import 'package:aves/model/image_collection.dart'; import 'package:aves/model/image_collection.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/album/sections.dart'; import 'package:aves/widgets/album/collection_section.dart';
import 'package:aves/widgets/album/thumbnail.dart';
import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/fullscreen/fullscreen_page.dart';
import 'package:draggable_scrollbar/draggable_scrollbar.dart'; import 'package:draggable_scrollbar/draggable_scrollbar.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class ThumbnailCollection extends AnimatedWidget { class ThumbnailCollection extends AnimatedWidget {
@ -103,126 +99,3 @@ class ThumbnailCollectionContent extends StatelessWidget {
); );
} }
} }
class SectionSliver extends StatelessWidget {
final ImageCollection collection;
final Map<dynamic, List<ImageEntry>> sections;
final dynamic sectionKey;
final double screenWidth;
const SectionSliver({
Key key,
@required this.collection,
@required this.sections,
@required this.sectionKey,
@required this.screenWidth,
}) : super(key: key);
@override
Widget build(BuildContext context) {
const columnCount = 4;
return SliverStickyHeader(
header: SectionHeader(
collection: collection,
sections: sections,
sectionKey: sectionKey,
),
sliver: SliverGrid(
delegate: SliverChildBuilderDelegate(
// TODO TLAD find out why thumbnails are rebuilt (with `initState`) when:
// - config change (show/hide status bar)
// - navigating away/back
(sliverContext, index) {
final sectionEntries = sections[sectionKey];
if (index >= sectionEntries.length) return null;
final entry = sectionEntries[index];
return GestureDetector(
onTap: () => _showFullscreen(sliverContext, entry),
child: Thumbnail(
entry: entry,
extent: screenWidth / columnCount,
),
);
},
childCount: sections[sectionKey].length,
addAutomaticKeepAlives: false,
addRepaintBoundaries: true,
),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columnCount,
),
),
overlapsContent: false,
);
}
void _showFullscreen(BuildContext context, ImageEntry entry) {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => FullscreenPage(
// collection: collection,
// initialUri: entry.uri,
// ),
// ),
// );
// TODO TLAD consider the following to have transparency while popping fullscreen by drag down
Navigator.push(
context,
PageRouteBuilder(
opaque: false,
pageBuilder: (BuildContext context, _, __) => FullscreenPage(
collection: collection,
initialUri: entry.uri,
),
),
);
}
}
class SectionHeader extends StatelessWidget {
final ImageCollection collection;
final Map<dynamic, List<ImageEntry>> sections;
final dynamic sectionKey;
const SectionHeader({
Key key,
@required this.collection,
@required this.sections,
@required this.sectionKey,
}) : super(key: key);
@override
Widget build(BuildContext context) {
Widget header = const SizedBox.shrink();
if (collection.sortFactor == SortFactor.date) {
switch (collection.groupFactor) {
case GroupFactor.album:
Widget albumIcon = IconUtils.getAlbumIcon(context, sectionKey as String);
if (albumIcon != null) {
albumIcon = Material(
type: MaterialType.circle,
elevation: 3,
color: Colors.transparent,
shadowColor: Colors.black,
child: albumIcon,
);
}
header = TitleSectionHeader(
leading: albumIcon,
title: collection.getUniqueAlbumName(sectionKey as String, sections.keys.cast<String>()),
);
break;
case GroupFactor.month:
header = MonthSectionHeader(date: sectionKey as DateTime);
break;
case GroupFactor.day:
header = DaySectionHeader(date: sectionKey as DateTime);
break;
}
}
return IgnorePointer(
child: header,
);
}
}

View file

@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
class TransparentMaterialPageRoute<T> extends PageRouteBuilder<T> {
TransparentMaterialPageRoute({
@required RoutePageBuilder pageBuilder,
}) : super(pageBuilder: pageBuilder);
@override
bool get opaque => false;
@override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
final PageTransitionsTheme theme = Theme.of(context).pageTransitionsTheme;
return theme.buildTransitions<T>(this, context, animation, secondaryAnimation, child);
}
}

View file

@ -39,7 +39,7 @@ class ImagePreviewState extends State<ImagePreview> with AfterInitMixin {
super.initState(); super.initState();
_entryChangeNotifier = Listenable.merge([ _entryChangeNotifier = Listenable.merge([
entry.imageChangeNotifier, entry.imageChangeNotifier,
entry.metadataChangeNotifier entry.metadataChangeNotifier,
]); ]);
_entryChangeNotifier.addListener(_onEntryChange); _entryChangeNotifier.addListener(_onEntryChange);
} }

View file

@ -3,7 +3,7 @@ import 'package:aves/model/image_metadata.dart';
import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/metadata_db.dart';
import 'package:aves/model/settings.dart'; import 'package:aves/model/settings.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/common/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';

View file

@ -3,7 +3,7 @@ import 'dart:math';
import 'package:aves/model/image_collection.dart'; import 'package:aves/model/image_collection.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/common/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/fullscreen/fullscreen_action_delegate.dart'; import 'package:aves/widgets/fullscreen/fullscreen_action_delegate.dart';
import 'package:aves/widgets/fullscreen/image_page.dart'; import 'package:aves/widgets/fullscreen/image_page.dart';
import 'package:aves/widgets/fullscreen/info/info_page.dart'; import 'package:aves/widgets/fullscreen/info/info_page.dart';
@ -298,7 +298,7 @@ class FullscreenVerticalPageView extends StatefulWidget {
class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView> { class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView> {
bool _isInitialScale = true; bool _isInitialScale = true;
Color _background = Colors.black; ValueNotifier<Color> _backgroundColorNotifier = ValueNotifier(Colors.black);
@override @override
void initState() { void initState() {
@ -328,24 +328,25 @@ class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView>
} }
void _onVerticalPageControllerChange() { void _onVerticalPageControllerChange() {
setState(() => _background = _background.withOpacity(min(1.0, widget.verticalPager.page))); _backgroundColorNotifier.value = _backgroundColorNotifier.value.withOpacity(min(1.0, widget.verticalPager.page));
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PageView( return ValueListenableBuilder(
valueListenable: _backgroundColorNotifier,
builder: (context, backgroundColor, child) => Container(
color: backgroundColor,
child: child,
),
child: PageView(
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
controller: widget.verticalPager, controller: widget.verticalPager,
physics: _isInitialScale ? const PageScrollPhysics() : const NeverScrollableScrollPhysics(), physics: _isInitialScale ? const PageScrollPhysics() : const NeverScrollableScrollPhysics(),
onPageChanged: widget.onVerticalPageChanged, onPageChanged: widget.onVerticalPageChanged,
children: [ children: [
Container( const SizedBox(),
color: _background, ImagePage(
child: const SizedBox(),
),
Container(
color: _background,
child: ImagePage(
collection: widget.collection, collection: widget.collection,
pageController: widget.horizontalPager, pageController: widget.horizontalPager,
onTap: widget.onImageTap, onTap: widget.onImageTap,
@ -353,7 +354,6 @@ class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView>
onScaleChanged: (state) => setState(() => _isInitialScale = state == PhotoViewScaleState.initial), onScaleChanged: (state) => setState(() => _isInitialScale = state == PhotoViewScaleState.initial),
videoControllers: widget.videoControllers, videoControllers: widget.videoControllers,
), ),
),
NotificationListener( NotificationListener(
onNotification: (notification) { onNotification: (notification) {
if (notification is BackUpNotification) widget.onImagePageRequested(); if (notification is BackUpNotification) widget.onImagePageRequested();
@ -362,6 +362,7 @@ class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView>
child: InfoPage(collection: widget.collection, entry: widget.entry), child: InfoPage(collection: widget.collection, entry: widget.entry),
), ),
], ],
),
); );
} }
} }

View file

@ -37,7 +37,7 @@ class BasicSection extends StatelessWidget {
if (rotation != null) InfoRow('Rotation', '$rotation°'); if (rotation != null) InfoRow('Rotation', '$rotation°');
return [ return [
InfoRow('Duration', entry.durationText), InfoRow('Duration', entry.durationText),
if (rotation != null) InfoRow('Rotation', '$rotation°') if (rotation != null) InfoRow('Rotation', '$rotation°'),
]; ];
} }
} }

View file

@ -1,6 +1,6 @@
import 'package:aves/model/image_collection.dart'; import 'package:aves/model/image_collection.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/common/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/fullscreen/info/basic_section.dart'; import 'package:aves/widgets/fullscreen/info/basic_section.dart';
import 'package:aves/widgets/fullscreen/info/location_section.dart'; import 'package:aves/widgets/fullscreen/info/location_section.dart';
import 'package:aves/widgets/fullscreen/info/metadata_section.dart'; import 'package:aves/widgets/fullscreen/info/metadata_section.dart';

View file

@ -17,7 +17,7 @@ class LocationSection extends AnimatedWidget {
key: key, key: key,
listenable: Listenable.merge([ listenable: Listenable.merge([
entry.metadataChangeNotifier, entry.metadataChangeNotifier,
entry.addressChangeNotifier entry.addressChangeNotifier,
])); ]));
@override @override

View file

@ -6,7 +6,7 @@ import 'package:aves/model/image_metadata.dart';
import 'package:aves/model/metadata_service.dart'; import 'package:aves/model/metadata_service.dart';
import 'package:aves/utils/constants.dart'; import 'package:aves/utils/constants.dart';
import 'package:aves/utils/geo_utils.dart'; import 'package:aves/utils/geo_utils.dart';
import 'package:aves/widgets/common/blurred.dart'; import 'package:aves/widgets/common/fx/blurred.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';

View file

@ -1,4 +1,4 @@
import 'package:aves/widgets/common/blurred.dart'; import 'package:aves/widgets/common/fx/blurred.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class OverlayButton extends StatelessWidget { class OverlayButton extends StatelessWidget {

View file

@ -1,7 +1,7 @@
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/utils/android_app_service.dart'; import 'package:aves/utils/android_app_service.dart';
import 'package:aves/utils/time_utils.dart'; import 'package:aves/utils/time_utils.dart';
import 'package:aves/widgets/common/blurred.dart'; import 'package:aves/widgets/common/fx/blurred.dart';
import 'package:aves/widgets/fullscreen/overlay/common.dart'; import 'package:aves/widgets/fullscreen/overlay/common.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';