tiling improvements (WIP)
This commit is contained in:
parent
ceed01f3ed
commit
895087f604
5 changed files with 80 additions and 49 deletions
|
@ -20,6 +20,8 @@ import kotlin.math.roundToInt
|
||||||
class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
|
class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
|
||||||
private val density = activity.resources.displayMetrics.density
|
private val density = activity.resources.displayMetrics.density
|
||||||
|
|
||||||
|
private val regionFetcher = RegionFetcher(activity)
|
||||||
|
|
||||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
"getObsoleteEntries" -> GlobalScope.launch { getObsoleteEntries(call, Coresult(result)) }
|
"getObsoleteEntries" -> GlobalScope.launch { getObsoleteEntries(call, Coresult(result)) }
|
||||||
|
@ -90,14 +92,13 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
RegionFetcher(
|
regionFetcher.fetch(
|
||||||
activity,
|
|
||||||
uri,
|
uri,
|
||||||
mimeType,
|
mimeType,
|
||||||
sampleSize,
|
sampleSize,
|
||||||
Rect(x, y, x + width, y + height),
|
Rect(x, y, x + width, y + height),
|
||||||
result,
|
result,
|
||||||
).fetch()
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getImageEntry(call: MethodCall, result: MethodChannel.Result) {
|
private suspend fun getImageEntry(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
|
|
@ -1,31 +1,49 @@
|
||||||
package deckers.thibault.aves.channel.calls
|
package deckers.thibault.aves.channel.calls
|
||||||
|
|
||||||
import android.app.Activity
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.graphics.BitmapRegionDecoder
|
import android.graphics.BitmapRegionDecoder
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
import deckers.thibault.aves.utils.MimeTypes
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
import deckers.thibault.aves.utils.StorageUtils
|
import deckers.thibault.aves.utils.StorageUtils
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
class RegionFetcher internal constructor(
|
class RegionFetcher internal constructor(
|
||||||
private val activity: Activity,
|
private val context: Context,
|
||||||
private val uri: Uri,
|
|
||||||
private val mimeType: String,
|
|
||||||
private val sampleSize: Int,
|
|
||||||
private val rect: Rect,
|
|
||||||
private val result: MethodChannel.Result,
|
|
||||||
) {
|
) {
|
||||||
|
private var lastDecoderRef: Pair<Uri, BitmapRegionDecoder>? = null
|
||||||
|
|
||||||
fun fetch() {
|
fun fetch(
|
||||||
val options = BitmapFactory.Options().apply { inSampleSize = sampleSize }
|
uri: Uri,
|
||||||
|
mimeType: String,
|
||||||
|
sampleSize: Int,
|
||||||
|
rect: Rect,
|
||||||
|
result: MethodChannel.Result,
|
||||||
|
) {
|
||||||
|
val options = BitmapFactory.Options().apply {
|
||||||
|
inSampleSize = sampleSize
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentDecoderRef = lastDecoderRef
|
||||||
|
if (currentDecoderRef != null && currentDecoderRef.first != uri) {
|
||||||
|
currentDecoderRef.second.recycle()
|
||||||
|
currentDecoderRef = null
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
StorageUtils.openInputStream(activity, uri).use { input ->
|
if (currentDecoderRef == null) {
|
||||||
val decoder = BitmapRegionDecoder.newInstance(input, false)
|
val newDecoder = StorageUtils.openInputStream(context, uri).use { input ->
|
||||||
|
BitmapRegionDecoder.newInstance(input, false)
|
||||||
|
}
|
||||||
|
currentDecoderRef = Pair(uri, newDecoder)
|
||||||
|
}
|
||||||
|
val decoder = currentDecoderRef.second
|
||||||
|
lastDecoderRef = currentDecoderRef
|
||||||
|
|
||||||
val data = decoder.decodeRegion(rect, options)?.let {
|
val data = decoder.decodeRegion(rect, options)?.let {
|
||||||
val stream = ByteArrayOutputStream()
|
val stream = ByteArrayOutputStream()
|
||||||
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
|
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
|
||||||
|
@ -42,9 +60,8 @@ class RegionFetcher internal constructor(
|
||||||
} else {
|
} else {
|
||||||
result.error("getRegion-null", "failed to decode region for uri=$uri rect=$rect", null)
|
result.error("getRegion-null", "failed to decode region for uri=$uri rect=$rect", null)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
result.error("getRegion-read-exception", "failed to get image from uri=$uri", e.message)
|
result.error("getRegion-read-exception", "failed to initialize region decoder for uri=$uri", e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,6 +2,10 @@ import 'dart:math';
|
||||||
|
|
||||||
const double _piOver180 = pi / 180.0;
|
const double _piOver180 = pi / 180.0;
|
||||||
|
|
||||||
double toDegrees(double radians) => radians / _piOver180;
|
final double log2 = log(2);
|
||||||
|
|
||||||
double toRadians(double degrees) => degrees * _piOver180;
|
double toDegrees(num radians) => radians / _piOver180;
|
||||||
|
|
||||||
|
double toRadians(num degrees) => degrees * _piOver180;
|
||||||
|
|
||||||
|
int highestPowerOf2(num x) => x < 1 ? 0 : pow(2, (log(x) / log2).floor());
|
||||||
|
|
|
@ -43,6 +43,7 @@ class _ImageViewState extends State<ImageView> {
|
||||||
StreamSubscription<PhotoViewControllerValue> _subscription;
|
StreamSubscription<PhotoViewControllerValue> _subscription;
|
||||||
|
|
||||||
static const backgroundDecoration = BoxDecoration(color: Colors.transparent);
|
static const backgroundDecoration = BoxDecoration(color: Colors.transparent);
|
||||||
|
static const maxScale = 2.0;
|
||||||
|
|
||||||
ImageEntry get entry => widget.entry;
|
ImageEntry get entry => widget.entry;
|
||||||
|
|
||||||
|
@ -140,6 +141,7 @@ class _ImageViewState extends State<ImageView> {
|
||||||
loadFailedChild: _buildError(),
|
loadFailedChild: _buildError(),
|
||||||
backgroundDecoration: backgroundDecoration,
|
backgroundDecoration: backgroundDecoration,
|
||||||
controller: _photoViewController,
|
controller: _photoViewController,
|
||||||
|
maxScale: maxScale,
|
||||||
minScale: PhotoViewComputedScale.contained,
|
minScale: PhotoViewComputedScale.contained,
|
||||||
initialScale: PhotoViewComputedScale.contained,
|
initialScale: PhotoViewComputedScale.contained,
|
||||||
onTapUp: (tapContext, details, value) => onTap?.call(),
|
onTapUp: (tapContext, details, value) => onTap?.call(),
|
||||||
|
@ -166,6 +168,7 @@ class _ImageViewState extends State<ImageView> {
|
||||||
childSize: entry.displaySize,
|
childSize: entry.displaySize,
|
||||||
backgroundDecoration: backgroundDecoration,
|
backgroundDecoration: backgroundDecoration,
|
||||||
controller: _photoViewController,
|
controller: _photoViewController,
|
||||||
|
maxScale: maxScale,
|
||||||
minScale: PhotoViewComputedScale.contained,
|
minScale: PhotoViewComputedScale.contained,
|
||||||
initialScale: PhotoViewComputedScale.contained,
|
initialScale: PhotoViewComputedScale.contained,
|
||||||
onTapUp: (tapContext, details, value) => onTap?.call(),
|
onTapUp: (tapContext, details, value) => onTap?.call(),
|
||||||
|
@ -204,6 +207,7 @@ class _ImageViewState extends State<ImageView> {
|
||||||
childSize: entry.displaySize,
|
childSize: entry.displaySize,
|
||||||
backgroundDecoration: backgroundDecoration,
|
backgroundDecoration: backgroundDecoration,
|
||||||
controller: _photoViewController,
|
controller: _photoViewController,
|
||||||
|
maxScale: maxScale,
|
||||||
minScale: PhotoViewComputedScale.contained,
|
minScale: PhotoViewComputedScale.contained,
|
||||||
initialScale: PhotoViewComputedScale.contained,
|
initialScale: PhotoViewComputedScale.contained,
|
||||||
onTapUp: (tapContext, details, value) => onTap?.call(),
|
onTapUp: (tapContext, details, value) => onTap?.call(),
|
||||||
|
|
|
@ -27,8 +27,9 @@ class TiledImageView extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TiledImageViewState extends State<TiledImageView> {
|
class _TiledImageViewState extends State<TiledImageView> {
|
||||||
double _initialScale;
|
double _tileSide, _initialScale;
|
||||||
int _maxSampleSize;
|
int _maxSampleSize;
|
||||||
|
Matrix4 _transform;
|
||||||
|
|
||||||
ImageEntry get entry => widget.entry;
|
ImageEntry get entry => widget.entry;
|
||||||
|
|
||||||
|
@ -36,10 +37,11 @@ class _TiledImageViewState extends State<TiledImageView> {
|
||||||
|
|
||||||
ValueNotifier<ViewState> get viewStateNotifier => widget.viewStateNotifier;
|
ValueNotifier<ViewState> get viewStateNotifier => widget.viewStateNotifier;
|
||||||
|
|
||||||
static const tileSide = 200.0;
|
|
||||||
|
|
||||||
// margin around visible area to fetch surrounding tiles in advance
|
// margin around visible area to fetch surrounding tiles in advance
|
||||||
static const preFetchMargin = 50.0;
|
static const preFetchMargin = 0.0;
|
||||||
|
|
||||||
|
// magic number used to derive sample size from scale
|
||||||
|
static const scaleFactor = 2.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -57,8 +59,20 @@ class _TiledImageViewState extends State<TiledImageView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _init() {
|
void _init() {
|
||||||
|
_tileSide = viewportSize.shortestSide * scaleFactor;
|
||||||
_initialScale = min(viewportSize.width / entry.displaySize.width, viewportSize.height / entry.displaySize.height);
|
_initialScale = min(viewportSize.width / entry.displaySize.width, viewportSize.height / entry.displaySize.height);
|
||||||
_maxSampleSize = _sampleSizeForScale(_initialScale);
|
_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(-entry.displaySize.width / 2.0, -entry.displaySize.height / 2.0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -67,16 +81,6 @@ class _TiledImageViewState extends State<TiledImageView> {
|
||||||
|
|
||||||
final displayWidth = entry.displaySize.width;
|
final displayWidth = entry.displaySize.width;
|
||||||
final displayHeight = entry.displaySize.height;
|
final displayHeight = entry.displaySize.height;
|
||||||
final rotationDegrees = entry.rotationDegrees;
|
|
||||||
final isFlipped = entry.isFlipped;
|
|
||||||
Matrix4 transform;
|
|
||||||
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(-displayWidth / 2.0, -displayHeight / 2.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: viewStateNotifier,
|
animation: viewStateNotifier,
|
||||||
|
@ -98,7 +102,7 @@ class _TiledImageViewState extends State<TiledImageView> {
|
||||||
final tiles = <Widget>[];
|
final tiles = <Widget>[];
|
||||||
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()) {
|
||||||
final layerRegionSize = Size.square(tileSide * sampleSize);
|
final layerRegionSize = Size.square(_tileSide * sampleSize);
|
||||||
for (var x = 0.0; x < displayWidth; x += layerRegionSize.width) {
|
for (var x = 0.0; x < displayWidth; x += layerRegionSize.width) {
|
||||||
for (var y = 0.0; y < displayHeight; y += layerRegionSize.height) {
|
for (var y = 0.0; y < displayHeight; y += layerRegionSize.height) {
|
||||||
final regionOrigin = Offset(x, y);
|
final regionOrigin = Offset(x, y);
|
||||||
|
@ -114,10 +118,10 @@ class _TiledImageViewState extends State<TiledImageView> {
|
||||||
var regionRect = regionOrigin & thisRegionSize;
|
var regionRect = regionOrigin & thisRegionSize;
|
||||||
|
|
||||||
// apply EXIF orientation
|
// apply EXIF orientation
|
||||||
if (transform != null) {
|
if (_transform != null) {
|
||||||
regionRect = Rect.fromPoints(
|
regionRect = Rect.fromPoints(
|
||||||
MatrixUtils.transformPoint(transform, regionRect.topLeft),
|
MatrixUtils.transformPoint(_transform, regionRect.topLeft),
|
||||||
MatrixUtils.transformPoint(transform, regionRect.bottomRight),
|
MatrixUtils.transformPoint(_transform, regionRect.bottomRight),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,7 +153,7 @@ class _TiledImageViewState extends State<TiledImageView> {
|
||||||
int _sampleSizeForScale(double scale) {
|
int _sampleSizeForScale(double scale) {
|
||||||
var sample = 0;
|
var sample = 0;
|
||||||
if (0 < scale && scale < 1) {
|
if (0 < scale && scale < 1) {
|
||||||
sample = pow(2, (log(1 / scale) / log(2)).floor());
|
sample = highestPowerOf2((1 / scale) / scaleFactor);
|
||||||
}
|
}
|
||||||
return max<int>(1, sample);
|
return max<int>(1, sample);
|
||||||
}
|
}
|
||||||
|
@ -157,6 +161,7 @@ class _TiledImageViewState extends State<TiledImageView> {
|
||||||
|
|
||||||
class RegionTile extends StatelessWidget {
|
class RegionTile extends StatelessWidget {
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
|
|
||||||
// `tileRect` uses Flutter view coordinates
|
// `tileRect` uses Flutter view coordinates
|
||||||
// `regionRect` uses the raw image pixel coordinates
|
// `regionRect` uses the raw image pixel coordinates
|
||||||
final Rect tileRect, regionRect;
|
final Rect tileRect, regionRect;
|
||||||
|
|
Loading…
Reference in a new issue