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 canHaveAlpha => MimeTypes.alphaImages.contains(mimeType);
|
||||
|
||||
ImageEntry copyWith({
|
||||
@required String uri,
|
||||
@required String path,
|
||||
|
|
|
@ -55,6 +55,7 @@ class Settings extends ChangeNotifier {
|
|||
static const coordinateFormatKey = 'coordinates_format';
|
||||
|
||||
// rendering
|
||||
static const rasterBackgroundKey = 'raster_background';
|
||||
static const vectorBackgroundKey = 'vector_background';
|
||||
|
||||
// search
|
||||
|
@ -185,6 +186,10 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
// 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);
|
||||
|
||||
set vectorBackground(EntryBackground newValue) => setAndNotify(vectorBackgroundKey, newValue.toString());
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
class MimeTypes {
|
||||
static const anyImage = 'image/*';
|
||||
|
||||
static const bmp = 'image/bmp';
|
||||
static const gif = 'image/gif';
|
||||
static const heic = 'image/heic';
|
||||
static const heif = 'image/heif';
|
||||
static const ico = 'image/x-icon';
|
||||
static const jpeg = 'image/jpeg';
|
||||
static const png = 'image/png';
|
||||
static const svg = 'image/svg+xml';
|
||||
static const tiff = 'image/tiff';
|
||||
static const webp = 'image/webp';
|
||||
|
||||
static const tiff = 'image/tiff';
|
||||
static const psd = 'image/vnd.adobe.photoshop';
|
||||
|
||||
static const arw = 'image/x-sony-arw';
|
||||
|
@ -40,6 +42,10 @@ class MimeTypes {
|
|||
static const mp4 = 'video/mp4';
|
||||
|
||||
// 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 bool isImage(String mimeType) => mimeType.startsWith('image');
|
||||
|
|
|
@ -15,6 +15,7 @@ class XMP {
|
|||
'exifEX': 'Exif Ex',
|
||||
'GettyImagesGIFT': 'Getty Images',
|
||||
'GIMP': 'GIMP',
|
||||
'GCamera': 'Google Camera',
|
||||
'GFocus': 'Google Focus',
|
||||
'GPano': 'Google Panorama',
|
||||
'illustrator': 'Illustrator',
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
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/model/image_entry.dart';
|
||||
import 'package:aves/model/settings/entry_background.dart';
|
||||
|
@ -30,6 +28,8 @@ class ImageView extends StatefulWidget {
|
|||
final List<Tuple2<String, IjkMediaController>> videoControllers;
|
||||
final VoidCallback onDisposed;
|
||||
|
||||
static const decorationCheckSize = 20.0;
|
||||
|
||||
const ImageView({
|
||||
Key key,
|
||||
@required this.entry,
|
||||
|
@ -56,8 +56,6 @@ class _ImageViewState extends State<ImageView> {
|
|||
|
||||
MagnifierTapCallback get onTap => widget.onTap;
|
||||
|
||||
static const decorationCheckSize = 20.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
@ -98,25 +96,6 @@ class _ImageViewState extends State<ImageView> {
|
|||
: 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() {
|
||||
return Magnifier(
|
||||
// key includes size and orientation to refresh when the image is rotated
|
||||
|
@ -124,7 +103,6 @@ class _ImageViewState extends State<ImageView> {
|
|||
child: TiledImageView(
|
||||
entry: entry,
|
||||
viewStateNotifier: _viewStateNotifier,
|
||||
baseChild: _loadingBuilder(context, fastThumbnailProvider),
|
||||
errorBuilder: (context, error, stackTrace) => ErrorChild(onTap: () => onTap?.call(null)),
|
||||
),
|
||||
childSize: entry.displaySize,
|
||||
|
@ -165,11 +143,11 @@ class _ImageViewState extends State<ImageView> {
|
|||
if (viewportSize == null) return child;
|
||||
|
||||
final side = viewportSize.shortestSide;
|
||||
final checkSize = side / ((side / decorationCheckSize).round());
|
||||
final checkSize = side / ((side / ImageView.decorationCheckSize).round());
|
||||
|
||||
final viewSize = entry.displaySize * viewState.scale;
|
||||
final decorationSize = Size(min(viewSize.width, viewportSize.width), min(viewSize.height, viewportSize.height));
|
||||
final offset = Offset(decorationSize.width - viewportSize.width, decorationSize.height - viewportSize.height) / 2;
|
||||
final decorationSize = applyBoxFit(BoxFit.none, viewSize, viewportSize).source;
|
||||
final offset = ((decorationSize - viewportSize) as Offset) / 2;
|
||||
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
|
|
|
@ -1,23 +1,26 @@
|
|||
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/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/common/fx/checkered_decoration.dart';
|
||||
import 'package:aves/widgets/fullscreen/image_view.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class TiledImageView extends StatefulWidget {
|
||||
final ImageEntry entry;
|
||||
final ValueNotifier<ViewState> viewStateNotifier;
|
||||
final Widget baseChild;
|
||||
final ImageErrorWidgetBuilder errorBuilder;
|
||||
|
||||
const TiledImageView({
|
||||
@required this.entry,
|
||||
@required this.viewStateNotifier,
|
||||
@required this.baseChild,
|
||||
@required this.errorBuilder,
|
||||
});
|
||||
|
||||
|
@ -26,159 +29,244 @@ class TiledImageView extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _TiledImageViewState extends State<TiledImageView> {
|
||||
bool _initialized = false;
|
||||
double _tileSide, _initialScale;
|
||||
bool _isTilingInitialized = false;
|
||||
int _maxSampleSize;
|
||||
Matrix4 _transform;
|
||||
double _tileSide;
|
||||
Matrix4 _tileTransform;
|
||||
ImageStream _fullImageStream;
|
||||
ImageStreamListener _fullImageListener;
|
||||
final ValueNotifier<bool> _fullImageLoaded = ValueNotifier(false);
|
||||
|
||||
ImageEntry get entry => widget.entry;
|
||||
|
||||
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);
|
||||
|
||||
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,
|
||||
mimeType: entry.mimeType,
|
||||
rotationDegrees: entry.rotationDegrees,
|
||||
isFlipped: entry.isFlipped,
|
||||
expectedContentLength: entry.sizeBytes,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// magic number used to derive sample size from scale
|
||||
static const scaleFactor = 2.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fullImageListener = ImageStreamListener(_onFullImageCompleted);
|
||||
if (!useTiles) _registerFullImage();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(TiledImageView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
final oldViewState = oldWidget.viewStateNotifier.value;
|
||||
final viewState = widget.viewStateNotifier.value;
|
||||
if (oldViewState.viewportSize != viewState.viewportSize || oldWidget.entry.displaySize != widget.entry.displaySize) {
|
||||
_initialized = false;
|
||||
if (oldWidget.entry != widget.entry || oldViewState.viewportSize != viewState.viewportSize) {
|
||||
_isTilingInitialized = false;
|
||||
_fullImageLoaded.value = false;
|
||||
_unregisterFullImage();
|
||||
}
|
||||
}
|
||||
|
||||
void _initFromViewport(Size viewportSize) {
|
||||
final displaySize = entry.displaySize;
|
||||
_tileSide = viewportSize.shortestSide * scaleFactor;
|
||||
_initialScale = min(viewportSize.width / displaySize.width, viewportSize.height / displaySize.height);
|
||||
_maxSampleSize = _sampleSizeForScale(_initialScale);
|
||||
@override
|
||||
void dispose() {
|
||||
_unregisterFullImage();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
if (viewStateNotifier == null) return SizedBox.shrink();
|
||||
|
||||
final displayWidth = entry.displaySize.width.round();
|
||||
final displayHeight = entry.displaySize.height.round();
|
||||
|
||||
return ValueListenableBuilder<ViewState>(
|
||||
valueListenable: viewStateNotifier,
|
||||
builder: (context, viewState, child) {
|
||||
final viewportSize = viewState.viewportSize;
|
||||
if (viewportSize == null) return SizedBox.shrink();
|
||||
if (!_initialized) _initFromViewport(viewportSize);
|
||||
final viewportSized = viewportSize != null;
|
||||
if (viewportSized && useTiles && !_isTilingInitialized) _initTiling(viewportSize);
|
||||
|
||||
var scale = viewState.scale;
|
||||
if (scale == 0.0) {
|
||||
// for initial scale as `contained`
|
||||
scale = _initialScale;
|
||||
}
|
||||
final scaledSize = entry.displaySize * scale;
|
||||
final loading = SizedBox(
|
||||
width: scaledSize.width,
|
||||
height: scaledSize.height,
|
||||
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: fullImage,
|
||||
gaplessPlayback: true,
|
||||
errorBuilder: widget.errorBuilder,
|
||||
width: scaledSize.width,
|
||||
fit: BoxFit.contain,
|
||||
filterQuality: FilterQuality.medium,
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: children,
|
||||
return SizedBox.fromSize(
|
||||
size: entry.displaySize * viewState.scale,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
if (useBackground && viewportSized) _buildBackground(viewState),
|
||||
_buildLoading(viewState),
|
||||
if (useTiles) ..._getTiles(viewState),
|
||||
if (!useTiles)
|
||||
Image(
|
||||
image: fullImageProvider,
|
||||
gaplessPlayback: true,
|
||||
errorBuilder: widget.errorBuilder,
|
||||
width: (entry.displaySize * viewState.scale).width,
|
||||
fit: BoxFit.contain,
|
||||
filterQuality: FilterQuality.medium,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<RegionTile> _getTiles(ViewState viewState, int displayWidth, int displayHeight, double 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),
|
||||
void _initTiling(Size viewportSize) {
|
||||
final displaySize = entry.displaySize;
|
||||
_tileSide = viewportSize.shortestSide * scaleFactor;
|
||||
// scale for initial state `contained`
|
||||
final containedScale = min(viewportSize.width / displaySize.width, viewportSize.height / displaySize.height);
|
||||
_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>[];
|
||||
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 instead of splitting it in tiles
|
||||
final useTiles = sampleSize != _maxSampleSize;
|
||||
// so we subsample the whole image without tiling
|
||||
final fullImageRegion = sampleSize == _maxSampleSize;
|
||||
final regionSide = (_tileSide * sampleSize).round();
|
||||
final layerRegionWidth = useTiles ? regionSide : displayWidth;
|
||||
final layerRegionHeight = useTiles ? regionSide : displayHeight;
|
||||
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) {
|
||||
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)) {
|
||||
Rectangle<int> regionRect;
|
||||
|
||||
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 {
|
||||
regionRect = Rectangle<int>(x, y, thisRegionWidth, thisRegionHeight);
|
||||
}
|
||||
|
||||
final rects = _getTileRects(
|
||||
x: x,
|
||||
y: y,
|
||||
layerRegionWidth: layerRegionWidth,
|
||||
layerRegionHeight: layerRegionHeight,
|
||||
displayWidth: displayWidth,
|
||||
displayHeight: displayHeight,
|
||||
scale: scale,
|
||||
viewRect: viewRect,
|
||||
);
|
||||
if (rects != null) {
|
||||
tiles.add(RegionTile(
|
||||
entry: entry,
|
||||
tileRect: tileRect,
|
||||
regionRect: regionRect,
|
||||
tileRect: rects.item1,
|
||||
regionRect: rects.item2,
|
||||
sampleSize: sampleSize,
|
||||
));
|
||||
}
|
||||
|
@ -188,6 +276,52 @@ class _TiledImageViewState extends State<TiledImageView> {
|
|||
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) {
|
||||
var sample = 0;
|
||||
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/dialogs/aves_selection_dialog.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_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -115,8 +115,18 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('SVG background'),
|
||||
trailing: SvgBackgroundSelector(),
|
||||
title: Text('Raster image background'),
|
||||
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(
|
||||
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