2

我正在尝试在GridView.builder小部件中加载手机的图库(带分页)。

这是我使用包创建的问题。 我得到了一些帮助,这让我想到了一个可能的解决方案(请参阅我对这个问题的最后评论)。photo_manager

我希望能够在不闪烁或白页的情况下加载资产。
在 IOS 本机上它非常快速和流畅,我想在 Flutter 中实现同样的效果。

您将在上面的 github 链接中找到我制作的所有代码。我已经设法使用内存中的对象来做到这一点Map,但我需要改进算法以使其不在 OOM 中。

想要的解决方案(一个或另一个):

  • 一个简单的方法是,将手机的图库加载到与本地 IOS 一样快的速度GridView,无论它在工作时使用哪个包。
  • 对我目前较差的算法的改进,例如将 15 个资产保持在当前资产之上,15 个资产在内存中和滚动期间,不断更新这些值以围绕列表中的当前位置移动范围。

如果这还不够清楚,请告诉我,作为提醒,请查看我对这个问题的最后一个重要评论。

4

2 回答 2

1

您可以使用这样的逻辑:

final Map<String, Uint8List?> _cachedMap = {};
void precacheAssets(int index) async {
    // Handle cache before index
    for (int i = max(0, index - 50); i < 50; i++) {
      getItemAtIndex(i);
    }
    // Handle cache after index
    for (int i = min(assetsList.length, index + 50); i < 50 + min(assetsList.length, index + 50); i++) {
      getItemAtIndex(i);
    }
    _cachedMap.removeWhere((key, value) {
      int currIndex = assetsList.indexWhere((element) => element.id == key);
      return currIndex < index - 50 && currIndex > index + 50;
    });
  }
  /// Get the asset from memory or fetch it if it doesn’t exist yet.
  /// Called in the builder method to display assets, not to precache them.
  Future<Uint8List?> getItemAtIndex(int index) async {
    AssetEntity entity = assetsList[index];
    if (_cachedMap.containsKey(entity.id)) {
      return _cachedMap[entity.id];
    }
    else {
      Uint8List? thumb = await entity.thumbDataWithOption(
          ThumbOption.ios(
              width: width,
              height: height,
              deliveryMode: DeliveryMode.highQualityFormat,
              quality: 90));
      _cachedMap[entity.id] = thumb;
      return thumb;
    }
  }

例如,您可以在特定索引处调用该precacheAssets方法,该方法将告诉每 25 个项目,将 50 个下一个项目放入缓存中,这样它将向现有缓存中添加 25 个更多项目。GridView.builderif (index % 25 == 0)

此外,调用as参数,如果它在内存中,getItemAtIndex您将立即获得资产,否则照常加载。Future.builderfuture

随意更改值并对其进行测试,我的 iPhone 中的这些值已经改进了它,但如果你滚动得非常快,你仍然会看到和以前一样。

在这种情况下,您可以添加一个FadeTransition,这将产生一个不难看的 UI。

于 2021-11-01T13:13:54.393 回答
0

解决方案非常简单:GridView用拇指而不是大图像填充您的:

// Load [AssetEntity]s:
images = await album.getAssetListRange(start: 0, end: 50);

// Then build [GridView]:
GridView.builder(
  gridDelegate:
      const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 4,
    crossAxisSpacing: 3,
    mainAxisSpacing: 3,
  ),
  itemCount: images.length,
  itemBuilder: (context, index) {
    final image = images[index];
    return Image(
      image: DeviceImage(
        image,
        size: const Size(200, 200),
      ),
      fit: BoxFit.cover,
    );
  },
);

如您所见,我传递AssetEntityDeviceImage提供者。DeviceImage加载大小为 (200, 200) 的原始字节,并Image显示拇指。

这是DeviceImage代码,我基于以下代码构建它local_image_provider

import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui' as ui show Codec;
import 'dart:ui';

import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/services.dart';
import 'package:photo_manager/photo_manager.dart';

/// Decodes the given [LocalImage] object as an [ImageProvider], associating it with the given
/// scale.
///
///
/// In general only the constructor of this object should be used directly,
/// after that use the resulting object wherever an [ImageProvider] is
/// needed in Flutter. In particular with an [Image] widget.
///
class DeviceImage extends ImageProvider<DeviceImage> {
  static const int _kMaxSize = 1200;
  static const String _kNoImageBase64 =
      "iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAIAAABLixI0AAAAA3NCSVQICAjb4U/gAAADCUlEQVQ4ja2VT0zTYBTAX9dttB3b6AzFAEIlgEaH7gRxHIyBYGIkGMNAQpaBCdEYjiacPHD0QCQSlcSL8aaZhIN/4xBFlkVj5mY0hgMskwDyt5SNtdBu9VDp1m6AGt/p/fm+33vv+16/IpIkwX8S/d7hZCy2NTMtzs/rS0ryDlegZvPfs1IpZsQbuzeYYlcz3Yi5wNx1lex0I/ocG5HsHoWlxcUr3eLs9G75dYXF1NB9rLJyH5YkCHOdLnHme9qF6nU2SoqxEr+Zxh04WPLkGZqfr8qhYa/cvpUG6VBLb1+5P1z2Yrx8Mlh496GuoFCOpFZ/Lvff0NabafA8n3j9XDFN167j7R28IHAcx3GcruYEOXAnvTjwbovjcvSYSCS8Xi83NXX2lVcObOkMo+fbthFEk9z19BGWFGR9vKEFPW5vbW0lCAKUewyFQtFoFDBstLmDjMdNsY0UimaDiriEAkoCEsNxNhoNhUJOpzPNYhhGVqiq6tra2rW1Nb/fD/F4JsiYStmDHxSTMZMsqs/cqxoTDMNcLpfBYAAAk8k0MjKihMxJsf79WCG7LJsSwGzVMU3VKlZxcbEMAgCaptOgVPL0m5dkYl0Bfaup+3qIBrWoWJFIhGVZq9UKAOFwWGnNOeFTQADIRsulz4BClqhYkiQNDw/b7XaGYSKRiOys/zhJ7bQGANa+ftrVvuzzBQIBDUs7q9vb28FgcGFhQa6uQBRLF38oUUtvH+lqB4DGxsa6urp9WABAEITH4+np6aEoqmhlSfFjZ87Zui4rZlNTE4Zhe7EIgnC73RRF4TjudrtLd6YJAIw1JzWLEfUAqs4LRVEZpHCPnqrfnIvKZl71kewmdmUZjUYFJIvN023zdO+NyM0SBEFzO5a3Y8aJMVkX7Y71i22a9TlYJEkCgCiKPp8vM1wf+lSxPC/rK1/AZ7FllyPvBeXsHQ5H5qD/udA07XA4ZF31rvI8r3lm2aFB/vEDWTc4G2w3BzKjCIJkjoX229akRZov8FW/r89QVobj+B415vh3/LP8AvvVK04ZJmjyAAAAAElFTkSuQmCC";
  static final Uint8List noImageBytes = base64Decode(_kNoImageBase64);

  /// Creates an object that decodes a [LocalImage] as an image.
  ///
  /// The arguments must not be null. [scale] returns a scaled down
  /// version of the image. For example to load a thumbnail you could
  /// use something like .1 as the scale. There's a convenience method
  /// on [LocalImage] that can calculate the scale for a given pixel
  /// size.
  /// [minPixels] can be used to specify a minimum independent of the
  /// requested scale. The idea is that scaling an image that you don't know
  /// the original size of can result in some results that are too small.
  /// If the goal is to display the image in a 50x50 thumbnail then you might
  /// want to set 50 as the minPixels, then regardless of the image size and
  /// scale you'll get at least 50 pixels in each dimension. This parameter
  /// was added as a result of a strange result in iOS where an image with
  /// a portrait aspect ratio was failing to load when scaled below 120 pixels.
  /// Setting 150 as the minimum in this case resolved the problem.
  const DeviceImage(this.assetEntity,
      {this.scale = 1.0, this.minPixels = 0, this.quality = 70, this.size});

  /// The LocalImage to decode into an image.
  final AssetEntity assetEntity;

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

  /// The minPixels to place in the [ImageInfo] object of the image.
  final int minPixels;

  /// Optional image quality (0-100), default is set to 70.
  final int quality;

  /// Optional image size. If null, then full size will be loaded.
  final Size? size;

  @override
  Future<DeviceImage> obtainKey(ImageConfiguration? configuration) {
    return SynchronousFuture<DeviceImage>(this);
  }

  @override
  ImageStreamCompleter load(DeviceImage key, DecoderCallback decode) {
    return MultiFrameImageStreamCompleter(
      codec: _loadAsync(key, decode),
      scale: key.scale,
      informationCollector: () sync* {
        yield ErrorDescription('Id: ${assetEntity.id}');
      },
    );
  }

  @override
  void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream,
      DeviceImage key, ImageErrorListener handleError) {
    if (shouldCache()) {
      super.resolveStreamForKey(configuration, stream, key, handleError);
      return;
    }
    final ImageStreamCompleter completer =
        load(key, PaintingBinding.instance!.instantiateImageCodec);
    stream.setCompleter(completer);
  }

  int get height => max((assetEntity.height * scale).round(), minPixels);
  int get width => max((assetEntity.width * scale).round(), minPixels);

  @visibleForTesting
  bool shouldCache() {
    return size == null;
  }

  Future<ui.Codec> _loadAsync(DeviceImage key, DecoderCallback decoder) async {
    assert(key == this);
    try {
      final int width;
      final int height;
      if (size == null) {
        width = _kMaxSize;
        height = _kMaxSize;
      } else {
        width = size!.width.toInt();
        height = size!.height.toInt();
      }
      final bytes = await assetEntity.thumbDataWithSize(
        width,
        height,
        quality: quality,
      );
      if (bytes == null || bytes.lengthInBytes == 0) {
        return decoder(noImageBytes);
      }

      return await decoder(bytes);
    } on PlatformException {
      return await decoder(noImageBytes);
    }
  }

  @override
  bool operator ==(dynamic other) {
    if (other.runtimeType != runtimeType) return false;
    final DeviceImage typedOther = other;
    return assetEntity.id == typedOther.assetEntity.id &&
        scale == typedOther.scale;
  }

  @override
  int get hashCode => assetEntity.hashCode;

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

注意:代码不是生产就绪的。

于 2021-10-28T16:01:27.790 回答