Skip to content

Commit

Permalink
Refactor image retrieval (#320)
Browse files Browse the repository at this point in the history
This makes the image retrieval process not rely on a FutureBuilder.
Previously we recreated Image behavior by showing a loader whilst the image loads, using a completer to detect when the image had finished load.
Now we rely only on the good old state and some ifs in the build method.
Also, we have split that code from the custom child code. The main widget had logics for both image and custom child modes. Now we split that into two internal widgets, the wrappers.

This should resolve the following issues #316 #303
  • Loading branch information
renancaraujo authored Aug 22, 2020
1 parent c4467a3 commit 6cdd860
Show file tree
Hide file tree
Showing 4 changed files with 356 additions and 174 deletions.
14 changes: 9 additions & 5 deletions example/lib/screens/examples/hero_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,22 @@ class HeroExample extends StatelessWidget {
context,
MaterialPageRoute(
builder: (context) => const HeroPhotoViewRouteWrapper(
imageProvider: AssetImage("assets/large-image.jpg"),
imageProvider: NetworkImage(
"https://source.unsplash.com/4900x3600/?camera,paper",
),
),
),
);
},
child: Container(
child: Hero(
tag: "someTag",
child: Image.asset("assets/large-image.jpg", width: 150.0),
child: Image.network(
"https://source.unsplash.com/4900x3600/?camera,paper",
width: 350.0,
loadingBuilder: (_, child, chunk) =>
chunk != null ? const Text("loading") : child,
),
),
),
),
Expand All @@ -35,14 +42,12 @@ class HeroExample extends StatelessWidget {
class HeroPhotoViewRouteWrapper extends StatelessWidget {
const HeroPhotoViewRouteWrapper({
this.imageProvider,
this.loadingBuilder,
this.backgroundDecoration,
this.minScale,
this.maxScale,
});

final ImageProvider imageProvider;
final LoadingBuilder loadingBuilder;
final Decoration backgroundDecoration;
final dynamic minScale;
final dynamic maxScale;
Expand All @@ -55,7 +60,6 @@ class HeroPhotoViewRouteWrapper extends StatelessWidget {
),
child: PhotoView(
imageProvider: imageProvider,
loadingBuilder: loadingBuilder,
backgroundDecoration: backgroundDecoration,
minScale: minScale,
maxScale: maxScale,
Expand Down
5 changes: 3 additions & 2 deletions example/lib/screens/examples/network-images.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ class NetworkExamples extends StatelessWidget {
MaterialPageRoute(
builder: (context) => CommonExampleRouteWrapper(
imageProvider: const NetworkImage(
"https://source.unsplash.com/1900x3600/?camera,paper"),
"https://source.unsplash.com/1900x3600/?camera,paper",
),
loadingBuilder: (context, event) {
if (event == null) {
return const Center(
Expand All @@ -40,7 +41,7 @@ class NetworkExamples extends StatelessWidget {
},
),
ExampleButtonNode(
title: "Image from the internet (with custom loader)",
title: "Error image",
onPressed: () {
Navigator.push(
context,
Expand Down
231 changes: 64 additions & 167 deletions lib/photo_view.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
library photo_view;

import 'dart:async';

import 'package:flutter/material.dart';

import 'package:photo_view/src/controller/photo_view_controller.dart';
import 'package:photo_view/src/controller/photo_view_scalestate_controller.dart';
import 'package:photo_view/src/core/photo_view_core.dart';
import 'package:photo_view/src/photo_view_computed_scale.dart';
import 'package:photo_view/src/photo_view_default_widgets.dart';
import 'package:photo_view/src/photo_view_scale_state.dart';
import 'package:photo_view/src/photo_view_wrappers.dart';
import 'package:photo_view/src/utils/photo_view_hero_attributes.dart';
import 'package:photo_view/src/utils/photo_view_utils.dart';

export 'src/controller/photo_view_controller.dart';
export 'src/controller/photo_view_scalestate_controller.dart';
Expand Down Expand Up @@ -259,6 +256,7 @@ class PhotoView extends StatefulWidget {
this.tightMode,
this.filterQuality,
this.disableGestures,
this.errorBuilder,
}) : child = null,
childSize = null,
super(key: key);
Expand Down Expand Up @@ -292,6 +290,7 @@ class PhotoView extends StatefulWidget {
this.filterQuality,
this.disableGestures,
}) : loadFailedChild = null,
errorBuilder = null,
imageProvider = null,
gaplessPlayback = false,
loadingBuilder = null,
Expand All @@ -306,6 +305,10 @@ class PhotoView extends StatefulWidget {
final LoadingBuilder loadingBuilder;

/// Show loadFailedChild when the image failed to load
final ImageErrorWidgetBuilder errorBuilder;

/// Show loadFailedChild when the image failed to load
@Deprecated("Use errorBuilder instead")
final Widget loadFailedChild;

/// Changes the background behind image, defaults to `Colors.black`.
Expand Down Expand Up @@ -385,74 +388,29 @@ class PhotoView extends StatefulWidget {
// Useful when custom gesture detector is used in child widget.
final bool disableGestures;

bool get _isCustomChild {
return child != null;
}

@override
State<StatefulWidget> createState() {
return _PhotoViewState();
}
}

class _PhotoViewState extends State<PhotoView> {
Size _childSize;
bool _loading;
ImageChunkEvent _imageChunkEvent;
// image retrieval

// controller
bool _controlledController;
PhotoViewControllerBase _controller;

bool _controlledScaleStateController;
PhotoViewScaleStateController _scaleStateController;

Future<ImageInfo> _getImage() {
final Completer completer = Completer<ImageInfo>();
final ImageStream stream = widget.imageProvider.resolve(
const ImageConfiguration(),
);
final listener = ImageStreamListener((
ImageInfo info,
bool synchronousCall,
) {
if (completer.isCompleted) {
return;
}
completer.complete(info);
if (mounted) {
final setupCallback = () {
_childSize = Size(
info.image.width.toDouble(),
info.image.height.toDouble(),
);
_loading = false;
_imageChunkEvent = null;
};
synchronousCall ? setupCallback() : setState(setupCallback);
}
}, onChunk: (event) {
if (mounted) {
setState(() => _imageChunkEvent = event);
}
}, onError: (exception, stackTrace) {
if (completer.isCompleted) {
return;
}
completer.completeError(exception, stackTrace);
});
stream.addListener(listener);
completer.future.then((_) {
stream.removeListener(listener);
});
return completer.future;
}

@override
void initState() {
super.initState();
if (widget.child == null) {
_getImage();
} else {
_childSize = widget.childSize;
_loading = false;
_imageChunkEvent = null;
}

if (widget.controller == null) {
_controlledController = true;
_controller = PhotoViewController();
Expand All @@ -474,11 +432,6 @@ class _PhotoViewState extends State<PhotoView> {

@override
void didUpdateWidget(PhotoView oldWidget) {
if (oldWidget.childSize != widget.childSize && widget.childSize != null) {
setState(() {
_childSize = widget.childSize;
});
}
if (widget.controller == null) {
if (!_controlledController) {
_controlledController = true;
Expand Down Expand Up @@ -525,114 +478,58 @@ class _PhotoViewState extends State<PhotoView> {
BuildContext context,
BoxConstraints constraints,
) {
return widget.child == null
? _buildImage(context, constraints)
: _buildCustomChild(context, constraints);
final computedOuterSize = widget.customSize ?? constraints.biggest;

return widget._isCustomChild
? CustomChildWrapper(
child: widget.child,
childSize: widget.childSize,
backgroundDecoration: widget.backgroundDecoration,
heroAttributes: widget.heroAttributes,
scaleStateChangedCallback: widget.scaleStateChangedCallback,
enableRotation: widget.enableRotation,
controller: _controller,
scaleStateController: _scaleStateController,
maxScale: widget.maxScale,
minScale: widget.minScale,
initialScale: widget.initialScale,
basePosition: widget.basePosition,
scaleStateCycle: widget.scaleStateCycle,
onTapUp: widget.onTapUp,
onTapDown: widget.onTapDown,
outerSize: computedOuterSize,
gestureDetectorBehavior: widget.gestureDetectorBehavior,
tightMode: widget.tightMode,
filterQuality: widget.filterQuality,
disableGestures: widget.disableGestures,
)
: ImageWrapper(
imageProvider: widget.imageProvider,
loadingBuilder: widget.loadingBuilder,
loadFailedChild: widget.loadFailedChild,
backgroundDecoration: widget.backgroundDecoration,
gaplessPlayback: widget.gaplessPlayback,
heroAttributes: widget.heroAttributes,
scaleStateChangedCallback: widget.scaleStateChangedCallback,
enableRotation: widget.enableRotation,
controller: _controller,
scaleStateController: _scaleStateController,
maxScale: widget.maxScale,
minScale: widget.minScale,
initialScale: widget.initialScale,
basePosition: widget.basePosition,
scaleStateCycle: widget.scaleStateCycle,
onTapUp: widget.onTapUp,
onTapDown: widget.onTapDown,
outerSize: computedOuterSize,
gestureDetectorBehavior: widget.gestureDetectorBehavior,
tightMode: widget.tightMode,
filterQuality: widget.filterQuality,
disableGestures: widget.disableGestures,
);
},
);
}

Widget _buildCustomChild(BuildContext context, BoxConstraints constraints) {
final _computedOuterSize = widget.customSize ?? constraints.biggest;

final scaleBoundaries = ScaleBoundaries(
widget.minScale ?? 0.0,
widget.maxScale ?? double.infinity,
widget.initialScale ?? PhotoViewComputedScale.contained,
_computedOuterSize,
_childSize ?? constraints.biggest,
);

return PhotoViewCore.customChild(
customChild: widget.child,
backgroundDecoration: widget.backgroundDecoration,
enableRotation: widget.enableRotation,
heroAttributes: widget.heroAttributes,
controller: _controller,
scaleStateController: _scaleStateController,
scaleStateCycle: widget.scaleStateCycle ?? defaultScaleStateCycle,
basePosition: widget.basePosition ?? Alignment.center,
scaleBoundaries: scaleBoundaries,
onTapUp: widget.onTapUp,
onTapDown: widget.onTapDown,
gestureDetectorBehavior: widget.gestureDetectorBehavior,
tightMode: widget.tightMode ?? false,
filterQuality: widget.filterQuality ?? FilterQuality.none,
disableGestures: widget.disableGestures ?? false,
);
}

Widget _buildImage(BuildContext context, BoxConstraints constraints) {
return widget.heroAttributes == null
? _buildAsync(context, constraints)
: _buildSync(context, constraints);
}

Widget _buildAsync(BuildContext context, BoxConstraints constraints) {
return FutureBuilder(
future: _getImage(),
builder: (BuildContext context, AsyncSnapshot<ImageInfo> info) {
if (info.hasError) {
return _buildLoadFailed();
}
if (info.hasData) {
return _buildWrapperImage(context, constraints);
}
return _buildLoading();
});
}

Widget _buildSync(BuildContext context, BoxConstraints constraints) {
if (_loading == null) {
return _buildLoading();
}
return _buildWrapperImage(context, constraints);
}

Widget _buildWrapperImage(BuildContext context, BoxConstraints constraints) {
final _computedOuterSize = widget.customSize ?? constraints.biggest;

final scaleBoundaries = ScaleBoundaries(
widget.minScale ?? 0.0,
widget.maxScale ?? double.infinity,
widget.initialScale ?? PhotoViewComputedScale.contained,
_computedOuterSize,
_childSize,
);

return PhotoViewCore(
imageProvider: widget.imageProvider,
backgroundDecoration: widget.backgroundDecoration,
gaplessPlayback: widget.gaplessPlayback,
enableRotation: widget.enableRotation,
heroAttributes: widget.heroAttributes,
basePosition: widget.basePosition ?? Alignment.center,
controller: _controller,
scaleStateController: _scaleStateController,
scaleStateCycle: widget.scaleStateCycle ?? defaultScaleStateCycle,
scaleBoundaries: scaleBoundaries,
onTapUp: widget.onTapUp,
onTapDown: widget.onTapDown,
gestureDetectorBehavior: widget.gestureDetectorBehavior,
tightMode: widget.tightMode ?? false,
filterQuality: widget.filterQuality ?? FilterQuality.none,
disableGestures: widget.disableGestures ?? false,
);
}

Widget _buildLoading() {
if (widget.loadingBuilder != null) {
return widget.loadingBuilder(context, _imageChunkEvent);
}

return PhotoViewDefaultLoading(
event: _imageChunkEvent,
);
}

Widget _buildLoadFailed() {
return widget.loadFailedChild ?? PhotoViewDefaultError();
}
}

/// The default [ScaleStateCycle]
Expand Down Expand Up @@ -673,7 +570,7 @@ typedef PhotoViewImageTapDownCallback = Function(
PhotoViewControllerValue controllerValue,
);

/// A type definition for a callback to show a widget while a image is loading, a [ImageChunkEvent] is passed to inform progress
/// A type definition for a callback to show a widget while the image is loading, a [ImageChunkEvent] is passed to inform progress
typedef LoadingBuilder = Widget Function(
BuildContext context,
ImageChunkEvent event,
Expand Down
Loading

0 comments on commit 6cdd860

Please sign in to comment.