raster image background

This commit is contained in:
Thibault Deckers 2020-12-23 16:00:46 +09:00
parent 5e7c85597a
commit 07b9db6750
9 changed files with 345 additions and 198 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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; void _registerFullImage() {
final isFlipped = entry.isFlipped; _fullImageStream = fullImageProvider.resolve(ImageConfiguration.empty);
_transform = null; _fullImageStream.addListener(_fullImageListener);
if (rotationDegrees != 0 || isFlipped) { }
_transform = Matrix4.identity()
..translate(entry.width / 2.0, entry.height / 2.0) void _unregisterFullImage() {
..scale(isFlipped ? -1.0 : 1.0, 1.0, 1.0) _fullImageStream?.removeListener(_fullImageListener);
..rotateZ(-toRadians(rotationDegrees.toDouble())) _fullImageStream = null;
..translate(-displaySize.width / 2.0, -displaySize.height / 2.0); }
}
_initialized = true; 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, Image(
); image: fullImageProvider,
gaplessPlayback: true,
List<Widget> children; errorBuilder: widget.errorBuilder,
if (useTiles) { width: (entry.displaySize * viewState.scale).width,
children = [ fit: BoxFit.contain,
loading, filterQuality: FilterQuality.medium,
..._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,
); );
}, },
); );
} }
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) { if (rects != 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);
}
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) {

View 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();
}
}

View file

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

View file

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