FadeInImage官方默认只支持缓存到内存中,在项目中一般都需要把图片缓存到本地文件中
通过观察FadeInImage
的构造函数中,得知image
是调用ResizeImage.resizeIfNeeded(imageCacheWidth, imageCacheHeight, NetworkImage(image, scale: imageScale))
这个方法来获得图片的,而获得ImageProvider
又是通过NetworkImage(image, scale: imageScale)
FadeInImage.assetNetwork({
Key? key,
required String placeholder,
this.placeholderErrorBuilder,
required String image,
this.imageErrorBuilder,
AssetBundle? bundle,
double? placeholderScale,
double imageScale = 1.0,
this.excludeFromSemantics = false,
this.imageSemanticLabel,
this.fadeOutDuration = const Duration(milliseconds: 300),
this.fadeOutCurve = Curves.easeOut,
this.fadeInDuration = const Duration(milliseconds: 700),
this.fadeInCurve = Curves.easeIn,
this.width,
this.height,
this.fit,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.matchTextDirection = false,
int? placeholderCacheWidth,
int? placeholderCacheHeight,
int? imageCacheWidth,
int? imageCacheHeight,
}) : assert(placeholder != null),
assert(image != null),
placeholder = placeholderScale != null
? ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, ExactAssetImage(placeholder, bundle: bundle, scale: placeholderScale))
: ResizeImage.resizeIfNeeded(placeholderCacheWidth, placeholderCacheHeight, AssetImage(placeholder, bundle: bundle)),
assert(imageScale != null),
assert(fadeOutDuration != null),
assert(fadeOutCurve != null),
assert(fadeInDuration != null),
assert(fadeInCurve != null),
assert(alignment != null),
assert(repeat != null),
assert(matchTextDirection != null),
image = ResizeImage.resizeIfNeeded(imageCacheWidth, imageCacheHeight, NetworkImage(image, scale: imageScale)),
super(key: key);
继续跟进发现NetworkImage
是继承ImageProvider
的一个抽象类,里面有个工厂构造函数
abstract class NetworkImage extends ImageProvider<NetworkImage> {
/// Creates an object that fetches the image at the given URL.
///
/// The arguments [url] and [scale] must not be null.
const factory NetworkImage(String url, { double scale, Map<String, String>? headers }) = network_image.NetworkImage;
/// The URL from which the image will be fetched.
String get url;
/// The scale to place in the [ImageInfo] object of the image.
double get scale;
/// The HTTP headers that will be used with [HttpClient.get] to fetch image from network.
///
/// When running flutter on the web, headers are not used.
Map<String, String>? get headers;
@override
ImageStreamCompleter load(NetworkImage key, DecoderCallback decode);
}
通过修改这里的源码来实现本地缓存图片
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'binding.dart';
import 'debug.dart';
import 'image_provider.dart' as image_provider;
import 'image_stream.dart';
/// The dart:io implementation of [image_provider.NetworkImage].
@immutable
class NetworkImage
extends image_provider.ImageProvider<image_provider.NetworkImage>
implements image_provider.NetworkImage {
/// Creates an object that fetches the image at the given URL.
///
/// The arguments [url] and [scale] must not be null.
const NetworkImage(this.url, {this.scale = 1.0, this.headers})
: assert(url != null),
assert(scale != null);
@override
final String url;
@override
final double scale;
@override
final Map<String, String>? headers;
@override
Future<NetworkImage> obtainKey(
image_provider.ImageConfiguration configuration) {
return SynchronousFuture<NetworkImage>(this);
}
@override
ImageStreamCompleter load(
image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
// Ownership of this controller is handed off to [_loadAsync]; it is that
// method's responsibility to close the controller's stream when the image
// has been loaded or an error is thrown.
final StreamController<ImageChunkEvent> chunkEvents =
StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key as NetworkImage, chunkEvents, decode),
chunkEvents: chunkEvents.stream,
scale: key.scale,
debugLabel: key.url,
informationCollector: () {
return <DiagnosticsNode>[
DiagnosticsProperty<image_provider.ImageProvider>(
'Image provider', this),
DiagnosticsProperty<image_provider.NetworkImage>('Image key', key),
];
},
);
}
// Do not access this field directly; use [_httpClient] instead.
// We set `autoUncompress` to false to ensure that we can trust the value of
// the `Content-Length` HTTP header. We automatically uncompress the content
// in our call to [consolidateHttpClientResponseBytes].
static final HttpClient _sharedHttpClient = HttpClient()
..autoUncompress = false;
static HttpClient get _httpClient {
HttpClient client = _sharedHttpClient;
assert(() {
if (debugNetworkImageHttpClientProvider != null)
client = debugNetworkImageHttpClientProvider!();
return true;
}());
return client;
}
Future<ui.Codec> _loadAsync(
NetworkImage key,
StreamController<ImageChunkEvent> chunkEvents,
image_provider.DecoderCallback decode,
) async {
try {
assert(key == this);
final Uri resolved = Uri.base.resolve(key.url);
final HttpClientRequest request = await _httpClient.getUrl(resolved);
headers?.forEach((String name, String value) {
request.headers.add(name, value);
});
final HttpClientResponse response = await request.close();
if (response.statusCode != HttpStatus.ok) {
// The network may be only temporarily unavailable, or the file will be
// added on the server later. Avoid having future calls to resolve
// fail to check the network again.
await response.drain<List<int>>();
throw image_provider.NetworkImageLoadException(
statusCode: response.statusCode, uri: resolved);
}
final Uint8List bytes = await consolidateHttpClientResponseBytes(
response,
onBytesReceived: (int cumulative, int? total) {
chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: cumulative,
expectedTotalBytes: total,
));
},
);
if (bytes.lengthInBytes == 0)
throw Exception('NetworkImage is an empty file: $resolved');
return decode(bytes);
} catch (e) {
// Depending on where the exception was thrown, the image cache may not
// have had a chance to track the key in the cache at all.
// Schedule a microtask to give the cache a chance to add the key.
scheduleMicrotask(() {
PaintingBinding.instance!.imageCache!.evict(key);
});
rethrow;
} finally {
chunkEvents.close();
}
}
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
return other is NetworkImage && other.url == url && other.scale == scale;
}
@override
int get hashCode => ui.hashValues(url, scale);
@override
String toString() =>
'${objectRuntimeType(this, 'NetworkImage')}("$url", scale: $scale)';
}
在这之前需要先导入
path_provider: ^2.0.6
-
crypto: ^3.0.1
注意_loadAsync
这个方法才是真正获取网络图片的实现.
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'dart:convert' as convert;
import 'package:flutter/foundation.dart';
import 'package:crypto/crypto.dart';
import 'package:path_provider/path_provider.dart';
@immutable
class CustomLocalCacheNetworkImage extends ImageProvider<NetworkImage>
implements NetworkImage {
const CustomLocalCacheNetworkImage(
this.url, {
this.scale = 1.0,
this.headers,
this.isLocalCache = false,
}) : assert(url != null),
assert(scale != null);
@override
final String url;
@override
final double scale;
@override
final Map<String, String>? headers;
final bool isLocalCache;
@override
Future<NetworkImage> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<NetworkImage>(this);
}
@override
ImageStreamCompleter load(NetworkImage key, DecoderCallback decode) {
// Ownership of this controller is handed off to [_loadAsync]; it is that
// method's responsibility to close the controller's stream when the image
// has been loaded or an error is thrown.
final StreamController<ImageChunkEvent> chunkEvents =
StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key as NetworkImage, chunkEvents, decode),
chunkEvents: chunkEvents.stream,
scale: key.scale,
debugLabel: key.url,
informationCollector: () {
return <DiagnosticsNode>[
DiagnosticsProperty<ImageProvider>('Image provider', this),
DiagnosticsProperty<NetworkImage>('Image key', key),
];
},
);
}
static final HttpClient _sharedHttpClient = HttpClient()
..autoUncompress = false;
static HttpClient get _httpClient {
HttpClient client = _sharedHttpClient;
assert(() {
if (debugNetworkImageHttpClientProvider != null)
client = debugNetworkImageHttpClientProvider!();
return true;
}());
return client;
}
Future<ui.Codec> _loadAsync(
NetworkImage key,
StreamController<ImageChunkEvent> chunkEvents,
DecoderCallback decode,
) async {
try {
assert(key == this);
//检查是否有本地缓存,有则直接返回图片
if (isLocalCache != null && isLocalCache == true) {
final Uint8List? bytes = await _getImageFromLocal(key.url);
if (bytes != null && bytes.length != null && bytes.lengthInBytes != 0) {
return await PaintingBinding.instance!.instantiateImageCodec(bytes);
}
}
final Uri resolved = Uri.base.resolve(key.url);
final HttpClientRequest request = await _httpClient.getUrl(resolved);
headers?.forEach((String name, String value) {
request.headers.add(name, value);
});
final HttpClientResponse response = await request.close();
if (response.statusCode != HttpStatus.ok) {
// The network may be only temporarily unavailable, or the file will be
// added on the server later. Avoid having future calls to resolve
// fail to check the network again.
await response.drain<List<int>>();
throw NetworkImageLoadException(
statusCode: response.statusCode, uri: resolved);
}
final Uint8List bytes = await consolidateHttpClientResponseBytes(
response,
onBytesReceived: (int cumulative, int? total) {
chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: cumulative,
expectedTotalBytes: total,
));
},
);
//网络请求结束后,将图片缓存到本地
if (isLocalCache != null &&
isLocalCache == true &&
bytes.lengthInBytes != 0) {
_saveImageToLocal(bytes, key.url);
}
if (bytes.lengthInBytes == 0)
throw Exception('NetworkImage is an empty file: $resolved');
return decode(bytes);
} catch (e) {
// Depending on where the exception was thrown, the image cache may not
// have had a chance to track the key in the cache at all.
// Schedule a microtask to give the cache a chance to add the key.
scheduleMicrotask(() {
PaintingBinding.instance!.imageCache!.evict(key);
});
rethrow;
} finally {
chunkEvents.close();
}
}
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
return other is NetworkImage && other.url == url && other.scale == scale;
}
@override
int get hashCode => ui.hashValues(url, scale);
@override
String toString() =>
'${objectRuntimeType(this, 'NetworkImage')}("$url", scale: $scale)';
void _saveImageToLocal(Uint8List list, String name) async {
String path = await _getCachePathString(name);
var file = File(path);
bool isExist = await file.exists();
if (!isExist) {
File(path).writeAsBytesSync(list);
}
}
//本地获取图片
Future<Uint8List?> _getImageFromLocal(String name) async {
String path = await _getCachePathString(name);
var file = File(path);
print('url_------>$path');
bool isExist = await file.exists();
if (isExist) {
final Uint8List bytes = await file.readAsBytes();
return bytes;
}
return null;
}
//获取图片的缓存路径并创建
Future<String> _getCachePathString(String name) async {
String fileName = md5.convert(convert.utf8.encode(name)).toString();
String extensionName = name.split('/').last.split('.').last;
final tempDir = await getTemporaryDirectory();
Directory directory = Directory('${tempDir.path}/CacheImages/');
bool isExistFold = await directory.exists();
if (!isExistFold) {
await directory.create();
}
return '${directory.path}$fileName.$extensionName';
}
}
class CustomFadeInImage extends StatefulWidget {
const CustomFadeInImage({
Key? key,
required this.placeholder,
this.placeholderErrorBuilder,
required this.image,
this.imageErrorBuilder,
this.excludeFromSemantics = false,
this.imageSemanticLabel,
this.fadeOutDuration = const Duration(milliseconds: 300),
this.fadeOutCurve = Curves.easeOut,
this.fadeInDuration = const Duration(milliseconds: 700),
this.fadeInCurve = Curves.easeIn,
this.width,
this.height,
this.fit,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.matchTextDirection = false,
}) : assert(placeholder != null),
assert(image != null),
assert(fadeOutDuration != null),
assert(fadeOutCurve != null),
assert(fadeInDuration != null),
assert(fadeInCurve != null),
assert(alignment != null),
assert(repeat != null),
assert(matchTextDirection != null),
super(key: key);
CustomFadeInImage.memoryNetwork({
Key? key,
required Uint8List placeholder,
this.placeholderErrorBuilder,
required String image,
this.imageErrorBuilder,
double placeholderScale = 1.0,
double imageScale = 1.0,
this.excludeFromSemantics = false,
this.imageSemanticLabel,
this.fadeOutDuration = const Duration(milliseconds: 300),
this.fadeOutCurve = Curves.easeOut,
this.fadeInDuration = const Duration(milliseconds: 700),
this.fadeInCurve = Curves.easeIn,
this.width,
this.height,
this.fit,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.matchTextDirection = false,
int? placeholderCacheWidth,
int? placeholderCacheHeight,
int? imageCacheWidth,
int? imageCacheHeight,
}) : assert(placeholder != null),
assert(image != null),
assert(placeholderScale != null),
assert(imageScale != null),
assert(fadeOutDuration != null),
assert(fadeOutCurve != null),
assert(fadeInDuration != null),
assert(fadeInCurve != null),
assert(alignment != null),
assert(repeat != null),
assert(matchTextDirection != null),
placeholder = ResizeImage.resizeIfNeeded(
placeholderCacheWidth,
placeholderCacheHeight,
MemoryImage(placeholder, scale: placeholderScale)),
image = ResizeImage.resizeIfNeeded(
imageCacheWidth,
imageCacheHeight,
CustomLocalCacheNetworkImage(image,
scale: imageScale, isLocalCache: true)),
super(key: key);
CustomFadeInImage.assetNetwork({
Key? key,
required String placeholder,
this.placeholderErrorBuilder,
required String image,
this.imageErrorBuilder,
AssetBundle? bundle,
double? placeholderScale,
double imageScale = 1.0,
this.excludeFromSemantics = false,
this.imageSemanticLabel,
this.fadeOutDuration = const Duration(milliseconds: 300),
this.fadeOutCurve = Curves.easeOut,
this.fadeInDuration = const Duration(milliseconds: 700),
this.fadeInCurve = Curves.easeIn,
this.width,
this.height,
this.fit,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.matchTextDirection = false,
int? placeholderCacheWidth,
int? placeholderCacheHeight,
int? imageCacheWidth,
int? imageCacheHeight,
}) : assert(placeholder != null),
assert(image != null),
placeholder = placeholderScale != null
? ResizeImage.resizeIfNeeded(
placeholderCacheWidth,
placeholderCacheHeight,
ExactAssetImage(placeholder,
bundle: bundle, scale: placeholderScale))
: ResizeImage.resizeIfNeeded(
placeholderCacheWidth,
placeholderCacheHeight,
AssetImage(placeholder, bundle: bundle)),
assert(imageScale != null),
assert(fadeOutDuration != null),
assert(fadeOutCurve != null),
assert(fadeInDuration != null),
assert(fadeInCurve != null),
assert(alignment != null),
assert(repeat != null),
assert(matchTextDirection != null),
image = ResizeImage.resizeIfNeeded(
imageCacheWidth,
imageCacheHeight,
CustomLocalCacheNetworkImage(image,
scale: imageScale, isLocalCache: true)),
super(key: key);
final ImageProvider placeholder;
final ImageErrorWidgetBuilder? placeholderErrorBuilder;
final ImageProvider image;
final ImageErrorWidgetBuilder? imageErrorBuilder;
final Duration fadeOutDuration;
final Curve fadeOutCurve;
final Duration fadeInDuration;
final Curve fadeInCurve;
final double? width;
final double? height;
final BoxFit? fit;
final AlignmentGeometry alignment;
final ImageRepeat repeat;
final bool matchTextDirection;
final bool excludeFromSemantics;
final String? imageSemanticLabel;
@override
State<CustomFadeInImage> createState() => _CustomFadeInImageState();
}
class _CustomFadeInImageState extends State<CustomFadeInImage> {
static const Animation<double> _kOpaqueAnimation =
AlwaysStoppedAnimation<double>(1.0);
final ProxyAnimation _imageAnimation = ProxyAnimation(_kOpaqueAnimation);
final ProxyAnimation _placeholderAnimation =
ProxyAnimation(_kOpaqueAnimation);
void _resetAnimations() {
_imageAnimation.parent = _kOpaqueAnimation;
_placeholderAnimation.parent = _kOpaqueAnimation;
}
Image _image({
required ImageProvider image,
ImageErrorWidgetBuilder? errorBuilder,
ImageFrameBuilder? frameBuilder,
required Animation<double> opacity,
}) {
assert(image != null);
return Image(
image: image,
errorBuilder: errorBuilder,
frameBuilder: frameBuilder,
opacity: opacity,
width: widget.width,
height: widget.height,
fit: widget.fit,
alignment: widget.alignment,
repeat: widget.repeat,
matchTextDirection: widget.matchTextDirection,
gaplessPlayback: true,
excludeFromSemantics: true,
);
}
@override
Widget build(BuildContext context) {
Widget result = _image(
image: widget.image,
errorBuilder: widget.imageErrorBuilder,
opacity: _imageAnimation,
frameBuilder: (BuildContext context, Widget child, int? frame,
bool wasSynchronouslyLoaded) {
if (wasSynchronouslyLoaded) {
_resetAnimations();
return child;
}
return _AnimatedFadeOutFadeIn(
target: child,
targetProxyAnimation: _imageAnimation,
placeholder: _image(
image: widget.placeholder,
errorBuilder: widget.placeholderErrorBuilder,
opacity: _placeholderAnimation,
),
placeholderProxyAnimation: _placeholderAnimation,
isTargetLoaded: frame != null,
fadeInDuration: widget.fadeInDuration,
fadeOutDuration: widget.fadeOutDuration,
fadeInCurve: widget.fadeInCurve,
fadeOutCurve: widget.fadeOutCurve,
);
},
);
if (!widget.excludeFromSemantics) {
result = Semantics(
container: widget.imageSemanticLabel != null,
image: true,
label: widget.imageSemanticLabel ?? '',
child: result,
);
}
return result;
}
}
class _AnimatedFadeOutFadeIn extends ImplicitlyAnimatedWidget {
const _AnimatedFadeOutFadeIn({
Key? key,
required this.target,
required this.targetProxyAnimation,
required this.placeholder,
required this.placeholderProxyAnimation,
required this.isTargetLoaded,
required this.fadeOutDuration,
required this.fadeOutCurve,
required this.fadeInDuration,
required this.fadeInCurve,
}) : assert(target != null),
assert(placeholder != null),
assert(isTargetLoaded != null),
assert(fadeOutDuration != null),
assert(fadeOutCurve != null),
assert(fadeInDuration != null),
assert(fadeInCurve != null),
super(key: key, duration: fadeInDuration + fadeOutDuration);
final Widget target;
final ProxyAnimation targetProxyAnimation;
final Widget placeholder;
final ProxyAnimation placeholderProxyAnimation;
final bool isTargetLoaded;
final Duration fadeInDuration;
final Duration fadeOutDuration;
final Curve fadeInCurve;
final Curve fadeOutCurve;
@override
_AnimatedFadeOutFadeInState createState() => _AnimatedFadeOutFadeInState();
}
class _AnimatedFadeOutFadeInState
extends ImplicitlyAnimatedWidgetState<_AnimatedFadeOutFadeIn> {
Tween<double>? _targetOpacity;
Tween<double>? _placeholderOpacity;
Animation<double>? _targetOpacityAnimation;
Animation<double>? _placeholderOpacityAnimation;
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
_targetOpacity = visitor(
_targetOpacity,
widget.isTargetLoaded ? 1.0 : 0.0,
(dynamic value) => Tween<double>(begin: value as double),
) as Tween<double>?;
_placeholderOpacity = visitor(
_placeholderOpacity,
widget.isTargetLoaded ? 0.0 : 1.0,
(dynamic value) => Tween<double>(begin: value as double),
) as Tween<double>?;
}
@override
void didUpdateTweens() {
_placeholderOpacityAnimation =
animation.drive(TweenSequence<double>(<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween:
_placeholderOpacity!.chain(CurveTween(curve: widget.fadeOutCurve)),
weight: widget.fadeOutDuration.inMilliseconds.toDouble(),
),
TweenSequenceItem<double>(
tween: ConstantTween<double>(0),
weight: widget.fadeInDuration.inMilliseconds.toDouble(),
),
]))
..addStatusListener((AnimationStatus status) {
if (_placeholderOpacityAnimation!.isCompleted) {
// Need to rebuild to remove placeholder now that it is invisible.
setState(() {});
}
});
_targetOpacityAnimation =
animation.drive(TweenSequence<double>(<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: ConstantTween<double>(0),
weight: widget.fadeOutDuration.inMilliseconds.toDouble(),
),
TweenSequenceItem<double>(
tween: _targetOpacity!.chain(CurveTween(curve: widget.fadeInCurve)),
weight: widget.fadeInDuration.inMilliseconds.toDouble(),
),
]));
if (!widget.isTargetLoaded &&
_isValid(_placeholderOpacity!) &&
_isValid(_targetOpacity!)) {
// Jump (don't fade) back to the placeholder image, so as to be ready
// for the full animation when the new target image becomes ready.
controller.value = controller.upperBound;
}
widget.targetProxyAnimation.parent = _targetOpacityAnimation;
widget.placeholderProxyAnimation.parent = _placeholderOpacityAnimation;
}
bool _isValid(Tween<double> tween) {
return tween.begin != null && tween.end != null;
}
@override
Widget build(BuildContext context) {
if (_placeholderOpacityAnimation!.isCompleted) {
return widget.target;
}
return Stack(
fit: StackFit.passthrough,
alignment: AlignmentDirectional.center,
// Text direction is irrelevant here since we're using center alignment,
// but it allows the Stack to avoid a call to Directionality.of()
textDirection: TextDirection.ltr,
children: <Widget>[
widget.target,
widget.placeholder,
],
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Animation<double>>(
'targetOpacity', _targetOpacityAnimation));
properties.add(DiagnosticsProperty<Animation<double>>(
'placeholderOpacity', _placeholderOpacityAnimation));
}
}