PiN73 / thumbhash

Dart/Flutter implementation of ThumbHash algorithm — a very compact representation of an image placeholder
5 stars 1 forks source link

Update toImage for caching. #2

Open njovy opened 1 year ago

njovy commented 1 year ago

My app happened to load identical thumbhashes in a list, and I realized that the current version of thumbhash toImage() never got loaded from the cache. They blink every time you scroll up and down. (Already set gaplessPlayback: true)

The == operator and hashCode for Uint8List don't work as expected (see https://github.com/dart-lang/sdk/issues/16335). The code provided below is a minimal example that you can use instead of toImage(). In the Flutter framework, images are cached, but if you use MemoryImage, it won't utilize the cache because it relies on the Uint8List hashCode, which means it will never be the same even if the content is identical.

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:thumbhash/thumbhash.dart' as thumb;
import 'dart:ui' as ui;

Future<ui.Image> thumbHashDecodeImage({
  required Uint8List thumbHash,
}) async {
  final completer = Completer<ui.Image>();

  final image = thumb.thumbHashToRGBA(thumbHash);
  ui.decodeImageFromPixels(image.rgba, image.width, image.height,
      ui.PixelFormat.rgba8888, completer.complete);

  return completer.future;
}

class ThumbHashImage extends ImageProvider<ThumbHashImage> {
  /// Creates an object that decodes a [thumbHash] as an image.
  ///
  /// The arguments must not be null.
  const ThumbHashImage(this.thumbHash, {this.scale = 1.0});

  /// The bytes to decode into an image.
  final Uint8List thumbHash;

  /// The scale to place in the [ImageInfo] object of the image.
  final double scale;

  @override
  Future<ThumbHashImage> obtainKey(ImageConfiguration configuration) =>
      SynchronousFuture<ThumbHashImage>(this);

  @override
  ImageStreamCompleter load(ThumbHashImage key, DecoderCallback decode) =>
      OneFrameImageStreamCompleter(_loadAsync(key));

  Future<ImageInfo> _loadAsync(ThumbHashImage key) async {
    assert(key == this);
    final image = await thumbHashDecodeImage(
      thumbHash: thumbHash,
    );
    return ImageInfo(image: image, scale: key.scale);
  }

  @override
  bool operator ==(Object other) => other.runtimeType != runtimeType
      ? false
      : other is ThumbHashImage &&
          memEquals(thumbHash, other.thumbHash) &&
          other.scale == scale;

  @override
  int get hashCode => thumbHash.reduce((value, element) => value + element.hashCode);

  @override
  String toString() => '$runtimeType($thumbHash, scale: $scale)';
}

bool memEquals(Uint8List bytes1, Uint8List bytes2) {
  if (identical(bytes1, bytes2)) {
    return true;
  }

  if (bytes1.lengthInBytes != bytes2.lengthInBytes) {
    return false;
  }

  // Treat the original byte lists as lists of 8-byte words.
  var numWords = bytes1.lengthInBytes ~/ 8;
  var words1 = bytes1.buffer.asUint64List(0, numWords);
  var words2 = bytes2.buffer.asUint64List(0, numWords);

  for (var i = 0; i < words1.length; i += 1) {
    if (words1[i] != words2[i]) {
      return false;
    }
  }

  // Compare any remaining bytes.
  for (var i = words1.lengthInBytes; i < bytes1.lengthInBytes; i += 1) {
    if (bytes1[i] != bytes2[i]) {
      return false;
    }
  }

  return true;
}

hashCode can be improved with a better hashCode implementation.