diff --git a/lib/image_providers/app_icon_image_provider.dart b/lib/image_providers/app_icon_image_provider.dart index 06d21092c..cbc7490e5 100644 --- a/lib/image_providers/app_icon_image_provider.dart +++ b/lib/image_providers/app_icon_image_provider.dart @@ -2,7 +2,7 @@ import 'dart:ui' as ui show Codec; import 'package:aves/services/android_app_service.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; class AppIconImage extends ImageProvider { const AppIconImage({ diff --git a/lib/image_providers/region_provider.dart b/lib/image_providers/region_provider.dart index 9170c8ec5..5d41855c4 100644 --- a/lib/image_providers/region_provider.dart +++ b/lib/image_providers/region_provider.dart @@ -2,10 +2,9 @@ import 'dart:async'; import 'dart:math'; import 'dart:ui' as ui show Codec; -import 'package:aves/model/image_entry.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; class RegionProvider extends ImageProvider { final RegionProviderKey key; @@ -23,7 +22,7 @@ class RegionProvider extends ImageProvider { codec: _loadAsync(key, decode), scale: key.scale, informationCollector: () sync* { - yield ErrorDescription('uri=${key.uri}, page=${key.page}, mimeType=${key.mimeType}, regionRect=${key.regionRect}'); + yield ErrorDescription('uri=${key.uri}, page=${key.page}, mimeType=${key.mimeType}, region=${key.region}'); }, ); } @@ -39,7 +38,7 @@ class RegionProvider extends ImageProvider { key.rotationDegrees, key.isFlipped, key.sampleSize, - key.regionRect, + key.region, key.imageSize, page: page, taskKey: key, @@ -64,21 +63,23 @@ class RegionProvider extends ImageProvider { } class RegionProviderKey { + // do not store the entry as it is, because the key should be constant + // but the entry attributes may change over time final String uri, mimeType; - final int rotationDegrees, sampleSize, page; + final int page, rotationDegrees, sampleSize; final bool isFlipped; - final Rectangle regionRect; + final Rectangle region; final Size imageSize; final double scale; const RegionProviderKey({ @required this.uri, @required this.mimeType, + @required this.page, @required this.rotationDegrees, @required this.isFlipped, - this.page, @required this.sampleSize, - @required this.regionRect, + @required this.region, @required this.imageSize, this.scale = 1.0, }) : assert(uri != null), @@ -86,48 +87,29 @@ class RegionProviderKey { assert(rotationDegrees != null), assert(isFlipped != null), assert(sampleSize != null), - assert(regionRect != null), + assert(region != null), assert(imageSize != null), assert(scale != null); - // do not store the entry as it is, because the key should be constant - // but the entry attributes may change over time - factory RegionProviderKey.fromEntry( - ImageEntry entry, { - @required int sampleSize, - @required Rectangle rect, - }) { - return RegionProviderKey( - uri: entry.uri, - mimeType: entry.mimeType, - rotationDegrees: entry.rotationDegrees, - isFlipped: entry.isFlipped, - page: entry.page, - sampleSize: sampleSize, - regionRect: rect, - imageSize: Size(entry.width.toDouble(), entry.height.toDouble()), - ); - } - @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; - return other is RegionProviderKey && other.uri == uri && other.mimeType == mimeType && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.page == page && other.sampleSize == sampleSize && other.regionRect == regionRect && other.imageSize == imageSize && other.scale == scale; + return other is RegionProviderKey && other.uri == uri && other.mimeType == mimeType && other.page == page && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.sampleSize == sampleSize && other.region == region && other.imageSize == imageSize && other.scale == scale; } @override int get hashCode => hashValues( uri, mimeType, + page, rotationDegrees, isFlipped, - page, sampleSize, - regionRect, + region, imageSize, scale, ); @override - String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, page=$page, sampleSize=$sampleSize, regionRect=$regionRect, imageSize=$imageSize, scale=$scale}'; + String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, page=$page, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, sampleSize=$sampleSize, region=$region, imageSize=$imageSize, scale=$scale}'; } diff --git a/lib/image_providers/thumbnail_provider.dart b/lib/image_providers/thumbnail_provider.dart index be69e5eca..02b4ac2f2 100644 --- a/lib/image_providers/thumbnail_provider.dart +++ b/lib/image_providers/thumbnail_provider.dart @@ -1,9 +1,8 @@ import 'dart:ui' as ui show Codec; -import 'package:aves/model/image_entry.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; class ThumbnailProvider extends ImageProvider { final ThumbnailProviderKey key; @@ -35,14 +34,13 @@ class ThumbnailProvider extends ImageProvider { final page = key.page; try { final bytes = await ImageFileService.getThumbnail( - uri, - mimeType, - key.dateModifiedSecs, - key.rotationDegrees, - key.isFlipped, - key.extent, - key.extent, + uri: uri, + mimeType: mimeType, page: page, + rotationDegrees: key.rotationDegrees, + isFlipped: key.isFlipped, + dateModifiedSecs: key.dateModifiedSecs, + extent: key.extent, taskKey: key, ); if (bytes == null) { @@ -65,61 +63,49 @@ class ThumbnailProvider extends ImageProvider { } class ThumbnailProviderKey { + // do not store the entry as it is, because the key should be constant + // but the entry attributes may change over time final String uri, mimeType; - final int dateModifiedSecs, rotationDegrees, page; + final int page, rotationDegrees; final bool isFlipped; + final int dateModifiedSecs; final double extent, scale; const ThumbnailProviderKey({ @required this.uri, @required this.mimeType, - @required this.dateModifiedSecs, + @required this.page, @required this.rotationDegrees, @required this.isFlipped, - this.page, + @required this.dateModifiedSecs, this.extent = 0, this.scale = 1, }) : assert(uri != null), assert(mimeType != null), - assert(dateModifiedSecs != null), assert(rotationDegrees != null), assert(isFlipped != null), + assert(dateModifiedSecs != null), assert(extent != null), assert(scale != null); - // do not store the entry as it is, because the key should be constant - // but the entry attributes may change over time - factory ThumbnailProviderKey.fromEntry(ImageEntry entry, {double extent = 0}) { - return ThumbnailProviderKey( - uri: entry.uri, - mimeType: entry.mimeType, - // `dateModifiedSecs` can be missing in viewer mode - dateModifiedSecs: entry.dateModifiedSecs ?? -1, - rotationDegrees: entry.rotationDegrees, - isFlipped: entry.isFlipped, - page: entry.page, - extent: extent, - ); - } - @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; - return other is ThumbnailProviderKey && other.uri == uri && other.extent == extent && other.mimeType == mimeType && other.dateModifiedSecs == dateModifiedSecs && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.page == page && other.scale == scale; + return other is ThumbnailProviderKey && other.uri == uri && other.mimeType == mimeType && other.page == page && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.dateModifiedSecs == dateModifiedSecs && other.extent == extent && other.scale == scale; } @override int get hashCode => hashValues( uri, mimeType, - dateModifiedSecs, + page, rotationDegrees, isFlipped, - page, + dateModifiedSecs, extent, scale, ); @override - String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, dateModifiedSecs=$dateModifiedSecs, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, page=$page, extent=$extent, scale=$scale}'; + String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, page=$page, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, dateModifiedSecs=$dateModifiedSecs, extent=$extent, scale=$scale}'; } diff --git a/lib/image_providers/uri_image_provider.dart b/lib/image_providers/uri_image_provider.dart index 22749c5a1..4e2d3f46c 100644 --- a/lib/image_providers/uri_image_provider.dart +++ b/lib/image_providers/uri_image_provider.dart @@ -3,7 +3,7 @@ import 'dart:ui' as ui show Codec; import 'package:aves/services/image_file_service.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:pedantic/pedantic.dart'; class UriImage extends ImageProvider { @@ -15,7 +15,7 @@ class UriImage extends ImageProvider { const UriImage({ @required this.uri, @required this.mimeType, - this.page, + @required this.page, @required this.rotationDegrees, @required this.isFlipped, this.expectedContentLength, diff --git a/lib/image_providers/uri_picture_provider.dart b/lib/image_providers/uri_picture_provider.dart index 913c78690..f6e8dc9c1 100644 --- a/lib/image_providers/uri_picture_provider.dart +++ b/lib/image_providers/uri_picture_provider.dart @@ -1,6 +1,6 @@ import 'package:aves/services/image_file_service.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:pedantic/pedantic.dart'; @@ -30,7 +30,7 @@ class UriPicture extends PictureProvider { Future _loadAsync(UriPicture key, {PictureErrorListener onError}) async { assert(key == this); - final data = await ImageFileService.getImage(uri, mimeType, 0, false); + final data = await ImageFileService.getSvg(uri, mimeType); if (data == null || data.isEmpty) { return null; } diff --git a/lib/main.dart b/lib/main.dart index 92cd32fbc..2445de98a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -47,8 +47,8 @@ class _AvesAppState extends State { // observers are not registered when using the same list object with different items // the list itself needs to be reassigned List _navigatorObservers = []; - final _newIntentChannel = EventChannel('deckers.thibault/aves/intent'); - final _navigatorKey = GlobalKey(); + final EventChannel _newIntentChannel = EventChannel('deckers.thibault/aves/intent'); + final GlobalKey _navigatorKey = GlobalKey(debugLabel: 'app-navigator'); static const accentColor = Colors.indigoAccent; diff --git a/lib/model/entry_cache.dart b/lib/model/entry_cache.dart index f135107e9..291cac72f 100644 --- a/lib/model/entry_cache.dart +++ b/lib/model/entry_cache.dart @@ -13,11 +13,13 @@ class EntryCache { bool oldIsFlipped, ) async { // TODO TLAD provide page parameter for multipage items, if someday image editing features are added for them + int page; // evict fullscreen image await UriImage( uri: uri, mimeType: mimeType, + page: page, rotationDegrees: oldRotationDegrees, isFlipped: oldIsFlipped, ).evict(); @@ -26,6 +28,7 @@ class EntryCache { await ThumbnailProvider(ThumbnailProviderKey( uri: uri, mimeType: mimeType, + page: page, dateModifiedSecs: dateModifiedSecs, rotationDegrees: oldRotationDegrees, isFlipped: oldIsFlipped, @@ -38,6 +41,7 @@ class EntryCache { (extent) => ThumbnailProvider(ThumbnailProviderKey( uri: uri, mimeType: mimeType, + page: page, dateModifiedSecs: dateModifiedSecs, rotationDegrees: oldRotationDegrees, isFlipped: oldIsFlipped, diff --git a/lib/model/entry_images.dart b/lib/model/entry_images.dart new file mode 100644 index 000000000..74214c382 --- /dev/null +++ b/lib/model/entry_images.dart @@ -0,0 +1,63 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:aves/image_providers/region_provider.dart'; +import 'package:aves/image_providers/thumbnail_provider.dart'; +import 'package:aves/image_providers/uri_image_provider.dart'; +import 'package:aves/model/image_entry.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +extension ExtraAvesEntry on ImageEntry { + ThumbnailProvider getThumbnail({double extent = 0}) => ThumbnailProvider(_getThumbnailProviderKey(extent)); + + ThumbnailProviderKey _getThumbnailProviderKey(double extent) { + // we standardize the thumbnail loading dimension by taking the nearest larger power of 2 + // so that there are less variants of the thumbnails to load and cache + // it increases the chance of cache hit when loading similarly sized columns (e.g. on orientation change) + final requestExtent = extent == 0 ? .0 : pow(2, (log(extent) / log(2)).ceil()).toDouble(); + + return ThumbnailProviderKey( + uri: uri, + mimeType: mimeType, + page: page, + rotationDegrees: rotationDegrees, + isFlipped: isFlipped, + dateModifiedSecs: dateModifiedSecs ?? -1, + extent: requestExtent, + ); + } + + RegionProvider getRegion({@required int sampleSize, Rectangle region}) => RegionProvider(getRegionProviderKey(sampleSize, region)); + + RegionProviderKey getRegionProviderKey(int sampleSize, Rectangle region) { + return RegionProviderKey( + uri: uri, + mimeType: mimeType, + page: page, + rotationDegrees: rotationDegrees, + isFlipped: isFlipped, + sampleSize: sampleSize, + region: region ?? Rectangle(0, 0, width, height), + imageSize: Size(width.toDouble(), height.toDouble()), + ); + } + + UriImage get uriImage => UriImage( + uri: uri, + mimeType: mimeType, + page: page, + rotationDegrees: rotationDegrees, + isFlipped: isFlipped, + expectedContentLength: sizeBytes, + ); + + bool _isReady(Object providerKey) => imageCache.statusForKey(providerKey).keepAlive; + + ImageProvider getBestThumbnail(double extent) { + final sizedThumbnailKey = _getThumbnailProviderKey(extent); + if (_isReady(sizedThumbnailKey)) return ThumbnailProvider(sizedThumbnailKey); + + return getThumbnail(); + } +} diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart index 079c72782..121be5731 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/image_entry.dart @@ -31,6 +31,8 @@ class ImageEntry { int sourceRotationDegrees; final int sizeBytes; String sourceTitle; + + // `dateModifiedSecs` can be missing in viewer mode int _dateModifiedSecs; final int sourceDateTakenMillis; final int durationMillis; @@ -227,7 +229,11 @@ class ImageEntry { !isAnimated && page == null; - bool get canTile => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff; + bool get supportTiling => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff; + + // as of panorama v0.3.1, the `Panorama` widget throws on initialization when the image is already resolved + // so we use tiles for panoramas as a workaround to not collide with the `panorama` package resolution + bool get useTiles => supportTiling && (width > 4096 || height > 4096 || is360); bool get isRaw => MimeTypes.rawImages.contains(mimeType); diff --git a/lib/services/app_shortcut_service.dart b/lib/services/app_shortcut_service.dart index 53b6c8f0c..6e0bf158a 100644 --- a/lib/services/app_shortcut_service.dart +++ b/lib/services/app_shortcut_service.dart @@ -26,18 +26,18 @@ class AppShortcutService { return false; } - static Future pin(String label, ImageEntry iconEntry, Set filters) async { + static Future pin(String label, ImageEntry entry, Set filters) async { Uint8List iconBytes; - if (iconEntry != null) { - final size = iconEntry.isVideo ? 0.0 : 256.0; + if (entry != null) { + final size = entry.isVideo ? 0.0 : 256.0; iconBytes = await ImageFileService.getThumbnail( - iconEntry.uri, - iconEntry.mimeType, - iconEntry.dateModifiedSecs, - iconEntry.rotationDegrees, - iconEntry.isFlipped, - size, - size, + uri: entry.uri, + mimeType: entry.mimeType, + page: entry.page, + rotationDegrees: entry.rotationDegrees, + isFlipped: entry.isFlipped, + dateModifiedSecs: entry.dateModifiedSecs, + extent: size, ); } try { diff --git a/lib/services/image_file_service.dart b/lib/services/image_file_service.dart index d69cff77b..80cd3fc37 100644 --- a/lib/services/image_file_service.dart +++ b/lib/services/image_file_service.dart @@ -69,6 +69,21 @@ class ImageFileService { return null; } + static Future getSvg( + String uri, + String mimeType, { + int expectedContentLength, + BytesReceivedCallback onBytesReceived, + }) => + getImage( + uri, + mimeType, + 0, + false, + expectedContentLength: expectedContentLength, + onBytesReceived: onBytesReceived, + ); + static Future getImage( String uri, String mimeType, @@ -155,15 +170,14 @@ class ImageFileService { ); } - static Future getThumbnail( - String uri, - String mimeType, - int dateModifiedSecs, - int rotationDegrees, - bool isFlipped, - double width, - double height, { - int page, + static Future getThumbnail({ + @required String uri, + @required String mimeType, + @required int rotationDegrees, + @required int page, + @required bool isFlipped, + @required int dateModifiedSecs, + @required double extent, Object taskKey, int priority, }) { @@ -179,8 +193,8 @@ class ImageFileService { 'dateModifiedSecs': dateModifiedSecs, 'rotationDegrees': rotationDegrees, 'isFlipped': isFlipped, - 'widthDip': width, - 'heightDip': height, + 'widthDip': extent, + 'heightDip': extent, 'page': page, 'defaultSizeDip': thumbnailDefaultSize, }); @@ -191,7 +205,7 @@ class ImageFileService { return null; }, // debugLabel: 'getThumbnail width=$width, height=$height entry=${entry.filenameWithoutExtension}', - priority: priority ?? (width == 0 || height == 0 ? ServiceCallPriority.getFastThumbnail : ServiceCallPriority.getSizedThumbnail), + priority: priority ?? (extent == 0 ? ServiceCallPriority.getFastThumbnail : ServiceCallPriority.getSizedThumbnail), key: taskKey, ); } diff --git a/lib/services/svg_metadata_service.dart b/lib/services/svg_metadata_service.dart index 8d09750bc..1152a74d9 100644 --- a/lib/services/svg_metadata_service.dart +++ b/lib/services/svg_metadata_service.dart @@ -17,7 +17,7 @@ class SvgMetadataService { static Future getSize(ImageEntry entry) async { try { - final data = await ImageFileService.getImage(entry.uri, entry.mimeType, 0, false); + final data = await ImageFileService.getSvg(entry.uri, entry.mimeType); final document = XmlDocument.parse(utf8.decode(data)); final root = document.rootElement; @@ -59,7 +59,7 @@ class SvgMetadataService { } try { - final data = await ImageFileService.getImage(entry.uri, entry.mimeType, 0, false); + final data = await ImageFileService.getSvg(entry.uri, entry.mimeType); final document = XmlDocument.parse(utf8.decode(data)); final root = document.rootElement; diff --git a/lib/widgets/collection/filter_bar.dart b/lib/widgets/collection/filter_bar.dart index 230e1cfea..10e922443 100644 --- a/lib/widgets/collection/filter_bar.dart +++ b/lib/widgets/collection/filter_bar.dart @@ -25,7 +25,7 @@ class FilterBar extends StatefulWidget implements PreferredSizeWidget { } class _FilterBarState extends State { - final GlobalKey _animatedListKey = GlobalKey(); + final GlobalKey _animatedListKey = GlobalKey(debugLabel: 'filter-bar-animated-list'); CollectionFilter _userRemovedFilter; @override diff --git a/lib/widgets/collection/thumbnail/raster.dart b/lib/widgets/collection/thumbnail/raster.dart index 65c473554..8ab298158 100644 --- a/lib/widgets/collection/thumbnail/raster.dart +++ b/lib/widgets/collection/thumbnail/raster.dart @@ -1,7 +1,5 @@ -import 'dart:math'; - import 'package:aves/image_providers/thumbnail_provider.dart'; -import 'package:aves/image_providers/uri_image_provider.dart'; +import 'package:aves/model/entry_images.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/collection/thumbnail/error.dart'; @@ -37,11 +35,6 @@ class _RasterImageThumbnailState extends State { Object get heroTag => widget.heroTag; - // we standardize the thumbnail loading dimension by taking the nearest larger power of 2 - // so that there are less variants of the thumbnails to load and cache - // it increases the chance of cache hit when loading similarly sized columns (e.g. on orientation change) - double get requestExtent => pow(2, (log(extent) / log(2)).ceil()).toDouble(); - @override void initState() { super.initState(); @@ -76,13 +69,9 @@ class _RasterImageThumbnailState extends State { void _initProvider() { if (!entry.canDecode) return; - _fastThumbnailProvider = ThumbnailProvider( - ThumbnailProviderKey.fromEntry(entry), - ); + _fastThumbnailProvider = entry.getThumbnail(); if (!entry.isVideo) { - _sizedThumbnailProvider = ThumbnailProvider( - ThumbnailProviderKey.fromEntry(entry, extent: requestExtent), - ); + _sizedThumbnailProvider = entry.getThumbnail(extent: extent); } } @@ -146,22 +135,8 @@ class _RasterImageThumbnailState extends State { : Hero( tag: heroTag, flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) { - ImageProvider heroImageProvider = _fastThumbnailProvider; - if (!entry.isVideo) { - final imageProvider = UriImage( - uri: entry.uri, - mimeType: entry.mimeType, - page: entry.page, - rotationDegrees: entry.rotationDegrees, - isFlipped: entry.isFlipped, - expectedContentLength: entry.sizeBytes, - ); - if (imageCache.statusForKey(imageProvider).keepAlive) { - heroImageProvider = imageProvider; - } - } return TransitionImage( - image: heroImageProvider, + image: entry.getBestThumbnail(extent), animation: animation, ); }, diff --git a/lib/widgets/collection/thumbnail_collection.dart b/lib/widgets/collection/thumbnail_collection.dart index d9a123b67..57b086c66 100644 --- a/lib/widgets/collection/thumbnail_collection.dart +++ b/lib/widgets/collection/thumbnail_collection.dart @@ -36,7 +36,7 @@ class ThumbnailCollection extends StatelessWidget { final ValueNotifier _appBarHeightNotifier = ValueNotifier(0); final ValueNotifier _tileExtentNotifier = ValueNotifier(0); final ValueNotifier _isScrollingNotifier = ValueNotifier(false); - final GlobalKey _scrollableKey = GlobalKey(); + final GlobalKey _scrollableKey = GlobalKey(debugLabel: 'thumbnail-collection-scrollable'); static const columnCountDefault = 4; static const extentMin = 46.0; diff --git a/lib/widgets/common/magnifier/core/core.dart b/lib/widgets/common/magnifier/core/core.dart index 047a1a6eb..a22e2c1ba 100644 --- a/lib/widgets/common/magnifier/core/core.dart +++ b/lib/widgets/common/magnifier/core/core.dart @@ -15,7 +15,6 @@ class MagnifierCore extends StatefulWidget { Key key, @required this.child, @required this.onTap, - @required this.gestureDetectorBehavior, @required this.controller, @required this.scaleStateCycle, @required this.applyScale, @@ -29,7 +28,6 @@ class MagnifierCore extends StatefulWidget { final MagnifierTapCallback onTap; - final HitTestBehavior gestureDetectorBehavior; final bool applyScale; final double panInertia; diff --git a/lib/widgets/common/magnifier/magnifier.dart b/lib/widgets/common/magnifier/magnifier.dart index 22c7d83d6..61fe5a27c 100644 --- a/lib/widgets/common/magnifier/magnifier.dart +++ b/lib/widgets/common/magnifier/magnifier.dart @@ -18,106 +18,64 @@ import 'package:flutter/material.dart'; - added single & double tap position feedback - fixed focus when scaling by double-tap/pinch */ -class Magnifier extends StatefulWidget { +class Magnifier extends StatelessWidget { const Magnifier({ Key key, - @required this.child, - this.childSize, - this.controller, - this.maxScale, - this.minScale, - this.initialScale, - this.scaleStateCycle, + @required this.controller, + @required this.childSize, + this.minScale = const ScaleLevel(factor: .0), + this.maxScale = const ScaleLevel(factor: double.infinity), + this.initialScale = const ScaleLevel(ref: ScaleReference.contained), + this.scaleStateCycle = defaultScaleStateCycle, + this.applyScale = true, this.onTap, - this.gestureDetectorBehavior, - this.applyScale, - }) : super(key: key); + @required this.child, + }) : assert(controller != null), + assert(childSize != null), + assert(minScale != null), + assert(maxScale != null), + assert(initialScale != null), + assert(scaleStateCycle != null), + assert(applyScale != null), + super(key: key); - final Widget child; + final MagnifierController controller; // The size of the custom [child]. This value is used to compute the relation between the child and the container's size to calculate the scale value. final Size childSize; - // Defines the maximum size in which the image will be allowed to assume, it is proportional to the original image size. - final ScaleLevel maxScale; - // Defines the minimum size in which the image will be allowed to assume, it is proportional to the original image size. final ScaleLevel minScale; + // Defines the maximum size in which the image will be allowed to assume, it is proportional to the original image size. + final ScaleLevel maxScale; + // Defines the size the image will assume when the component is initialized, it is proportional to the original image size. final ScaleLevel initialScale; - final MagnifierController controller; final ScaleStateCycle scaleStateCycle; - final MagnifierTapCallback onTap; - final HitTestBehavior gestureDetectorBehavior; final bool applyScale; - - @override - State createState() { - return _MagnifierState(); - } -} - -class _MagnifierState extends State { - bool _controlledController; - MagnifierController _controller; - - Size get childSize => widget.childSize; - - @override - void initState() { - super.initState(); - if (widget.controller == null) { - _controlledController = true; - _controller = MagnifierController(); - } else { - _controlledController = false; - _controller = widget.controller; - } - } - - @override - void didUpdateWidget(covariant Magnifier oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.controller == null) { - if (!_controlledController) { - _controlledController = true; - _controller = MagnifierController(); - } - } else { - _controlledController = false; - _controller = widget.controller; - } - } - - @override - void dispose() { - if (_controlledController) { - _controller.dispose(); - } - super.dispose(); - } + final MagnifierTapCallback onTap; + final Widget child; @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { - _controller.setScaleBoundaries(ScaleBoundaries( - widget.minScale ?? 0.0, - widget.maxScale ?? ScaleLevel(factor: double.infinity), - widget.initialScale ?? ScaleLevel(ref: ScaleReference.contained), - constraints.biggest, - widget.childSize?.isEmpty == true ? constraints.biggest : widget.childSize, + controller.setScaleBoundaries(ScaleBoundaries( + minScale: minScale, + maxScale: maxScale, + initialScale: initialScale, + viewportSize: constraints.biggest, + childSize: childSize?.isEmpty == false ? childSize : constraints.biggest, )); return MagnifierCore( - child: widget.child, - controller: _controller, - scaleStateCycle: widget.scaleStateCycle ?? defaultScaleStateCycle, - onTap: widget.onTap, - gestureDetectorBehavior: widget.gestureDetectorBehavior, - applyScale: widget.applyScale ?? true, + child: child, + controller: controller, + scaleStateCycle: scaleStateCycle, + onTap: onTap, + applyScale: applyScale, ); }, ); diff --git a/lib/widgets/common/magnifier/scale/scale_boundaries.dart b/lib/widgets/common/magnifier/scale/scale_boundaries.dart index b5f565fb4..30615777a 100644 --- a/lib/widgets/common/magnifier/scale/scale_boundaries.dart +++ b/lib/widgets/common/magnifier/scale/scale_boundaries.dart @@ -7,20 +7,22 @@ import 'package:flutter/foundation.dart'; /// Internal class to wrap custom scale boundaries (min, max and initial) /// Also, stores values regarding the two sizes: the container and the child. class ScaleBoundaries { - const ScaleBoundaries( - this._minScale, - this._maxScale, - this._initialScale, - this.viewportSize, - this.childSize, - ); - final ScaleLevel _minScale; final ScaleLevel _maxScale; final ScaleLevel _initialScale; final Size viewportSize; final Size childSize; + const ScaleBoundaries({ + @required ScaleLevel minScale, + @required ScaleLevel maxScale, + @required ScaleLevel initialScale, + @required this.viewportSize, + @required this.childSize, + }) : _minScale = minScale, + _maxScale = maxScale, + _initialScale = initialScale; + double _scaleForLevel(ScaleLevel level) { final factor = level.factor; switch (level.ref) { diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index 4d866db2b..fcdb2e61f 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -40,7 +40,7 @@ class FilterGridPage extends StatelessWidget { final ValueNotifier _appBarHeightNotifier = ValueNotifier(0); final ValueNotifier _tileExtentNotifier = ValueNotifier(0); - final GlobalKey _scrollableKey = GlobalKey(); + final GlobalKey _scrollableKey = GlobalKey(debugLabel: 'filter-grid-page-scrollable'); static const columnCountDefault = 2; static const extentMin = 60.0; diff --git a/lib/widgets/viewer/debug_page.dart b/lib/widgets/viewer/debug_page.dart index 9fbe1403e..768a2883c 100644 --- a/lib/widgets/viewer/debug_page.dart +++ b/lib/widgets/viewer/debug_page.dart @@ -1,6 +1,6 @@ -import 'package:aves/image_providers/thumbnail_provider.dart'; import 'package:aves/image_providers/uri_picture_provider.dart'; import 'package:aves/main.dart'; +import 'package:aves/model/entry_images.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/viewer/debug/db.dart'; @@ -135,18 +135,14 @@ class ViewerDebugPage extends StatelessWidget { Text('Raster (fast)'), Center( child: Image( - image: ThumbnailProvider( - ThumbnailProviderKey.fromEntry(entry), - ), + image: entry.getThumbnail(), ), ), SizedBox(height: 16), Text('Raster ($extent)'), Center( child: Image( - image: ThumbnailProvider( - ThumbnailProviderKey.fromEntry(entry, extent: extent), - ), + image: entry.getThumbnail(extent: extent), ), ), ], diff --git a/lib/widgets/viewer/entry_action_delegate.dart b/lib/widgets/viewer/entry_action_delegate.dart index 1b95e87bd..8aea86ac8 100644 --- a/lib/widgets/viewer/entry_action_delegate.dart +++ b/lib/widgets/viewer/entry_action_delegate.dart @@ -158,7 +158,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { MaterialPageRoute( settings: RouteSettings(name: SourceViewerPage.routeName), builder: (context) => SourceViewerPage( - loader: () => ImageFileService.getImage(entry.uri, entry.mimeType, 0, false).then(utf8.decode), + loader: () => ImageFileService.getSvg(entry.uri, entry.mimeType).then(utf8.decode), ), ), ); diff --git a/lib/widgets/viewer/entry_scroller.dart b/lib/widgets/viewer/entry_scroller.dart index 7f8e124b1..baa4bee88 100644 --- a/lib/widgets/viewer/entry_scroller.dart +++ b/lib/widgets/viewer/entry_scroller.dart @@ -7,6 +7,7 @@ import 'package:aves/widgets/viewer/multipage.dart'; import 'package:aves/widgets/viewer/visual/entry_page_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; +import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; class MultiEntryScroller extends StatefulWidget { @@ -79,16 +80,22 @@ class _MultiEntryScrollerState extends State with AutomaticK ); } - EntryPageView _buildViewer(ImageEntry entry, {MultiPageInfo multiPageInfo, int page}) { - return EntryPageView( - key: Key('imageview'), - mainEntry: entry, - multiPageInfo: multiPageInfo, - page: page, - heroTag: widget.collection.heroTag(entry), - onTap: (_) => widget.onTap?.call(), - videoControllers: widget.videoControllers, - onDisposed: () => widget.onViewDisposed?.call(entry.uri), + Widget _buildViewer(ImageEntry entry, {MultiPageInfo multiPageInfo, int page}) { + return Selector( + selector: (c, mq) => mq.size, + builder: (c, mqSize, child) { + return EntryPageView( + key: Key('imageview'), + mainEntry: entry, + multiPageInfo: multiPageInfo, + page: page, + viewportSize: mqSize, + heroTag: widget.collection.heroTag(entry), + onTap: (_) => widget.onTap?.call(), + videoControllers: widget.videoControllers, + onDisposed: () => widget.onViewDisposed?.call(entry.uri), + ); + }, ); } @@ -150,13 +157,19 @@ class _SingleEntryScrollerState extends State with Automati ); } - EntryPageView _buildViewer({MultiPageInfo multiPageInfo, int page}) { - return EntryPageView( - mainEntry: entry, - multiPageInfo: multiPageInfo, - page: page, - onTap: (_) => widget.onTap?.call(), - videoControllers: widget.videoControllers, + Widget _buildViewer({MultiPageInfo multiPageInfo, int page}) { + return Selector( + selector: (c, mq) => mq.size, + builder: (c, mqSize, child) { + return EntryPageView( + mainEntry: entry, + multiPageInfo: multiPageInfo, + page: page, + viewportSize: mqSize, + onTap: (_) => widget.onTap?.call(), + videoControllers: widget.videoControllers, + ); + }, ); } diff --git a/lib/widgets/viewer/info/maps/marker.dart b/lib/widgets/viewer/info/maps/marker.dart index 9e3a42629..0cd8bc006 100644 --- a/lib/widgets/viewer/info/maps/marker.dart +++ b/lib/widgets/viewer/info/maps/marker.dart @@ -158,7 +158,7 @@ class _MarkerGeneratorWidgetState extends State { type: MaterialType.transparency, child: Stack( children: widget.markers.map((i) { - final key = GlobalKey(); + final key = GlobalKey(debugLabel: 'map-marker-$i'); _globalKeys.add(key); return RepaintBoundary( key: key, diff --git a/lib/widgets/viewer/overlay/video.dart b/lib/widgets/viewer/overlay/video.dart index eee35e659..1d3b287b5 100644 --- a/lib/widgets/viewer/overlay/video.dart +++ b/lib/widgets/viewer/overlay/video.dart @@ -28,7 +28,7 @@ class VideoControlOverlay extends StatefulWidget { } class _VideoControlOverlayState extends State with SingleTickerProviderStateMixin { - final GlobalKey _progressBarKey = GlobalKey(); + final GlobalKey _progressBarKey = GlobalKey(debugLabel: 'video-progress-bar'); bool _playingOnDragStart = false; AnimationController _playPauseAnimation; final List _subscriptions = []; diff --git a/lib/widgets/viewer/panorama_page.dart b/lib/widgets/viewer/panorama_page.dart index eec842376..31eeea732 100644 --- a/lib/widgets/viewer/panorama_page.dart +++ b/lib/widgets/viewer/panorama_page.dart @@ -1,4 +1,4 @@ -import 'package:aves/image_providers/uri_image_provider.dart'; +import 'package:aves/model/entry_images.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/panorama.dart'; import 'package:aves/theme/icons.dart'; @@ -72,14 +72,7 @@ class _PanoramaPageState extends State { ); }, child: Image( - image: UriImage( - uri: entry.uri, - mimeType: entry.mimeType, - page: entry.page, - rotationDegrees: entry.rotationDegrees, - isFlipped: entry.isFlipped, - expectedContentLength: entry.sizeBytes, - ), + image: entry.uriImage, ), ), Positioned( diff --git a/lib/widgets/viewer/printer.dart b/lib/widgets/viewer/printer.dart index f8f27fe6f..c43028cfd 100644 --- a/lib/widgets/viewer/printer.dart +++ b/lib/widgets/viewer/printer.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:aves/image_providers/uri_image_provider.dart'; +import 'package:aves/model/entry_images.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:aves/services/metadata_service.dart'; @@ -47,37 +47,25 @@ class EntryPrinter { final multiPageInfo = await MetadataService.getMultiPageInfo(entry); if (multiPageInfo.pageCount > 1) { for (final kv in multiPageInfo.pages.entries) { - _addPdfPage(await _buildPageImage(page: kv.key)); + final pageEntry = entry.getPageEntry(multiPageInfo: multiPageInfo, page: kv.key); + _addPdfPage(await _buildPageImage(pageEntry)); } } } if (pages.isEmpty) { - _addPdfPage(await _buildPageImage()); + _addPdfPage(await _buildPageImage(entry)); } return pages; } - Future _buildPageImage({int page}) async { - final uri = entry.uri; - final mimeType = entry.mimeType; - final rotationDegrees = entry.rotationDegrees; - final isFlipped = entry.isFlipped; - + Future _buildPageImage(ImageEntry entry) async { if (entry.isSvg) { - final bytes = await ImageFileService.getImage(uri, mimeType, rotationDegrees, isFlipped); + final bytes = await ImageFileService.getSvg(entry.uri, entry.mimeType); if (bytes != null && bytes.isNotEmpty) { return pdf.SvgImage(svg: utf8.decode(bytes)); } } else { - return pdf.Image(await flutterImageProvider( - UriImage( - uri: uri, - mimeType: mimeType, - page: page, - rotationDegrees: rotationDegrees, - isFlipped: isFlipped, - ), - )); + return pdf.Image(await flutterImageProvider(entry.uriImage)); } return null; } diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index bd36e6722..2e376a374 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -26,21 +26,20 @@ class EntryPageView extends StatefulWidget { final ImageEntry entry; final MultiPageInfo multiPageInfo; final int page; + final Size viewportSize; final Object heroTag; final MagnifierTapCallback onTap; final List> videoControllers; final VoidCallback onDisposed; static const decorationCheckSize = 20.0; - static const initialScale = ScaleLevel(ref: ScaleReference.contained); - static const minScale = ScaleLevel(ref: ScaleReference.contained); - static const maxScale = ScaleLevel(factor: 2.0); EntryPageView({ Key key, ImageEntry mainEntry, this.multiPageInfo, this.page, + this.viewportSize, this.heroTag, @required this.onTap, @required this.videoControllers, @@ -59,8 +58,14 @@ class _EntryPageViewState extends State { ImageEntry get entry => widget.entry; + Size get viewportSize => widget.viewportSize; + MagnifierTapCallback get onTap => widget.onTap; + static const initialScale = ScaleLevel(ref: ScaleReference.contained); + static const minScale = ScaleLevel(ref: ScaleReference.contained); + static const maxScale = ScaleLevel(factor: 2.0); + @override void initState() { super.initState(); @@ -87,13 +92,28 @@ class _EntryPageViewState extends State { } void _registerWidget() { - _viewStateNotifier.value = ViewState.zero; + // try to initialize the view state to match magnifier initial state + _viewStateNotifier.value = viewportSize != null + ? ViewState( + Offset.zero, + ScaleBoundaries( + minScale: minScale, + maxScale: maxScale, + initialScale: initialScale, + viewportSize: viewportSize, + childSize: entry.displaySize, + ).initialScale, + viewportSize, + ) + : ViewState.zero; + _magnifierController = MagnifierController(); _subscriptions.add(_magnifierController.stateStream.listen(_onViewStateChanged)); _subscriptions.add(_magnifierController.scaleBoundariesStream.listen(_onViewScaleBoundariesChanged)); } void _unregisterWidget() { + _magnifierController.dispose(); _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); @@ -113,8 +133,7 @@ class _EntryPageViewState extends State { } child ??= ErrorView(onTap: () => onTap?.call(null)); - // no hero for videos, as a typical video first frame is different from its thumbnail - return widget.heroTag != null && !entry.isVideo + return widget.heroTag != null ? Hero( tag: widget.heroTag, transitionOnUserGestures: true, @@ -124,21 +143,13 @@ class _EntryPageViewState extends State { } Widget _buildRasterView() { - return Magnifier( - // key includes size and orientation to refresh when the image is rotated - key: ValueKey('${entry.page}_${entry.rotationDegrees}_${entry.isFlipped}_${entry.width}_${entry.height}_${entry.path}'), - child: TiledImageView( + return _buildMagnifier( + applyScale: false, + child: RasterImageView( entry: entry, viewStateNotifier: _viewStateNotifier, errorBuilder: (context, error, stackTrace) => ErrorView(onTap: () => onTap?.call(null)), ), - childSize: entry.displaySize, - controller: _magnifierController, - maxScale: EntryPageView.maxScale, - minScale: EntryPageView.minScale, - initialScale: EntryPageView.initialScale, - onTap: (c, d, s, childPosition) => onTap?.call(childPosition), - applyScale: false, ); } @@ -146,7 +157,9 @@ class _EntryPageViewState extends State { final background = settings.vectorBackground; final colorFilter = background.isColor ? ColorFilter.mode(background.color, BlendMode.dstOver) : null; - Widget child = Magnifier( + var child = _buildMagnifier( + maxScale: ScaleLevel(factor: double.infinity), + scaleStateCycle: _vectorScaleStateCycle, child: SvgPicture( UriPicture( uri: entry.uri, @@ -154,12 +167,6 @@ class _EntryPageViewState extends State { colorFilter: colorFilter, ), ), - childSize: entry.displaySize, - controller: _magnifierController, - minScale: EntryPageView.minScale, - initialScale: EntryPageView.initialScale, - scaleStateCycle: _vectorScaleStateCycle, - onTap: (c, d, s, childPosition) => onTap?.call(childPosition), ); if (background == EntryBackground.checkered) { @@ -174,19 +181,33 @@ class _EntryPageViewState extends State { Widget _buildVideoView() { final videoController = widget.videoControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2; + if (videoController == null) return SizedBox(); + return _buildMagnifier( + child: VideoView( + entry: entry, + controller: videoController, + ), + ); + } + + Widget _buildMagnifier({ + ScaleLevel maxScale = maxScale, + ScaleStateCycle scaleStateCycle = defaultScaleStateCycle, + bool applyScale = true, + @required Widget child, + }) { return Magnifier( - child: videoController != null - ? AvesVideo( - entry: entry, - controller: videoController, - ) - : SizedBox(), - childSize: entry.displaySize, + // key includes size and orientation to refresh when the image is rotated + key: ValueKey('${entry.page}_${entry.rotationDegrees}_${entry.isFlipped}_${entry.width}_${entry.height}_${entry.path}'), controller: _magnifierController, - maxScale: EntryPageView.maxScale, - minScale: EntryPageView.minScale, - initialScale: EntryPageView.initialScale, + childSize: entry.displaySize, + minScale: minScale, + maxScale: maxScale, + initialScale: initialScale, + scaleStateCycle: scaleStateCycle, + applyScale: applyScale, onTap: (c, d, s, childPosition) => onTap?.call(childPosition), + child: child, ); } diff --git a/lib/widgets/viewer/visual/raster.dart b/lib/widgets/viewer/visual/raster.dart index 9b0262e23..36124bcef 100644 --- a/lib/widgets/viewer/visual/raster.dart +++ b/lib/widgets/viewer/visual/raster.dart @@ -1,12 +1,12 @@ import 'dart:math'; import 'package:aves/image_providers/region_provider.dart'; -import 'package:aves/image_providers/thumbnail_provider.dart'; -import 'package:aves/image_providers/uri_image_provider.dart'; +import 'package:aves/model/entry_images.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings/entry_background.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/utils/math_utils.dart'; +import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/fx/checkered_decoration.dart'; import 'package:aves/widgets/viewer/visual/entry_page_view.dart'; import 'package:aves/widgets/viewer/visual/state.dart'; @@ -14,22 +14,22 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:tuple/tuple.dart'; -class TiledImageView extends StatefulWidget { +class RasterImageView extends StatefulWidget { final ImageEntry entry; final ValueNotifier viewStateNotifier; final ImageErrorWidgetBuilder errorBuilder; - const TiledImageView({ + const RasterImageView({ @required this.entry, @required this.viewStateNotifier, @required this.errorBuilder, }); @override - _TiledImageViewState createState() => _TiledImageViewState(); + _RasterImageViewState createState() => _RasterImageViewState(); } -class _TiledImageViewState extends State { +class _RasterImageViewState extends State { Size _displaySize; bool _isTilingInitialized = false; int _maxSampleSize; @@ -45,42 +45,16 @@ class _TiledImageViewState extends State { bool get useBackground => entry.canHaveAlpha && settings.rasterBackground != EntryBackground.transparent; - // as of panorama v0.3.1, the `Panorama` widget throws on initialization when the image is already resolved - // so we use tiles for panoramas as a workaround to not collide with the `panorama` package resolution - bool get useTiles => entry.canTile && (entry.displaySize.longestSide > 4096 || entry.is360); + ViewState get viewState => viewStateNotifier.value; - ImageProvider get thumbnailProvider => ThumbnailProvider(ThumbnailProviderKey.fromEntry(entry)); + ImageProvider get thumbnailProvider => entry.getBestThumbnail(settings.getTileExtent(CollectionPage.routeName)); ImageProvider get fullImageProvider { - if (useTiles) { + if (entry.useTiles) { assert(_isTilingInitialized); - final displayWidth = _displaySize.width.round(); - final displayHeight = _displaySize.height.round(); - final viewState = viewStateNotifier.value; - final regionRect = _getTileRects( - x: 0, - y: 0, - layerRegionWidth: displayWidth, - layerRegionHeight: displayHeight, - displayWidth: displayWidth, - displayHeight: displayHeight, - scale: viewState.scale, - viewRect: _getViewRect(viewState, displayWidth, displayHeight), - )?.item2; - return RegionProvider(RegionProviderKey.fromEntry( - entry, - sampleSize: _maxSampleSize, - rect: regionRect, - )); + return entry.getRegion(sampleSize: _maxSampleSize); } else { - return UriImage( - uri: entry.uri, - mimeType: entry.mimeType, - page: entry.page, - rotationDegrees: entry.rotationDegrees, - isFlipped: entry.isFlipped, - expectedContentLength: entry.sizeBytes, - ); + return entry.uriImage; } } @@ -92,11 +66,11 @@ class _TiledImageViewState extends State { super.initState(); _displaySize = entry.displaySize; _fullImageListener = ImageStreamListener(_onFullImageCompleted); - if (!useTiles) _registerFullImage(); + if (!entry.useTiles) _registerFullImage(); } @override - void didUpdateWidget(covariant TiledImageView oldWidget) { + void didUpdateWidget(covariant RasterImageView oldWidget) { super.didUpdateWidget(oldWidget); final oldViewState = oldWidget.viewStateNotifier.value; @@ -133,11 +107,12 @@ class _TiledImageViewState extends State { Widget build(BuildContext context) { if (viewStateNotifier == null) return SizedBox.shrink(); + final useTiles = entry.useTiles; return ValueListenableBuilder( valueListenable: viewStateNotifier, builder: (context, viewState, child) { final viewportSize = viewState.viewportSize; - final viewportSized = viewportSize != null; + final viewportSized = viewportSize?.isEmpty == false; if (viewportSized && useTiles && !_isTilingInitialized) _initTiling(viewportSize); return SizedBox.fromSize( @@ -145,9 +120,9 @@ class _TiledImageViewState extends State { child: Stack( alignment: Alignment.center, children: [ - if (useBackground && viewportSized) _buildBackground(viewState), - _buildLoading(viewState), - if (useTiles) ..._getTiles(viewState), + if (useBackground && viewportSized) _buildBackground(), + _buildLoading(), + if (useTiles) ..._getTiles(), if (!useTiles) Image( image: fullImageProvider, @@ -184,7 +159,7 @@ class _TiledImageViewState extends State { _registerFullImage(); } - Widget _buildLoading(ViewState viewState) { + Widget _buildLoading() { return ValueListenableBuilder( valueListenable: _fullImageLoaded, builder: (context, fullImageLoaded, child) { @@ -204,7 +179,7 @@ class _TiledImageViewState extends State { ); } - Widget _buildBackground(ViewState viewState) { + Widget _buildBackground() { final viewportSize = viewState.viewportSize; assert(viewportSize != null); @@ -212,19 +187,30 @@ class _TiledImageViewState extends State { final decorationOffset = ((viewSize - viewportSize) as Offset) / 2 - viewState.position; final decorationSize = applyBoxFit(BoxFit.none, viewSize, viewportSize).source; - Decoration decoration; + Widget child; final background = settings.rasterBackground; if (background == EntryBackground.checkered) { final side = viewportSize.shortestSide; final checkSize = side / ((side / EntryPageView.decorationCheckSize).round()); final offset = ((decorationSize - viewportSize) as Offset) / 2; - decoration = CheckeredDecoration( - checkSize: checkSize, - offset: offset, + child = ValueListenableBuilder( + valueListenable: _fullImageLoaded, + builder: (context, fullImageLoaded, child) { + if (!fullImageLoaded) return SizedBox.shrink(); + + return DecoratedBox( + decoration: CheckeredDecoration( + checkSize: checkSize, + offset: offset, + ), + ); + }, ); } else { - decoration = BoxDecoration( - color: background.color, + child = DecoratedBox( + decoration: BoxDecoration( + color: background.color, + ), ); } return Positioned( @@ -232,36 +218,37 @@ class _TiledImageViewState extends State { top: decorationOffset.dy >= 0 ? decorationOffset.dy : null, width: decorationSize.width, height: decorationSize.height, - child: DecoratedBox( - decoration: decoration, - ), + child: child, ); } - List _getTiles(ViewState viewState) { + List _getTiles() { if (!_isTilingInitialized) return []; final displayWidth = _displaySize.width.round(); final displayHeight = _displaySize.height.round(); - final viewRect = _getViewRect(viewState, displayWidth, displayHeight); + final viewRect = _getViewRect(displayWidth, displayHeight); final scale = viewState.scale; - final tiles = []; + // for the largest sample size (matching the initial scale), the whole image is in view + // so we subsample the whole image without tiling + final fullImageRegionTile = RegionTile( + entry: entry, + tileRect: Rect.fromLTWH(0, 0, displayWidth * scale, displayHeight * scale), + sampleSize: _maxSampleSize, + ); + final tiles = [fullImageRegionTile]; + var minSampleSize = min(_sampleSizeForScale(scale), _maxSampleSize); - for (var sampleSize = _maxSampleSize; sampleSize >= minSampleSize; sampleSize = (sampleSize / 2).floor()) { - // for the largest sample size (matching the initial scale), the whole image is in view - // so we subsample the whole image without tiling - final fullImageRegion = sampleSize == _maxSampleSize; + int nextSampleSize(int sampleSize) => (sampleSize / 2).floor(); + for (var sampleSize = nextSampleSize(_maxSampleSize); sampleSize >= minSampleSize; sampleSize = nextSampleSize(sampleSize)) { final regionSide = (_tileSide * sampleSize).round(); - final layerRegionWidth = fullImageRegion ? displayWidth : regionSide; - final layerRegionHeight = fullImageRegion ? displayHeight : regionSide; - for (var x = 0; x < displayWidth; x += layerRegionWidth) { - for (var y = 0; y < displayHeight; y += layerRegionHeight) { + for (var x = 0; x < displayWidth; x += regionSide) { + for (var y = 0; y < displayHeight; y += regionSide) { final rects = _getTileRects( x: x, y: y, - layerRegionWidth: layerRegionWidth, - layerRegionHeight: layerRegionHeight, + regionSide: regionSide, displayWidth: displayWidth, displayHeight: displayHeight, scale: scale, @@ -281,7 +268,7 @@ class _TiledImageViewState extends State { return tiles; } - Rect _getViewRect(ViewState viewState, int displayWidth, int displayHeight) { + Rect _getViewRect(int displayWidth, int displayHeight) { final scale = viewState.scale; final centerOffset = viewState.position; final viewportSize = viewState.viewportSize; @@ -295,17 +282,16 @@ class _TiledImageViewState extends State { Tuple2> _getTileRects({ @required int x, @required int y, - @required int layerRegionWidth, - @required int layerRegionHeight, + @required int regionSide, @required int displayWidth, @required int displayHeight, @required double scale, @required Rect viewRect, }) { - final nextX = x + layerRegionWidth; - final nextY = y + layerRegionHeight; - final thisRegionWidth = layerRegionWidth - (nextX >= displayWidth ? nextX - displayWidth : 0); - final thisRegionHeight = layerRegionHeight - (nextY >= displayHeight ? nextY - displayHeight : 0); + final nextX = x + regionSide; + final nextY = y + regionSide; + final thisRegionWidth = regionSide - (nextX >= displayWidth ? nextX - displayWidth : 0); + final thisRegionHeight = regionSide - (nextY >= displayHeight ? nextY - displayHeight : 0); final tileRect = Rect.fromLTWH(x * scale, y * scale, thisRegionWidth * scale, thisRegionHeight * scale); // only build visible tiles @@ -348,12 +334,21 @@ class RegionTile extends StatefulWidget { const RegionTile({ @required this.entry, @required this.tileRect, - @required this.regionRect, + this.regionRect, @required this.sampleSize, }); @override _RegionTileState createState() => _RegionTileState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(IntProperty('contentId', entry.contentId)); + properties.add(DiagnosticsProperty('tileRect', tileRect)); + properties.add(DiagnosticsProperty>('regionRect', regionRect)); + properties.add(IntProperty('sampleSize', sampleSize)); + } } class _RegionTileState extends State { @@ -393,11 +388,10 @@ class _RegionTileState extends State { void _initProvider() { if (!entry.canDecode) return; - _provider = RegionProvider(RegionProviderKey.fromEntry( - entry, + _provider = entry.getRegion( sampleSize: widget.sampleSize, - rect: widget.regionRect, - )); + region: widget.regionRect, + ); } void _pauseProvider() => _provider?.pause(); @@ -440,12 +434,4 @@ class _RegionTileState extends State { child: child, ); } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(IntProperty('contentId', widget.entry.contentId)); - properties.add(IntProperty('sampleSize', widget.sampleSize)); - properties.add(DiagnosticsProperty>('regionRect', widget.regionRect)); - } } diff --git a/lib/widgets/viewer/visual/video.dart b/lib/widgets/viewer/visual/video.dart index 66b67b1e2..28e5937e4 100644 --- a/lib/widgets/viewer/visual/video.dart +++ b/lib/widgets/viewer/visual/video.dart @@ -1,26 +1,26 @@ import 'dart:async'; import 'dart:ui'; -import 'package:aves/image_providers/uri_image_provider.dart'; +import 'package:aves/model/entry_images.dart'; import 'package:aves/model/image_entry.dart'; import 'package:flutter/material.dart'; import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; -class AvesVideo extends StatefulWidget { +class VideoView extends StatefulWidget { final ImageEntry entry; final IjkMediaController controller; - const AvesVideo({ + const VideoView({ Key key, @required this.entry, @required this.controller, }) : super(key: key); @override - State createState() => _AvesVideoState(); + State createState() => _VideoViewState(); } -class _AvesVideoState extends State { +class _VideoViewState extends State { final List _subscriptions = []; ImageEntry get entry => widget.entry; @@ -34,7 +34,7 @@ class _AvesVideoState extends State { } @override - void didUpdateWidget(covariant AvesVideo oldWidget) { + void didUpdateWidget(covariant VideoView oldWidget) { super.didUpdateWidget(oldWidget); _unregisterWidget(oldWidget); _registerWidget(widget); @@ -46,11 +46,11 @@ class _AvesVideoState extends State { super.dispose(); } - void _registerWidget(AvesVideo widget) { + void _registerWidget(VideoView widget) { _subscriptions.add(widget.controller.playFinishStream.listen(_onPlayFinish)); } - void _unregisterWidget(AvesVideo widget) { + void _unregisterWidget(VideoView widget) { _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); @@ -98,16 +98,8 @@ class _AvesVideoState extends State { backgroundColor: Colors.transparent, ) : Image( - image: UriImage( - uri: entry.uri, - mimeType: entry.mimeType, - page: entry.page, - rotationDegrees: entry.rotationDegrees, - isFlipped: entry.isFlipped, - expectedContentLength: entry.sizeBytes, - ), - width: entry.width.toDouble(), - height: entry.height.toDouble(), + image: entry.getBestThumbnail(entry.displaySize.longestSide), + fit: BoxFit.contain, ); }); }