add blurhash
This commit is contained in:
parent
d99dba6a5e
commit
32882a3190
10 changed files with 752 additions and 5 deletions
|
|
@ -2,6 +2,7 @@ adaptive_number: ea9178fdd4d82ac45cf0ec966ac870dae661124f
|
|||
dots_indicator: 508f5883ac79bdbc10254092de3f28f571d261cd
|
||||
ed25519_edwards: 7353ba759ea9f4646cbf481c2ef949625c8ce4cf
|
||||
exif: bf170d5639f0b6fcb0947060cf8bd7b623df9069
|
||||
flutter_blurhash: c5cdec4986432e835bb91f5ce00564534450cdc7
|
||||
flutter_markdown_plus: dc1185c933fbf9dba559ef6c91586ff1503be3ee
|
||||
flutter_sharing_intent: aa1672f547d6579585fa27df0b28ffa2a2544aaa
|
||||
hand_signature: 1beedb164d093643365b0832277c377353c7464f
|
||||
|
|
|
|||
|
|
@ -66,4 +66,8 @@ exif:
|
|||
git: https://github.com/bigflood/dartexif.git
|
||||
dependencies:
|
||||
sprintf:
|
||||
git: https://github.com/Naddiseo/dart-sprintf.git
|
||||
git: https://github.com/Naddiseo/dart-sprintf.git
|
||||
|
||||
|
||||
flutter_blurhash:
|
||||
git: https://github.com/fluttercommunity/flutter_blurhash.git
|
||||
21
flutter_blurhash/LICENSE
Executable file
21
flutter_blurhash/LICENSE
Executable file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2019 Robert Felker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
5
flutter_blurhash/lib/flutter_blurhash.dart
Normal file
5
flutter_blurhash/lib/flutter_blurhash.dart
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
library flutter_blurhash;
|
||||
|
||||
export 'src/blurhash.dart';
|
||||
export 'src/blurhash_widget.dart';
|
||||
export 'src/blurhash_image.dart';
|
||||
378
flutter_blurhash/lib/src/blurhash.dart
Normal file
378
flutter_blurhash/lib/src/blurhash.dart
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Optimization modes for BlurHash decoder
|
||||
enum BlurHashOptimizationMode {
|
||||
/// Original algorithm
|
||||
none,
|
||||
|
||||
/// Optimized with better cache locality
|
||||
standard,
|
||||
|
||||
/// Approximation with faster sRGB conversion + cache locality
|
||||
approximation
|
||||
}
|
||||
|
||||
// Optimized BlurHash decode implementation
|
||||
Future<Uint8List> optimizedBlurHashDecode({
|
||||
required String blurHash,
|
||||
required int width,
|
||||
required int height,
|
||||
double punch = 1.0,
|
||||
BlurHashOptimizationMode optimizationMode = BlurHashOptimizationMode.standard,
|
||||
}) {
|
||||
_validateBlurHash(blurHash);
|
||||
|
||||
final sizeFlag = _decode83(blurHash[0]);
|
||||
final numY = (sizeFlag / 9).floor() + 1;
|
||||
final numX = (sizeFlag % 9) + 1;
|
||||
|
||||
final quantisedMaximumValue = _decode83(blurHash[1]);
|
||||
final maximumValue = (quantisedMaximumValue + 1) / 166;
|
||||
|
||||
// Preallocate colors array with fixed size
|
||||
final colors = List<List<double>>.filled(numX * numY, [0, 0, 0]);
|
||||
|
||||
// Decode DC component (first component)
|
||||
final dcValue = _decode83(blurHash.substring(2, 6));
|
||||
colors[0] = _decodeDC(dcValue);
|
||||
|
||||
// Decode AC components (remaining components)
|
||||
final adjustedPunch = maximumValue * punch;
|
||||
for (var i = 1; i < colors.length; i++) {
|
||||
final value = _decode83(blurHash.substring(4 + i * 2, 6 + i * 2));
|
||||
colors[i] = _decodeAC(value, adjustedPunch);
|
||||
}
|
||||
|
||||
// Precalculate cosine values for x and y
|
||||
final cosinesX = List<List<double>>.generate(
|
||||
numX,
|
||||
(i) => List<double>.generate(
|
||||
width,
|
||||
(x) => cos((pi * x * i) / width),
|
||||
),
|
||||
);
|
||||
|
||||
final cosinesY = List<List<double>>.generate(
|
||||
numY,
|
||||
(j) => List<double>.generate(
|
||||
height,
|
||||
(y) => cos((pi * y * j) / height),
|
||||
),
|
||||
);
|
||||
|
||||
final bytesPerRow = width * 4;
|
||||
final pixels = Uint8List(bytesPerRow * height);
|
||||
|
||||
// Process image in chunks to improve cache locality
|
||||
const chunkSize = 32;
|
||||
|
||||
// Process the image in tiles for better cache performance
|
||||
for (int yChunk = 0; yChunk < height; yChunk += chunkSize) {
|
||||
final yEnd = min(yChunk + chunkSize, height);
|
||||
|
||||
for (int xChunk = 0; xChunk < width; xChunk += chunkSize) {
|
||||
final xEnd = min(xChunk + chunkSize, width);
|
||||
|
||||
for (int y = yChunk; y < yEnd; y++) {
|
||||
int p = (y * width + xChunk) * 4;
|
||||
|
||||
for (int x = xChunk; x < xEnd; x++) {
|
||||
var r = 0.0, g = 0.0, b = 0.0;
|
||||
|
||||
// Use precalculated cosine values
|
||||
for (int j = 0; j < numY; j++) {
|
||||
final cosY = cosinesY[j][y];
|
||||
|
||||
for (int i = 0; i < numX; i++) {
|
||||
final basis = cosinesX[i][x] * cosY;
|
||||
final color = colors[i + j * numX];
|
||||
|
||||
r += color[0] * basis;
|
||||
g += color[1] * basis;
|
||||
b += color[2] * basis;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert linear RGB to sRGB space based on optimization mode
|
||||
switch (optimizationMode) {
|
||||
case BlurHashOptimizationMode.approximation:
|
||||
pixels[p++] = _approximatedLinearTosRGB(r);
|
||||
pixels[p++] = _approximatedLinearTosRGB(g);
|
||||
pixels[p++] = _approximatedLinearTosRGB(b);
|
||||
break;
|
||||
case BlurHashOptimizationMode.standard:
|
||||
case BlurHashOptimizationMode.none:
|
||||
pixels[p++] = _linearTosRGB(r);
|
||||
pixels[p++] = _linearTosRGB(g);
|
||||
pixels[p++] = _linearTosRGB(b);
|
||||
break;
|
||||
}
|
||||
|
||||
pixels[p++] = 255; // Alpha is always 255
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Future.value(pixels);
|
||||
}
|
||||
|
||||
// Create this once as a static variable
|
||||
final List<double> _sRGBLookupTable = _createSRGBLookupTable(256);
|
||||
|
||||
List<double> _createSRGBLookupTable(int size) {
|
||||
final table = List<double>.filled(size, 0);
|
||||
for (int i = 0; i < size; i++) {
|
||||
final v = i / (size - 1);
|
||||
if (v <= 0.0031308) {
|
||||
table[i] = v * 12.92;
|
||||
} else {
|
||||
table[i] = 1.055 * pow(v, 1 / 2.4) - 0.055;
|
||||
}
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
int _approximatedLinearTosRGB(double value) {
|
||||
final v = max(0.0, min(1.0, value));
|
||||
|
||||
// Find the closest indices in the lookup table
|
||||
final pos = v * (_sRGBLookupTable.length - 1);
|
||||
final idx = pos.floor();
|
||||
final fract = pos - idx;
|
||||
|
||||
// Edge case for the maximum value
|
||||
if (idx >= _sRGBLookupTable.length - 1) {
|
||||
return (_sRGBLookupTable[_sRGBLookupTable.length - 1] * 255 + 0.5).toInt();
|
||||
}
|
||||
|
||||
// Linear interpolation between the two closest values
|
||||
final result =
|
||||
_sRGBLookupTable[idx] * (1 - fract) + _sRGBLookupTable[idx + 1] * fract;
|
||||
return (result * 255 + 0.5).toInt();
|
||||
}
|
||||
|
||||
Future<Uint8List> blurHashDecode({
|
||||
required String blurHash,
|
||||
required int width,
|
||||
required int height,
|
||||
double punch = 1.0,
|
||||
}) {
|
||||
_validateBlurHash(blurHash);
|
||||
|
||||
final sizeFlag = _decode83(blurHash[0]);
|
||||
final numY = (sizeFlag / 9).floor() + 1;
|
||||
final numX = (sizeFlag % 9) + 1;
|
||||
|
||||
final quantisedMaximumValue = _decode83(blurHash[1]);
|
||||
final maximumValue = (quantisedMaximumValue + 1) / 166;
|
||||
|
||||
final colors = []..length = numX * numY;
|
||||
|
||||
for (var i = 0; i < colors.length; i++) {
|
||||
if (i == 0) {
|
||||
final value = _decode83(blurHash.substring(2, 6));
|
||||
colors[i] = _decodeDC(value);
|
||||
} else {
|
||||
final value = _decode83(blurHash.substring(4 + i * 2, 6 + i * 2));
|
||||
colors[i] = _decodeAC(value, maximumValue * punch);
|
||||
}
|
||||
}
|
||||
|
||||
final bytesPerRow = width * 4;
|
||||
final pixels = Uint8List(bytesPerRow * height);
|
||||
|
||||
int p = 0;
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
var r = .0;
|
||||
var g = .0;
|
||||
var b = .0;
|
||||
|
||||
for (int j = 0; j < numY; j++) {
|
||||
for (int i = 0; i < numX; i++) {
|
||||
final basis = cos((pi * x * i) / width) * cos((pi * y * j) / height);
|
||||
var color = colors[i + j * numX];
|
||||
r += color[0] * basis;
|
||||
g += color[1] * basis;
|
||||
b += color[2] * basis;
|
||||
}
|
||||
}
|
||||
|
||||
final intR = _linearTosRGB(r);
|
||||
final intG = _linearTosRGB(g);
|
||||
final intB = _linearTosRGB(b);
|
||||
|
||||
pixels[p++] = intR;
|
||||
pixels[p++] = intG;
|
||||
pixels[p++] = intB;
|
||||
pixels[p++] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
return Future.value(pixels);
|
||||
}
|
||||
|
||||
Future<ui.Image> blurHashDecodeImage({
|
||||
required String blurHash,
|
||||
required int width,
|
||||
required int height,
|
||||
double punch = 1.0,
|
||||
BlurHashOptimizationMode optimizationMode = BlurHashOptimizationMode.standard,
|
||||
}) async {
|
||||
_validateBlurHash(blurHash);
|
||||
|
||||
final completer = Completer<ui.Image>();
|
||||
|
||||
final Uint8List pixels;
|
||||
if (optimizationMode != BlurHashOptimizationMode.none) {
|
||||
pixels = await optimizedBlurHashDecode(
|
||||
blurHash: blurHash,
|
||||
width: width,
|
||||
height: height,
|
||||
punch: punch,
|
||||
optimizationMode: optimizationMode,
|
||||
);
|
||||
} else {
|
||||
pixels = await blurHashDecode(
|
||||
blurHash: blurHash,
|
||||
width: width,
|
||||
height: height,
|
||||
punch: punch,
|
||||
);
|
||||
}
|
||||
|
||||
if (kIsWeb) {
|
||||
completer.complete(_createBmp(pixels, width, height));
|
||||
} else {
|
||||
ui.decodeImageFromPixels(
|
||||
pixels, width, height, ui.PixelFormat.rgba8888, completer.complete);
|
||||
}
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<ui.Image> _createBmp(Uint8List pixels, int width, int height) async {
|
||||
int size = (width * height * 4) + 122;
|
||||
final bmp = Uint8List(size);
|
||||
final ByteData header = bmp.buffer.asByteData();
|
||||
header.setUint8(0x0, 0x42);
|
||||
header.setUint8(0x1, 0x4d);
|
||||
header.setInt32(0x2, size, Endian.little);
|
||||
header.setInt32(0xa, 122, Endian.little);
|
||||
header.setUint32(0xe, 108, Endian.little);
|
||||
header.setUint32(0x12, width, Endian.little);
|
||||
header.setUint32(0x16, -height, Endian.little);
|
||||
header.setUint16(0x1a, 1, Endian.little);
|
||||
header.setUint32(0x1c, 32, Endian.little);
|
||||
header.setUint32(0x1e, 3, Endian.little);
|
||||
header.setUint32(0x22, width * height * 4, Endian.little);
|
||||
header.setUint32(0x36, 0x000000ff, Endian.little);
|
||||
header.setUint32(0x3a, 0x0000ff00, Endian.little);
|
||||
header.setUint32(0x3e, 0x00ff0000, Endian.little);
|
||||
header.setUint32(0x42, 0xff000000, Endian.little);
|
||||
bmp.setRange(122, size, pixels);
|
||||
final codec = await ui.instantiateImageCodec(bmp);
|
||||
final frame = await codec.getNextFrame();
|
||||
return frame.image;
|
||||
}
|
||||
|
||||
double _sRGBToLinear(int value) {
|
||||
final v = value / 255;
|
||||
if (v <= 0.04045) {
|
||||
return v / 12.92;
|
||||
} else {
|
||||
return pow((v + 0.055) / 1.055, 2.4) as double;
|
||||
}
|
||||
}
|
||||
|
||||
int _linearTosRGB(double value) {
|
||||
final v = max(0, min(1, value));
|
||||
if (v <= 0.0031308) {
|
||||
return (v * 12.92 * 255 + 0.5).round();
|
||||
} else {
|
||||
return ((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5).round();
|
||||
}
|
||||
}
|
||||
|
||||
void _validateBlurHash(String blurHash) {
|
||||
if (blurHash.length < 6) {
|
||||
throw Exception('The blurhash string must be at least 6 characters');
|
||||
}
|
||||
|
||||
final sizeFlag = _decode83(blurHash[0]);
|
||||
final numY = (sizeFlag / 9).floor() + 1;
|
||||
final numX = (sizeFlag % 9) + 1;
|
||||
|
||||
if (blurHash.length != 4 + 2 * numX * numY) {
|
||||
throw Exception(
|
||||
'blurhash length mismatch: length is ${blurHash.length} but '
|
||||
'it should be ${4 + 2 * numX * numY}');
|
||||
}
|
||||
}
|
||||
|
||||
int _sign(double n) => (n < 0 ? -1 : 1);
|
||||
|
||||
num _signPow(double val, double exp) => _sign(val) * pow(val.abs(), exp);
|
||||
|
||||
int _decode83(String str) {
|
||||
var value = 0;
|
||||
final units = str.codeUnits;
|
||||
final digits = _digitCharacters.codeUnits;
|
||||
for (var i = 0; i < units.length; i++) {
|
||||
final code = units.elementAt(i);
|
||||
final digit = digits.indexOf(code);
|
||||
if (digit == -1) {
|
||||
throw ArgumentError.value(str, 'str');
|
||||
}
|
||||
value = value * 83 + digit;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
List<double> _decodeDC(int value) {
|
||||
final intR = value >> 16;
|
||||
final intG = (value >> 8) & 255;
|
||||
final intB = value & 255;
|
||||
return [_sRGBToLinear(intR), _sRGBToLinear(intG), _sRGBToLinear(intB)];
|
||||
}
|
||||
|
||||
List<double> _decodeAC(int value, double maximumValue) {
|
||||
final quantR = (value / (19 * 19)).floor();
|
||||
final quantG = (value / 19).floor() % 19;
|
||||
final quantB = value % 19;
|
||||
|
||||
final rgb = [
|
||||
_signPow((quantR - 9) / 9, 2.0) * maximumValue,
|
||||
_signPow((quantG - 9) / 9, 2.0) * maximumValue,
|
||||
_signPow((quantB - 9) / 9, 2.0) * maximumValue
|
||||
];
|
||||
|
||||
return rgb;
|
||||
}
|
||||
|
||||
bool validateBlurhash(String blurhash) {
|
||||
if (blurhash.isEmpty || blurhash.length < 6) {
|
||||
debugPrint('Blurhash should be at least 6 characters');
|
||||
return false;
|
||||
}
|
||||
|
||||
final sizeFlag = _decode83(blurhash[0]);
|
||||
final y = ((sizeFlag / 9) + 1).floor();
|
||||
final x = (sizeFlag % 9) + 1;
|
||||
|
||||
if (blurhash.length != 4 + 2 * x * y) {
|
||||
debugPrint(
|
||||
"blurhash length mismatch: length is ${blurhash.length} but it should be ${4 + 2 * x * y}");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const _digitCharacters =
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#\$%*+,-.:;=?@[]^_{|}~";
|
||||
62
flutter_blurhash/lib/src/blurhash_image.dart
Normal file
62
flutter_blurhash/lib/src/blurhash_image.dart
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter_blurhash/flutter_blurhash.dart';
|
||||
|
||||
const _DEFAULT_SIZE = 32;
|
||||
|
||||
class BlurHashImage extends ImageProvider<BlurHashImage> {
|
||||
/// Creates an object that decodes a [blurHash] as an image.
|
||||
///
|
||||
/// The arguments must not be null.
|
||||
const BlurHashImage(this.blurHash,
|
||||
{this.decodingWidth = _DEFAULT_SIZE,
|
||||
this.decodingHeight = _DEFAULT_SIZE,
|
||||
this.scale = 1.0});
|
||||
|
||||
/// The bytes to decode into an image.
|
||||
final String blurHash;
|
||||
|
||||
/// The scale to place in the [ImageInfo] object of the image.
|
||||
final double scale;
|
||||
|
||||
/// Decoding definition
|
||||
final int decodingWidth;
|
||||
|
||||
/// Decoding definition
|
||||
final int decodingHeight;
|
||||
|
||||
@override
|
||||
Future<BlurHashImage> obtainKey(ImageConfiguration configuration) =>
|
||||
SynchronousFuture<BlurHashImage>(this);
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(
|
||||
BlurHashImage key, ImageDecoderCallback decode) =>
|
||||
OneFrameImageStreamCompleter(_loadAsync(key));
|
||||
|
||||
Future<ImageInfo> _loadAsync(BlurHashImage key) async {
|
||||
assert(key == this);
|
||||
|
||||
final image = await blurHashDecodeImage(
|
||||
blurHash: blurHash,
|
||||
width: decodingWidth,
|
||||
height: decodingHeight,
|
||||
);
|
||||
return ImageInfo(image: image, scale: key.scale);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => other.runtimeType != runtimeType
|
||||
? false
|
||||
: other is BlurHashImage &&
|
||||
other.blurHash == blurHash &&
|
||||
other.scale == scale;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(blurHash.hashCode, scale);
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType($blurHash, scale: $scale)';
|
||||
}
|
||||
248
flutter_blurhash/lib/src/blurhash_widget.dart
Normal file
248
flutter_blurhash/lib/src/blurhash_widget.dart
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_blurhash/flutter_blurhash.dart';
|
||||
|
||||
const _DEFAULT_SIZE = 32;
|
||||
|
||||
/// Displays the blurhash [hash] and the fades into the [image] over the course
|
||||
/// of [duration].
|
||||
class BlurHash extends StatefulWidget {
|
||||
const BlurHash({
|
||||
required this.hash,
|
||||
Key? key,
|
||||
this.color = Colors.blueGrey,
|
||||
this.imageFit = BoxFit.fill,
|
||||
this.decodingWidth = _DEFAULT_SIZE,
|
||||
this.decodingHeight = _DEFAULT_SIZE,
|
||||
this.image,
|
||||
this.onDecoded,
|
||||
this.onDisplayed,
|
||||
this.onReady,
|
||||
this.onStarted,
|
||||
this.duration = const Duration(milliseconds: 1000),
|
||||
this.httpHeaders = const {},
|
||||
this.curve = Curves.easeOut,
|
||||
this.errorBuilder,
|
||||
this.optimizationMode = BlurHashOptimizationMode.none,
|
||||
}) : assert(decodingWidth > 0),
|
||||
assert(decodingHeight != 0),
|
||||
super(key: key);
|
||||
|
||||
/// Callback when hash is decoded
|
||||
final VoidCallback? onDecoded;
|
||||
|
||||
/// Callback when hash is displayed.
|
||||
final VoidCallback? onDisplayed;
|
||||
|
||||
/// Callback when image is downloaded
|
||||
final VoidCallback? onReady;
|
||||
|
||||
/// Callback when image is downloaded
|
||||
final VoidCallback? onStarted;
|
||||
|
||||
/// Hash to decode
|
||||
final String hash;
|
||||
|
||||
/// Displayed background color before decoding
|
||||
final Color color;
|
||||
|
||||
/// How to fit decoded & downloaded image
|
||||
final BoxFit imageFit;
|
||||
|
||||
/// Decoding definition
|
||||
final int decodingWidth;
|
||||
|
||||
/// Decoding definition
|
||||
final int decodingHeight;
|
||||
|
||||
/// Remote resource to download
|
||||
final String? image;
|
||||
|
||||
final Duration duration;
|
||||
|
||||
final Curve curve;
|
||||
|
||||
/// Http headers for secure call like bearer
|
||||
final Map<String, String> httpHeaders;
|
||||
|
||||
/// Network image errorBuilder
|
||||
final ImageErrorWidgetBuilder? errorBuilder;
|
||||
|
||||
/// The optimization mode to use for decoding
|
||||
final BlurHashOptimizationMode optimizationMode;
|
||||
|
||||
@override
|
||||
BlurHashState createState() => BlurHashState();
|
||||
}
|
||||
|
||||
class BlurHashState extends State<BlurHash> {
|
||||
late Future<ui.Image> _image;
|
||||
late bool loaded;
|
||||
late bool loading;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_init();
|
||||
}
|
||||
|
||||
void _init() {
|
||||
_decodeImage();
|
||||
loaded = false;
|
||||
loading = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(BlurHash oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.hash != oldWidget.hash ||
|
||||
widget.image != oldWidget.image ||
|
||||
widget.decodingWidth != oldWidget.decodingWidth ||
|
||||
widget.decodingHeight != oldWidget.decodingHeight ||
|
||||
widget.optimizationMode != oldWidget.optimizationMode) {
|
||||
_init();
|
||||
}
|
||||
}
|
||||
|
||||
void _decodeImage() {
|
||||
_image = blurHashDecodeImage(
|
||||
blurHash: widget.hash,
|
||||
width: widget.decodingWidth,
|
||||
height: widget.decodingHeight,
|
||||
optimizationMode: widget.optimizationMode,
|
||||
);
|
||||
|
||||
_image.whenComplete(() => widget.onDecoded?.call());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Stack(
|
||||
fit: StackFit.expand,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
buildBlurHashBackground(),
|
||||
if (widget.image != null) prepareDisplayedImage(widget.image!),
|
||||
],
|
||||
);
|
||||
|
||||
Widget prepareDisplayedImage(String image) => Image.network(
|
||||
image,
|
||||
fit: widget.imageFit,
|
||||
headers: widget.httpHeaders,
|
||||
errorBuilder: widget.errorBuilder,
|
||||
loadingBuilder: (context, img, loadingProgress) {
|
||||
// Download started
|
||||
if (loading == false) {
|
||||
loading = true;
|
||||
widget.onStarted?.call();
|
||||
}
|
||||
|
||||
if (loadingProgress == null) {
|
||||
// Image is now loaded, trigger the event
|
||||
loaded = true;
|
||||
widget.onReady?.call();
|
||||
return _DisplayImage(
|
||||
child: img,
|
||||
duration: widget.duration,
|
||||
curve: widget.curve,
|
||||
onCompleted: () => widget.onDisplayed?.call(),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/// Decode the blurhash then display the resulting Image
|
||||
Widget buildBlurHashBackground() => FutureBuilder<ui.Image>(
|
||||
future: _image,
|
||||
builder: (ctx, snap) => snap.hasData
|
||||
? Image(image: UiImage(snap.data!), fit: widget.imageFit)
|
||||
: Container(color: widget.color),
|
||||
);
|
||||
}
|
||||
|
||||
// Inner display details & controls
|
||||
class _DisplayImage extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Duration duration;
|
||||
final Curve curve;
|
||||
final VoidCallback onCompleted;
|
||||
|
||||
const _DisplayImage({
|
||||
required this.child,
|
||||
this.duration = const Duration(milliseconds: 800),
|
||||
required this.curve,
|
||||
required this.onCompleted,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_DisplayImageState createState() => _DisplayImageState();
|
||||
}
|
||||
|
||||
class _DisplayImageState extends State<_DisplayImage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late Animation<double> opacity;
|
||||
late AnimationController controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => FadeTransition(
|
||||
opacity: opacity,
|
||||
child: widget.child,
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = AnimationController(duration: widget.duration, vsync: this);
|
||||
final curved = CurvedAnimation(parent: controller, curve: widget.curve);
|
||||
opacity = Tween<double>(begin: .0, end: 1.0).animate(curved);
|
||||
controller.forward();
|
||||
|
||||
curved.addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) widget.onCompleted.call();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class UiImage extends ImageProvider<UiImage> {
|
||||
final ui.Image image;
|
||||
final double scale;
|
||||
|
||||
const UiImage(this.image, {this.scale = 1.0});
|
||||
|
||||
@override
|
||||
Future<UiImage> obtainKey(ImageConfiguration configuration) =>
|
||||
SynchronousFuture<UiImage>(this);
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(UiImage key, ImageDecoderCallback decode) =>
|
||||
OneFrameImageStreamCompleter(_loadAsync(key));
|
||||
|
||||
Future<ImageInfo> _loadAsync(UiImage key) async {
|
||||
assert(key == this);
|
||||
return ImageInfo(image: image, scale: key.scale);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
return other is UiImage && other.image == image && other.scale == scale;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(image.hashCode, scale);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'$runtimeType(${describeIdentity(image)}, scale: $scale)';
|
||||
}
|
||||
30
flutter_blurhash/pubspec.yaml
Executable file
30
flutter_blurhash/pubspec.yaml
Executable file
|
|
@ -0,0 +1,30 @@
|
|||
name: flutter_blurhash
|
||||
description: >
|
||||
Compact representation of placeholder for an image.
|
||||
Encode a blurry image under 30 characters for instant display like used by Medium
|
||||
version: 0.9.1
|
||||
homepage: https://github.com/fluttercommunity/flutter_blurhash
|
||||
repository: https://github.com/fluttercommunity/flutter_blurhash
|
||||
issue_tracker: https://github.com/fluttercommunity/flutter_blurhash/issues
|
||||
maintainer: Robert Felker (@Solido)
|
||||
|
||||
screenshots:
|
||||
- description: 'Content preview as blurred buffer.'
|
||||
path: screenshots/blurred.png
|
||||
- description: 'Content loaded.'
|
||||
path: screenshots/loaded.png
|
||||
|
||||
environment:
|
||||
# flutter: ">=3.16.0"
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
dev_dependencies:
|
||||
|
||||
flutter_lints:
|
||||
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
|
@ -3,8 +3,6 @@ description: Render After Effects animations natively on Flutter. This package i
|
|||
version: 3.5.1
|
||||
repository: https://github.com/xvrh/lottie-flutter
|
||||
|
||||
workspace:
|
||||
- example
|
||||
|
||||
funding:
|
||||
- https://www.buymeacoffee.com/xvrh
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
dependency_overrides:
|
||||
lottie:
|
||||
path: ./dependencies/lottie
|
||||
flutter_blurhash:
|
||||
path: ./dependencies/flutter_blurhash
|
||||
|
|
|
|||
Loading…
Reference in a new issue