raster image background
This commit is contained in:
parent
5e7c85597a
commit
07b9db6750
9 changed files with 345 additions and 198 deletions
|
@ -63,6 +63,8 @@ class ImageEntry {
|
||||||
|
|
||||||
bool get canDecode => !undecodable.contains(mimeType);
|
bool get canDecode => !undecodable.contains(mimeType);
|
||||||
|
|
||||||
|
bool get canHaveAlpha => MimeTypes.alphaImages.contains(mimeType);
|
||||||
|
|
||||||
ImageEntry copyWith({
|
ImageEntry copyWith({
|
||||||
@required String uri,
|
@required String uri,
|
||||||
@required String path,
|
@required String path,
|
||||||
|
|
|
@ -55,6 +55,7 @@ class Settings extends ChangeNotifier {
|
||||||
static const coordinateFormatKey = 'coordinates_format';
|
static const coordinateFormatKey = 'coordinates_format';
|
||||||
|
|
||||||
// rendering
|
// rendering
|
||||||
|
static const rasterBackgroundKey = 'raster_background';
|
||||||
static const vectorBackgroundKey = 'vector_background';
|
static const vectorBackgroundKey = 'vector_background';
|
||||||
|
|
||||||
// search
|
// search
|
||||||
|
@ -185,6 +186,10 @@ class Settings extends ChangeNotifier {
|
||||||
|
|
||||||
// rendering
|
// rendering
|
||||||
|
|
||||||
|
EntryBackground get rasterBackground => getEnumOrDefault(rasterBackgroundKey, EntryBackground.transparent, EntryBackground.values);
|
||||||
|
|
||||||
|
set rasterBackground(EntryBackground newValue) => setAndNotify(rasterBackgroundKey, newValue.toString());
|
||||||
|
|
||||||
EntryBackground get vectorBackground => getEnumOrDefault(vectorBackgroundKey, EntryBackground.white, EntryBackground.values);
|
EntryBackground get vectorBackground => getEnumOrDefault(vectorBackgroundKey, EntryBackground.white, EntryBackground.values);
|
||||||
|
|
||||||
set vectorBackground(EntryBackground newValue) => setAndNotify(vectorBackgroundKey, newValue.toString());
|
set vectorBackground(EntryBackground newValue) => setAndNotify(vectorBackgroundKey, newValue.toString());
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
class MimeTypes {
|
class MimeTypes {
|
||||||
static const anyImage = 'image/*';
|
static const anyImage = 'image/*';
|
||||||
|
|
||||||
|
static const bmp = 'image/bmp';
|
||||||
static const gif = 'image/gif';
|
static const gif = 'image/gif';
|
||||||
static const heic = 'image/heic';
|
static const heic = 'image/heic';
|
||||||
static const heif = 'image/heif';
|
static const heif = 'image/heif';
|
||||||
|
static const ico = 'image/x-icon';
|
||||||
static const jpeg = 'image/jpeg';
|
static const jpeg = 'image/jpeg';
|
||||||
static const png = 'image/png';
|
static const png = 'image/png';
|
||||||
static const svg = 'image/svg+xml';
|
static const svg = 'image/svg+xml';
|
||||||
|
static const tiff = 'image/tiff';
|
||||||
static const webp = 'image/webp';
|
static const webp = 'image/webp';
|
||||||
|
|
||||||
static const tiff = 'image/tiff';
|
|
||||||
static const psd = 'image/vnd.adobe.photoshop';
|
static const psd = 'image/vnd.adobe.photoshop';
|
||||||
|
|
||||||
static const arw = 'image/x-sony-arw';
|
static const arw = 'image/x-sony-arw';
|
||||||
|
@ -40,6 +42,10 @@ class MimeTypes {
|
||||||
static const mp4 = 'video/mp4';
|
static const mp4 = 'video/mp4';
|
||||||
|
|
||||||
// groups
|
// groups
|
||||||
|
|
||||||
|
// formats that support transparency
|
||||||
|
static const List<String> alphaImages = [bmp, gif, ico, png, svg, tiff, webp];
|
||||||
|
|
||||||
static const List<String> rawImages = [arw, cr2, crw, dcr, dng, erf, k25, kdc, mrw, nef, nrw, orf, pef, raf, raw, rw2, sr2, srf, srw, x3f];
|
static const List<String> rawImages = [arw, cr2, crw, dcr, dng, erf, k25, kdc, mrw, nef, nrw, orf, pef, raf, raw, rw2, sr2, srf, srw, x3f];
|
||||||
|
|
||||||
static bool isImage(String mimeType) => mimeType.startsWith('image');
|
static bool isImage(String mimeType) => mimeType.startsWith('image');
|
||||||
|
|
|
@ -15,6 +15,7 @@ class XMP {
|
||||||
'exifEX': 'Exif Ex',
|
'exifEX': 'Exif Ex',
|
||||||
'GettyImagesGIFT': 'Getty Images',
|
'GettyImagesGIFT': 'Getty Images',
|
||||||
'GIMP': 'GIMP',
|
'GIMP': 'GIMP',
|
||||||
|
'GCamera': 'Google Camera',
|
||||||
'GFocus': 'Google Focus',
|
'GFocus': 'Google Focus',
|
||||||
'GPano': 'Google Panorama',
|
'GPano': 'Google Panorama',
|
||||||
'illustrator': 'Illustrator',
|
'illustrator': 'Illustrator',
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:aves/image_providers/thumbnail_provider.dart';
|
|
||||||
import 'package:aves/image_providers/uri_picture_provider.dart';
|
import 'package:aves/image_providers/uri_picture_provider.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/settings/entry_background.dart';
|
import 'package:aves/model/settings/entry_background.dart';
|
||||||
|
@ -30,6 +28,8 @@ class ImageView extends StatefulWidget {
|
||||||
final List<Tuple2<String, IjkMediaController>> videoControllers;
|
final List<Tuple2<String, IjkMediaController>> videoControllers;
|
||||||
final VoidCallback onDisposed;
|
final VoidCallback onDisposed;
|
||||||
|
|
||||||
|
static const decorationCheckSize = 20.0;
|
||||||
|
|
||||||
const ImageView({
|
const ImageView({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.entry,
|
@required this.entry,
|
||||||
|
@ -56,8 +56,6 @@ class _ImageViewState extends State<ImageView> {
|
||||||
|
|
||||||
MagnifierTapCallback get onTap => widget.onTap;
|
MagnifierTapCallback get onTap => widget.onTap;
|
||||||
|
|
||||||
static const decorationCheckSize = 20.0;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
@ -98,25 +96,6 @@ class _ImageViewState extends State<ImageView> {
|
||||||
: child;
|
: child;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageProvider get fastThumbnailProvider => ThumbnailProvider(ThumbnailProviderKey.fromEntry(entry));
|
|
||||||
|
|
||||||
// this loading builder shows a transition image until the final image is ready
|
|
||||||
// if the image is already in the cache it will show the final image, otherwise the thumbnail
|
|
||||||
// in any case, we should use `Center` + `AspectRatio` + `BoxFit.fill` so that the transition image
|
|
||||||
// is laid the same way as the final image when `contained`
|
|
||||||
Widget _loadingBuilder(BuildContext context, ImageProvider imageProvider) {
|
|
||||||
return Center(
|
|
||||||
child: AspectRatio(
|
|
||||||
// enforce original aspect ratio, as some thumbnails aspect ratios slightly differ
|
|
||||||
aspectRatio: entry.displayAspectRatio,
|
|
||||||
child: Image(
|
|
||||||
image: imageProvider,
|
|
||||||
fit: BoxFit.fill,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildRasterView() {
|
Widget _buildRasterView() {
|
||||||
return Magnifier(
|
return Magnifier(
|
||||||
// key includes size and orientation to refresh when the image is rotated
|
// key includes size and orientation to refresh when the image is rotated
|
||||||
|
@ -124,7 +103,6 @@ class _ImageViewState extends State<ImageView> {
|
||||||
child: TiledImageView(
|
child: TiledImageView(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
viewStateNotifier: _viewStateNotifier,
|
viewStateNotifier: _viewStateNotifier,
|
||||||
baseChild: _loadingBuilder(context, fastThumbnailProvider),
|
|
||||||
errorBuilder: (context, error, stackTrace) => ErrorChild(onTap: () => onTap?.call(null)),
|
errorBuilder: (context, error, stackTrace) => ErrorChild(onTap: () => onTap?.call(null)),
|
||||||
),
|
),
|
||||||
childSize: entry.displaySize,
|
childSize: entry.displaySize,
|
||||||
|
@ -165,11 +143,11 @@ class _ImageViewState extends State<ImageView> {
|
||||||
if (viewportSize == null) return child;
|
if (viewportSize == null) return child;
|
||||||
|
|
||||||
final side = viewportSize.shortestSide;
|
final side = viewportSize.shortestSide;
|
||||||
final checkSize = side / ((side / decorationCheckSize).round());
|
final checkSize = side / ((side / ImageView.decorationCheckSize).round());
|
||||||
|
|
||||||
final viewSize = entry.displaySize * viewState.scale;
|
final viewSize = entry.displaySize * viewState.scale;
|
||||||
final decorationSize = Size(min(viewSize.width, viewportSize.width), min(viewSize.height, viewportSize.height));
|
final decorationSize = applyBoxFit(BoxFit.none, viewSize, viewportSize).source;
|
||||||
final offset = Offset(decorationSize.width - viewportSize.width, decorationSize.height - viewportSize.height) / 2;
|
final offset = ((decorationSize - viewportSize) as Offset) / 2;
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
|
|
|
@ -1,23 +1,26 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/image_providers/region_provider.dart';
|
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/image_providers/uri_image_provider.dart';
|
||||||
import 'package:aves/model/image_entry.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/utils/math_utils.dart';
|
||||||
|
import 'package:aves/widgets/common/fx/checkered_decoration.dart';
|
||||||
import 'package:aves/widgets/fullscreen/image_view.dart';
|
import 'package:aves/widgets/fullscreen/image_view.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
class TiledImageView extends StatefulWidget {
|
class TiledImageView extends StatefulWidget {
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
final ValueNotifier<ViewState> viewStateNotifier;
|
final ValueNotifier<ViewState> viewStateNotifier;
|
||||||
final Widget baseChild;
|
|
||||||
final ImageErrorWidgetBuilder errorBuilder;
|
final ImageErrorWidgetBuilder errorBuilder;
|
||||||
|
|
||||||
const TiledImageView({
|
const TiledImageView({
|
||||||
@required this.entry,
|
@required this.entry,
|
||||||
@required this.viewStateNotifier,
|
@required this.viewStateNotifier,
|
||||||
@required this.baseChild,
|
|
||||||
@required this.errorBuilder,
|
@required this.errorBuilder,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -26,159 +29,244 @@ class TiledImageView extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TiledImageViewState extends State<TiledImageView> {
|
class _TiledImageViewState extends State<TiledImageView> {
|
||||||
bool _initialized = false;
|
bool _isTilingInitialized = false;
|
||||||
double _tileSide, _initialScale;
|
|
||||||
int _maxSampleSize;
|
int _maxSampleSize;
|
||||||
Matrix4 _transform;
|
double _tileSide;
|
||||||
|
Matrix4 _tileTransform;
|
||||||
|
ImageStream _fullImageStream;
|
||||||
|
ImageStreamListener _fullImageListener;
|
||||||
|
final ValueNotifier<bool> _fullImageLoaded = ValueNotifier(false);
|
||||||
|
|
||||||
ImageEntry get entry => widget.entry;
|
ImageEntry get entry => widget.entry;
|
||||||
|
|
||||||
ValueNotifier<ViewState> get viewStateNotifier => widget.viewStateNotifier;
|
ValueNotifier<ViewState> get viewStateNotifier => widget.viewStateNotifier;
|
||||||
|
|
||||||
|
bool get useBackground => entry.canHaveAlpha && settings.rasterBackground != EntryBackground.transparent;
|
||||||
|
|
||||||
bool get useTiles => entry.canTile && (entry.width > 4096 || entry.height > 4096);
|
bool get useTiles => entry.canTile && (entry.width > 4096 || entry.height > 4096);
|
||||||
|
|
||||||
ImageProvider get fullImage => UriImage(
|
ImageProvider get thumbnailProvider => ThumbnailProvider(ThumbnailProviderKey.fromEntry(entry));
|
||||||
|
|
||||||
|
ImageProvider get fullImageProvider {
|
||||||
|
if (useTiles) {
|
||||||
|
assert(_isTilingInitialized);
|
||||||
|
final displayWidth = entry.displaySize.width.round();
|
||||||
|
final displayHeight = entry.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,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
return UriImage(
|
||||||
uri: entry.uri,
|
uri: entry.uri,
|
||||||
mimeType: entry.mimeType,
|
mimeType: entry.mimeType,
|
||||||
rotationDegrees: entry.rotationDegrees,
|
rotationDegrees: entry.rotationDegrees,
|
||||||
isFlipped: entry.isFlipped,
|
isFlipped: entry.isFlipped,
|
||||||
expectedContentLength: entry.sizeBytes,
|
expectedContentLength: entry.sizeBytes,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// magic number used to derive sample size from scale
|
// magic number used to derive sample size from scale
|
||||||
static const scaleFactor = 2.0;
|
static const scaleFactor = 2.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fullImageListener = ImageStreamListener(_onFullImageCompleted);
|
||||||
|
if (!useTiles) _registerFullImage();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(TiledImageView oldWidget) {
|
void didUpdateWidget(TiledImageView oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
|
|
||||||
final oldViewState = oldWidget.viewStateNotifier.value;
|
final oldViewState = oldWidget.viewStateNotifier.value;
|
||||||
final viewState = widget.viewStateNotifier.value;
|
final viewState = widget.viewStateNotifier.value;
|
||||||
if (oldViewState.viewportSize != viewState.viewportSize || oldWidget.entry.displaySize != widget.entry.displaySize) {
|
if (oldWidget.entry != widget.entry || oldViewState.viewportSize != viewState.viewportSize) {
|
||||||
_initialized = false;
|
_isTilingInitialized = false;
|
||||||
|
_fullImageLoaded.value = false;
|
||||||
|
_unregisterFullImage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initFromViewport(Size viewportSize) {
|
@override
|
||||||
final displaySize = entry.displaySize;
|
void dispose() {
|
||||||
_tileSide = viewportSize.shortestSide * scaleFactor;
|
_unregisterFullImage();
|
||||||
_initialScale = min(viewportSize.width / displaySize.width, viewportSize.height / displaySize.height);
|
super.dispose();
|
||||||
_maxSampleSize = _sampleSizeForScale(_initialScale);
|
|
||||||
|
|
||||||
final rotationDegrees = entry.rotationDegrees;
|
|
||||||
final isFlipped = entry.isFlipped;
|
|
||||||
_transform = null;
|
|
||||||
if (rotationDegrees != 0 || isFlipped) {
|
|
||||||
_transform = Matrix4.identity()
|
|
||||||
..translate(entry.width / 2.0, entry.height / 2.0)
|
|
||||||
..scale(isFlipped ? -1.0 : 1.0, 1.0, 1.0)
|
|
||||||
..rotateZ(-toRadians(rotationDegrees.toDouble()))
|
|
||||||
..translate(-displaySize.width / 2.0, -displaySize.height / 2.0);
|
|
||||||
}
|
}
|
||||||
_initialized = true;
|
|
||||||
|
void _registerFullImage() {
|
||||||
|
_fullImageStream = fullImageProvider.resolve(ImageConfiguration.empty);
|
||||||
|
_fullImageStream.addListener(_fullImageListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _unregisterFullImage() {
|
||||||
|
_fullImageStream?.removeListener(_fullImageListener);
|
||||||
|
_fullImageStream = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onFullImageCompleted(ImageInfo image, bool synchronousCall) {
|
||||||
|
_unregisterFullImage();
|
||||||
|
_fullImageLoaded.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (viewStateNotifier == null) return SizedBox.shrink();
|
if (viewStateNotifier == null) return SizedBox.shrink();
|
||||||
|
|
||||||
final displayWidth = entry.displaySize.width.round();
|
|
||||||
final displayHeight = entry.displaySize.height.round();
|
|
||||||
|
|
||||||
return ValueListenableBuilder<ViewState>(
|
return ValueListenableBuilder<ViewState>(
|
||||||
valueListenable: viewStateNotifier,
|
valueListenable: viewStateNotifier,
|
||||||
builder: (context, viewState, child) {
|
builder: (context, viewState, child) {
|
||||||
final viewportSize = viewState.viewportSize;
|
final viewportSize = viewState.viewportSize;
|
||||||
if (viewportSize == null) return SizedBox.shrink();
|
final viewportSized = viewportSize != null;
|
||||||
if (!_initialized) _initFromViewport(viewportSize);
|
if (viewportSized && useTiles && !_isTilingInitialized) _initTiling(viewportSize);
|
||||||
|
|
||||||
var scale = viewState.scale;
|
return SizedBox.fromSize(
|
||||||
if (scale == 0.0) {
|
size: entry.displaySize * viewState.scale,
|
||||||
// for initial scale as `contained`
|
child: Stack(
|
||||||
scale = _initialScale;
|
alignment: Alignment.center,
|
||||||
}
|
children: [
|
||||||
final scaledSize = entry.displaySize * scale;
|
if (useBackground && viewportSized) _buildBackground(viewState),
|
||||||
final loading = SizedBox(
|
_buildLoading(viewState),
|
||||||
width: scaledSize.width,
|
if (useTiles) ..._getTiles(viewState),
|
||||||
height: scaledSize.height,
|
if (!useTiles)
|
||||||
child: widget.baseChild,
|
|
||||||
);
|
|
||||||
|
|
||||||
List<Widget> children;
|
|
||||||
if (useTiles) {
|
|
||||||
children = [
|
|
||||||
loading,
|
|
||||||
..._getTiles(viewState, displayWidth, displayHeight, scale),
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
children = [
|
|
||||||
if (!imageCache.statusForKey(fullImage).keepAlive) loading,
|
|
||||||
Image(
|
Image(
|
||||||
image: fullImage,
|
image: fullImageProvider,
|
||||||
gaplessPlayback: true,
|
gaplessPlayback: true,
|
||||||
errorBuilder: widget.errorBuilder,
|
errorBuilder: widget.errorBuilder,
|
||||||
width: scaledSize.width,
|
width: (entry.displaySize * viewState.scale).width,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
filterQuality: FilterQuality.medium,
|
filterQuality: FilterQuality.medium,
|
||||||
)
|
),
|
||||||
];
|
],
|
||||||
}
|
),
|
||||||
|
|
||||||
return Stack(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
children: children,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<RegionTile> _getTiles(ViewState viewState, int displayWidth, int displayHeight, double scale) {
|
void _initTiling(Size viewportSize) {
|
||||||
final centerOffset = viewState.position;
|
final displaySize = entry.displaySize;
|
||||||
final viewportSize = viewState.viewportSize;
|
_tileSide = viewportSize.shortestSide * scaleFactor;
|
||||||
final viewOrigin = Offset(
|
// scale for initial state `contained`
|
||||||
((displayWidth * scale - viewportSize.width) / 2 - centerOffset.dx),
|
final containedScale = min(viewportSize.width / displaySize.width, viewportSize.height / displaySize.height);
|
||||||
((displayHeight * scale - viewportSize.height) / 2 - centerOffset.dy),
|
_maxSampleSize = _sampleSizeForScale(containedScale);
|
||||||
|
|
||||||
|
final rotationDegrees = entry.rotationDegrees;
|
||||||
|
final isFlipped = entry.isFlipped;
|
||||||
|
_tileTransform = null;
|
||||||
|
if (rotationDegrees != 0 || isFlipped) {
|
||||||
|
_tileTransform = Matrix4.identity()
|
||||||
|
..translate(entry.width / 2.0, entry.height / 2.0)
|
||||||
|
..scale(isFlipped ? -1.0 : 1.0, 1.0, 1.0)
|
||||||
|
..rotateZ(-toRadians(rotationDegrees.toDouble()))
|
||||||
|
..translate(-displaySize.width / 2.0, -displaySize.height / 2.0);
|
||||||
|
}
|
||||||
|
_isTilingInitialized = true;
|
||||||
|
_registerFullImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLoading(ViewState viewState) {
|
||||||
|
return ValueListenableBuilder(
|
||||||
|
valueListenable: _fullImageLoaded,
|
||||||
|
builder: (context, fullImageLoaded, child) {
|
||||||
|
if (fullImageLoaded) return SizedBox.shrink();
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: AspectRatio(
|
||||||
|
// enforce original aspect ratio, as some thumbnails aspect ratios slightly differ
|
||||||
|
aspectRatio: entry.displayAspectRatio,
|
||||||
|
child: Image(
|
||||||
|
image: thumbnailProvider,
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
final viewRect = viewOrigin & viewportSize;
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBackground(ViewState viewState) {
|
||||||
|
final viewportSize = viewState.viewportSize;
|
||||||
|
assert(viewportSize != null);
|
||||||
|
|
||||||
|
final viewSize = entry.displaySize * viewState.scale;
|
||||||
|
final decorationOffset = ((viewSize - viewportSize) as Offset) / 2 - viewState.position;
|
||||||
|
final decorationSize = applyBoxFit(BoxFit.none, viewSize, viewportSize).source;
|
||||||
|
|
||||||
|
Decoration decoration;
|
||||||
|
final background = settings.rasterBackground;
|
||||||
|
if (background == EntryBackground.checkered) {
|
||||||
|
final side = viewportSize.shortestSide;
|
||||||
|
final checkSize = side / ((side / ImageView.decorationCheckSize).round());
|
||||||
|
final offset = ((decorationSize - viewportSize) as Offset) / 2;
|
||||||
|
decoration = CheckeredDecoration(
|
||||||
|
checkSize: checkSize,
|
||||||
|
offset: offset,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
decoration = BoxDecoration(
|
||||||
|
color: background.color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Positioned(
|
||||||
|
left: decorationOffset.dx >= 0 ? decorationOffset.dx : null,
|
||||||
|
top: decorationOffset.dy >= 0 ? decorationOffset.dy : null,
|
||||||
|
width: decorationSize.width,
|
||||||
|
height: decorationSize.height,
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: decoration,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _getTiles(ViewState viewState) {
|
||||||
|
if (!_isTilingInitialized) return [];
|
||||||
|
|
||||||
|
final displayWidth = entry.displaySize.width.round();
|
||||||
|
final displayHeight = entry.displaySize.height.round();
|
||||||
|
final viewRect = _getViewRect(viewState, displayWidth, displayHeight);
|
||||||
|
final scale = viewState.scale;
|
||||||
|
|
||||||
final tiles = <RegionTile>[];
|
final tiles = <RegionTile>[];
|
||||||
var minSampleSize = min(_sampleSizeForScale(scale), _maxSampleSize);
|
var minSampleSize = min(_sampleSizeForScale(scale), _maxSampleSize);
|
||||||
for (var sampleSize = _maxSampleSize; sampleSize >= minSampleSize; sampleSize = (sampleSize / 2).floor()) {
|
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
|
// for the largest sample size (matching the initial scale), the whole image is in view
|
||||||
// so we subsample the whole image instead of splitting it in tiles
|
// so we subsample the whole image without tiling
|
||||||
final useTiles = sampleSize != _maxSampleSize;
|
final fullImageRegion = sampleSize == _maxSampleSize;
|
||||||
final regionSide = (_tileSide * sampleSize).round();
|
final regionSide = (_tileSide * sampleSize).round();
|
||||||
final layerRegionWidth = useTiles ? regionSide : displayWidth;
|
final layerRegionWidth = fullImageRegion ? displayWidth : regionSide;
|
||||||
final layerRegionHeight = useTiles ? regionSide : displayHeight;
|
final layerRegionHeight = fullImageRegion ? displayHeight : regionSide;
|
||||||
for (var x = 0; x < displayWidth; x += layerRegionWidth) {
|
for (var x = 0; x < displayWidth; x += layerRegionWidth) {
|
||||||
for (var y = 0; y < displayHeight; y += layerRegionHeight) {
|
for (var y = 0; y < displayHeight; y += layerRegionHeight) {
|
||||||
final nextX = x + layerRegionWidth;
|
final rects = _getTileRects(
|
||||||
final nextY = y + layerRegionHeight;
|
x: x,
|
||||||
final thisRegionWidth = layerRegionWidth - (nextX >= displayWidth ? nextX - displayWidth : 0);
|
y: y,
|
||||||
final thisRegionHeight = layerRegionHeight - (nextY >= displayHeight ? nextY - displayHeight : 0);
|
layerRegionWidth: layerRegionWidth,
|
||||||
final tileRect = Rect.fromLTWH(x * scale, y * scale, thisRegionWidth * scale, thisRegionHeight * scale);
|
layerRegionHeight: layerRegionHeight,
|
||||||
|
displayWidth: displayWidth,
|
||||||
// only build visible tiles
|
displayHeight: displayHeight,
|
||||||
if (viewRect.overlaps(tileRect)) {
|
scale: scale,
|
||||||
Rectangle<int> regionRect;
|
viewRect: viewRect,
|
||||||
|
|
||||||
if (_transform != null) {
|
|
||||||
// apply EXIF orientation
|
|
||||||
final regionRectDouble = Rect.fromLTWH(x.toDouble(), y.toDouble(), thisRegionWidth.toDouble(), thisRegionHeight.toDouble());
|
|
||||||
final tl = MatrixUtils.transformPoint(_transform, regionRectDouble.topLeft);
|
|
||||||
final br = MatrixUtils.transformPoint(_transform, regionRectDouble.bottomRight);
|
|
||||||
regionRect = Rectangle<int>.fromPoints(
|
|
||||||
Point<int>(tl.dx.round(), tl.dy.round()),
|
|
||||||
Point<int>(br.dx.round(), br.dy.round()),
|
|
||||||
);
|
);
|
||||||
} else {
|
if (rects != null) {
|
||||||
regionRect = Rectangle<int>(x, y, thisRegionWidth, thisRegionHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
tiles.add(RegionTile(
|
tiles.add(RegionTile(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
tileRect: tileRect,
|
tileRect: rects.item1,
|
||||||
regionRect: regionRect,
|
regionRect: rects.item2,
|
||||||
sampleSize: sampleSize,
|
sampleSize: sampleSize,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -188,6 +276,52 @@ class _TiledImageViewState extends State<TiledImageView> {
|
||||||
return tiles;
|
return tiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rect _getViewRect(ViewState viewState, int displayWidth, int displayHeight) {
|
||||||
|
final scale = viewState.scale;
|
||||||
|
final centerOffset = viewState.position;
|
||||||
|
final viewportSize = viewState.viewportSize;
|
||||||
|
final viewOrigin = Offset(
|
||||||
|
((displayWidth * scale - viewportSize.width) / 2 - centerOffset.dx),
|
||||||
|
((displayHeight * scale - viewportSize.height) / 2 - centerOffset.dy),
|
||||||
|
);
|
||||||
|
return viewOrigin & viewportSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
Tuple2<Rect, Rectangle<int>> _getTileRects({
|
||||||
|
@required int x,
|
||||||
|
@required int y,
|
||||||
|
@required int layerRegionWidth,
|
||||||
|
@required int layerRegionHeight,
|
||||||
|
@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 tileRect = Rect.fromLTWH(x * scale, y * scale, thisRegionWidth * scale, thisRegionHeight * scale);
|
||||||
|
|
||||||
|
// only build visible tiles
|
||||||
|
if (!viewRect.overlaps(tileRect)) return null;
|
||||||
|
|
||||||
|
Rectangle<int> regionRect;
|
||||||
|
if (_tileTransform != null) {
|
||||||
|
// apply EXIF orientation
|
||||||
|
final regionRectDouble = Rect.fromLTWH(x.toDouble(), y.toDouble(), thisRegionWidth.toDouble(), thisRegionHeight.toDouble());
|
||||||
|
final tl = MatrixUtils.transformPoint(_tileTransform, regionRectDouble.topLeft);
|
||||||
|
final br = MatrixUtils.transformPoint(_tileTransform, regionRectDouble.bottomRight);
|
||||||
|
regionRect = Rectangle<int>.fromPoints(
|
||||||
|
Point<int>(tl.dx.round(), tl.dy.round()),
|
||||||
|
Point<int>(br.dx.round(), br.dy.round()),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
regionRect = Rectangle<int>(x, y, thisRegionWidth, thisRegionHeight);
|
||||||
|
}
|
||||||
|
return Tuple2<Rect, Rectangle<int>>(tileRect, regionRect);
|
||||||
|
}
|
||||||
|
|
||||||
int _sampleSizeForScale(double scale) {
|
int _sampleSizeForScale(double scale) {
|
||||||
var sample = 0;
|
var sample = 0;
|
||||||
if (0 < scale && scale < 1) {
|
if (0 < scale && scale < 1) {
|
||||||
|
|
78
lib/widgets/settings/entry_background.dart
Normal file
78
lib/widgets/settings/entry_background.dart
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import 'package:aves/model/settings/entry_background.dart';
|
||||||
|
import 'package:aves/widgets/common/fx/borders.dart';
|
||||||
|
import 'package:aves/widgets/common/fx/checkered_decoration.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class EntryBackgroundSelector extends StatefulWidget {
|
||||||
|
final ValueGetter<EntryBackground> getter;
|
||||||
|
final ValueSetter<EntryBackground> setter;
|
||||||
|
|
||||||
|
const EntryBackgroundSelector({
|
||||||
|
@required this.getter,
|
||||||
|
@required this.setter,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
_EntryBackgroundSelectorState createState() => _EntryBackgroundSelectorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EntryBackgroundSelectorState extends State<EntryBackgroundSelector> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton<EntryBackground>(
|
||||||
|
items: _buildItems(context),
|
||||||
|
value: widget.getter(),
|
||||||
|
onChanged: (selected) {
|
||||||
|
widget.setter(selected);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DropdownMenuItem<EntryBackground>> _buildItems(BuildContext context) {
|
||||||
|
const radius = 12.0;
|
||||||
|
return [
|
||||||
|
EntryBackground.white,
|
||||||
|
EntryBackground.black,
|
||||||
|
EntryBackground.checkered,
|
||||||
|
EntryBackground.transparent,
|
||||||
|
].map((selected) {
|
||||||
|
Widget child;
|
||||||
|
switch (selected) {
|
||||||
|
case EntryBackground.transparent:
|
||||||
|
child = Icon(
|
||||||
|
Icons.clear,
|
||||||
|
size: 20,
|
||||||
|
color: Colors.white30,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case EntryBackground.checkered:
|
||||||
|
child = ClipOval(
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: CheckeredDecoration(
|
||||||
|
checkSize: radius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return DropdownMenuItem<EntryBackground>(
|
||||||
|
value: selected,
|
||||||
|
child: Container(
|
||||||
|
height: radius * 2,
|
||||||
|
width: radius * 2,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: selected.isColor ? selected.color : null,
|
||||||
|
border: AvesCircleBorder.build(context),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
||||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||||
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
||||||
import 'package:aves/widgets/settings/access_grants.dart';
|
import 'package:aves/widgets/settings/access_grants.dart';
|
||||||
import 'package:aves/widgets/settings/svg_background.dart';
|
import 'package:aves/widgets/settings/entry_background.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -115,8 +115,18 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('SVG background'),
|
title: Text('Raster image background'),
|
||||||
trailing: SvgBackgroundSelector(),
|
trailing: EntryBackgroundSelector(
|
||||||
|
getter: () => settings.rasterBackground,
|
||||||
|
setter: (value) => settings.rasterBackground = value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text('Vector image background'),
|
||||||
|
trailing: EntryBackgroundSelector(
|
||||||
|
getter: () => settings.vectorBackground,
|
||||||
|
setter: (value) => settings.vectorBackground = value,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Coordinate format'),
|
title: Text('Coordinate format'),
|
||||||
|
|
|
@ -1,67 +0,0 @@
|
||||||
import 'package:aves/model/settings/entry_background.dart';
|
|
||||||
import 'package:aves/model/settings/settings.dart';
|
|
||||||
import 'package:aves/widgets/common/fx/borders.dart';
|
|
||||||
import 'package:aves/widgets/common/fx/checkered_decoration.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class SvgBackgroundSelector extends StatefulWidget {
|
|
||||||
@override
|
|
||||||
_SvgBackgroundSelectorState createState() => _SvgBackgroundSelectorState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SvgBackgroundSelectorState extends State<SvgBackgroundSelector> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
const radius = 12.0;
|
|
||||||
return DropdownButtonHideUnderline(
|
|
||||||
child: DropdownButton<EntryBackground>(
|
|
||||||
items: [
|
|
||||||
EntryBackground.white,
|
|
||||||
EntryBackground.black,
|
|
||||||
EntryBackground.checkered,
|
|
||||||
EntryBackground.transparent,
|
|
||||||
].map((selected) {
|
|
||||||
Widget child;
|
|
||||||
switch (selected) {
|
|
||||||
case EntryBackground.transparent:
|
|
||||||
child = Icon(
|
|
||||||
Icons.clear,
|
|
||||||
size: 20,
|
|
||||||
color: Colors.white30,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case EntryBackground.checkered:
|
|
||||||
child = ClipOval(
|
|
||||||
child: DecoratedBox(
|
|
||||||
decoration: CheckeredDecoration(
|
|
||||||
checkSize: radius,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return DropdownMenuItem<EntryBackground>(
|
|
||||||
value: selected,
|
|
||||||
child: Container(
|
|
||||||
height: radius * 2,
|
|
||||||
width: radius * 2,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: selected.isColor ? selected.color : null,
|
|
||||||
border: AvesCircleBorder.build(context),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
value: settings.vectorBackground,
|
|
||||||
onChanged: (selected) {
|
|
||||||
settings.vectorBackground = selected;
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue