Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update toImage for caching. #2

Open
njovy opened this issue Jun 13, 2023 · 0 comments
Open

Update toImage for caching. #2

njovy opened this issue Jun 13, 2023 · 0 comments

Comments

@njovy
Copy link

njovy commented Jun 13, 2023

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 dart-lang/sdk#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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant