thumbnail & transition image loading review

This commit is contained in:
Thibault Deckers 2021-01-21 11:46:33 +09:00
parent 1a50fcc65e
commit c9fa903309
29 changed files with 385 additions and 408 deletions

View file

@ -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<AppIconImageKey> {
const AppIconImage({

View file

@ -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<RegionProviderKey> {
final RegionProviderKey key;
@ -23,7 +22,7 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
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<RegionProviderKey> {
key.rotationDegrees,
key.isFlipped,
key.sampleSize,
key.regionRect,
key.region,
key.imageSize,
page: page,
taskKey: key,
@ -64,21 +63,23 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
}
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<int> regionRect;
final Rectangle<int> 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<int> 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}';
}

View file

@ -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<ThumbnailProviderKey> {
final ThumbnailProviderKey key;
@ -35,14 +34,13 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
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<ThumbnailProviderKey> {
}
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}';
}

View file

@ -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<UriImage> {
@ -15,7 +15,7 @@ class UriImage extends ImageProvider<UriImage> {
const UriImage({
@required this.uri,
@required this.mimeType,
this.page,
@required this.page,
@required this.rotationDegrees,
@required this.isFlipped,
this.expectedContentLength,

View file

@ -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<UriPicture> {
Future<PictureInfo> _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;
}

View file

@ -47,8 +47,8 @@ class _AvesAppState extends State<AvesApp> {
// observers are not registered when using the same list object with different items
// the list itself needs to be reassigned
List<NavigatorObserver> _navigatorObservers = [];
final _newIntentChannel = EventChannel('deckers.thibault/aves/intent');
final _navigatorKey = GlobalKey<NavigatorState>();
final EventChannel _newIntentChannel = EventChannel('deckers.thibault/aves/intent');
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey(debugLabel: 'app-navigator');
static const accentColor = Colors.indigoAccent;

View file

@ -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,

View file

@ -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<int> region}) => RegionProvider(getRegionProviderKey(sampleSize, region));
RegionProviderKey getRegionProviderKey(int sampleSize, Rectangle<int> region) {
return RegionProviderKey(
uri: uri,
mimeType: mimeType,
page: page,
rotationDegrees: rotationDegrees,
isFlipped: isFlipped,
sampleSize: sampleSize,
region: region ?? Rectangle<int>(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();
}
}

View file

@ -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);

View file

@ -26,18 +26,18 @@ class AppShortcutService {
return false;
}
static Future<void> pin(String label, ImageEntry iconEntry, Set<CollectionFilter> filters) async {
static Future<void> pin(String label, ImageEntry entry, Set<CollectionFilter> 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 {

View file

@ -69,6 +69,21 @@ class ImageFileService {
return null;
}
static Future<Uint8List> getSvg(
String uri,
String mimeType, {
int expectedContentLength,
BytesReceivedCallback onBytesReceived,
}) =>
getImage(
uri,
mimeType,
0,
false,
expectedContentLength: expectedContentLength,
onBytesReceived: onBytesReceived,
);
static Future<Uint8List> getImage(
String uri,
String mimeType,
@ -155,15 +170,14 @@ class ImageFileService {
);
}
static Future<Uint8List> getThumbnail(
String uri,
String mimeType,
int dateModifiedSecs,
int rotationDegrees,
bool isFlipped,
double width,
double height, {
int page,
static Future<Uint8List> 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,
);
}

View file

@ -17,7 +17,7 @@ class SvgMetadataService {
static Future<Size> 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;

View file

@ -25,7 +25,7 @@ class FilterBar extends StatefulWidget implements PreferredSizeWidget {
}
class _FilterBarState extends State<FilterBar> {
final GlobalKey<AnimatedListState> _animatedListKey = GlobalKey();
final GlobalKey<AnimatedListState> _animatedListKey = GlobalKey(debugLabel: 'filter-bar-animated-list');
CollectionFilter _userRemovedFilter;
@override

View file

@ -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<RasterImageThumbnail> {
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<RasterImageThumbnail> {
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<RasterImageThumbnail> {
: 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,
);
},

View file

@ -36,7 +36,7 @@ class ThumbnailCollection extends StatelessWidget {
final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0);
final ValueNotifier<double> _tileExtentNotifier = ValueNotifier(0);
final ValueNotifier<bool> _isScrollingNotifier = ValueNotifier(false);
final GlobalKey _scrollableKey = GlobalKey();
final GlobalKey _scrollableKey = GlobalKey(debugLabel: 'thumbnail-collection-scrollable');
static const columnCountDefault = 4;
static const extentMin = 46.0;

View file

@ -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;

View file

@ -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<StatefulWidget> createState() {
return _MagnifierState();
}
}
class _MagnifierState extends State<Magnifier> {
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,
);
},
);

View file

@ -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) {

View file

@ -40,7 +40,7 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0);
final ValueNotifier<double> _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;

View file

@ -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),
),
),
],

View file

@ -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),
),
),
);

View file

@ -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<MultiEntryScroller> 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<MediaQueryData, Size>(
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<SingleEntryScroller> 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<MediaQueryData, Size>(
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,
);
},
);
}

View file

@ -158,7 +158,7 @@ class _MarkerGeneratorWidgetState extends State<MarkerGeneratorWidget> {
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,

View file

@ -28,7 +28,7 @@ class VideoControlOverlay extends StatefulWidget {
}
class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTickerProviderStateMixin {
final GlobalKey _progressBarKey = GlobalKey();
final GlobalKey _progressBarKey = GlobalKey(debugLabel: 'video-progress-bar');
bool _playingOnDragStart = false;
AnimationController _playPauseAnimation;
final List<StreamSubscription> _subscriptions = [];

View file

@ -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<PanoramaPage> {
);
},
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(

View file

@ -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<pdf.Widget> _buildPageImage({int page}) async {
final uri = entry.uri;
final mimeType = entry.mimeType;
final rotationDegrees = entry.rotationDegrees;
final isFlipped = entry.isFlipped;
Future<pdf.Widget> _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;
}

View file

@ -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<Tuple2<String, IjkMediaController>> 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<EntryPageView> {
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<EntryPageView> {
}
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<EntryPageView> {
}
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<EntryPageView> {
}
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<EntryPageView> {
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<EntryPageView> {
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<EntryPageView> {
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,
);
}

View file

@ -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<ViewState> 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<TiledImageView> {
class _RasterImageViewState extends State<RasterImageView> {
Size _displaySize;
bool _isTilingInitialized = false;
int _maxSampleSize;
@ -45,42 +45,16 @@ class _TiledImageViewState extends State<TiledImageView> {
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<TiledImageView> {
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<TiledImageView> {
Widget build(BuildContext context) {
if (viewStateNotifier == null) return SizedBox.shrink();
final useTiles = entry.useTiles;
return ValueListenableBuilder<ViewState>(
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<TiledImageView> {
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<TiledImageView> {
_registerFullImage();
}
Widget _buildLoading(ViewState viewState) {
Widget _buildLoading() {
return ValueListenableBuilder(
valueListenable: _fullImageLoaded,
builder: (context, fullImageLoaded, child) {
@ -204,7 +179,7 @@ class _TiledImageViewState extends State<TiledImageView> {
);
}
Widget _buildBackground(ViewState viewState) {
Widget _buildBackground() {
final viewportSize = viewState.viewportSize;
assert(viewportSize != null);
@ -212,19 +187,30 @@ class _TiledImageViewState extends State<TiledImageView> {
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<TiledImageView> {
top: decorationOffset.dy >= 0 ? decorationOffset.dy : null,
width: decorationSize.width,
height: decorationSize.height,
child: DecoratedBox(
decoration: decoration,
),
child: child,
);
}
List<Widget> _getTiles(ViewState viewState) {
List<Widget> _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 = <RegionTile>[];
// 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<TiledImageView> {
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<TiledImageView> {
Tuple2<Rect, Rectangle<int>> _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<Rect>('tileRect', tileRect));
properties.add(DiagnosticsProperty<Rectangle<int>>('regionRect', regionRect));
properties.add(IntProperty('sampleSize', sampleSize));
}
}
class _RegionTileState extends State<RegionTile> {
@ -393,11 +388,10 @@ class _RegionTileState extends State<RegionTile> {
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<RegionTile> {
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<Rectangle<int>>('regionRect', widget.regionRect));
}
}

View file

@ -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<StatefulWidget> createState() => _AvesVideoState();
State<StatefulWidget> createState() => _VideoViewState();
}
class _AvesVideoState extends State<AvesVideo> {
class _VideoViewState extends State<VideoView> {
final List<StreamSubscription> _subscriptions = [];
ImageEntry get entry => widget.entry;
@ -34,7 +34,7 @@ class _AvesVideoState extends State<AvesVideo> {
}
@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<AvesVideo> {
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<AvesVideo> {
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,
);
});
}