Skip to content

Commit

Permalink
Merge pull request #37 from thesmythgroup/add_loading_spinner
Browse files Browse the repository at this point in the history
GH #34: Initial work to add loading indicator
  • Loading branch information
jimjenkins5 authored Dec 13, 2023
2 parents 44b6817 + 903faa2 commit c11f4ea
Show file tree
Hide file tree
Showing 21 changed files with 445 additions and 60 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# EasyImageViewer Changelog

## 1.3.0

- Update Flutter to `3.16.3`
- Add loading progress indicator out of the box with options to fully customize both the widget used to display progress as well as the widget used to display an image (Flutter's `Image` by default).

## 1.2.1

- Update Dart to `>=2.12.0 <4.0.0`.
Expand Down
63 changes: 59 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ An easy way to display images in a full-screen dialog, including pinch & zoom.
* Optionally allow "swipe down to dismiss" by passing in `swipeDismissible: true`
* No dependencies besides Flutter
* Callbacks for `onPageChanged` and `onViewerDismissed`
* Fully customizable loading/progress indicator

## Usage

Expand All @@ -31,10 +32,10 @@ Show a bunch of images:

```dart
MultiImageProvider multiImageProvider = MultiImageProvider([
Image.network("https://picsum.photos/id/1001/5616/3744").image,
Image.network("https://picsum.photos/id/1003/1181/1772").image,
Image.network("https://picsum.photos/id/1004/5616/3744").image,
Image.network("https://picsum.photos/id/1005/5760/3840").image
const NetworkImage("https://picsum.photos/id/1001/4912/3264"),
const NetworkImage("https://picsum.photos/id/1003/1181/1772"),
const NetworkImage("https://picsum.photos/id/1004/4912/3264"),
const NetworkImage("https://picsum.photos/id/1005/4912/3264")
]);
showImageViewerPager(context, multiImageProvider, onPageChanged: (page) {
Expand Down Expand Up @@ -88,6 +89,60 @@ showImageViewerPager(context, productsImageProvider, onPageChanged: (page) {
});
```

## Customizing Progress Indicator and Image Widget

You can subclass `EasyImageProvider` and override `progressIndicatorWidgetBuilder`. That way you can
provide your own progress indicator when an image is loading. Here's an example for using a
linear progress indicator with a label:

```dart
class CustomImageWidgetProvider extends EasyImageProvider {
@override
final int initialIndex;
final List<String> imageUrls;
CustomImageWidgetProvider({required this.imageUrls, this.initialIndex = 0})
: super();
@override
ImageProvider<Object> imageBuilder(BuildContext context, int index) {
return NetworkImage(imageUrls[index]);
}
@override
Widget progressIndicatorWidgetBuilder(BuildContext context, int index, {double? value}) {
// Create a custom linear progress indicator
// with a label showing the progress value
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
LinearProgressIndicator(
value: value,
),
Text(
"${(value ?? 0) * 100}%",
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 20,
),
)
],
);
}
@override
int get imageCount => imageUrls.length;
}
```

You can also adjust `animationDuration` and `animationCurve` for the transition that is used
to fade in the loaded image.

Finally, you can even override the `imageWidgetBuilder` method and completely customize the
appearance of each individual image. Keep in mind that it should be an "image-like" widget
since it will be treated as such: the user can pinch&zoom the returned widget etc.

## How to release a new version on pub.dev
1. Update the version number in `pubspec.yaml`.
2. Add an entry for the new version in `CHANGELOG.md`.
Expand Down
2 changes: 1 addition & 1 deletion example/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ subprojects {
project.evaluationDependsOn(':app')
}

task clean(type: Delete) {
tasks.register("clean", Delete) {
delete rootProject.buildDir
}
2 changes: 1 addition & 1 deletion example/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1300;
LastUpgradeCheck = 1430;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
LastUpgradeVersion = "1430"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
61 changes: 57 additions & 4 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class MyApp extends StatelessWidget {
title: 'EasyImageViewer Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
progressIndicatorTheme: const ProgressIndicatorThemeData(color: Colors.green),
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'EasyImageViewer Demo'),
Expand All @@ -38,10 +39,10 @@ class _MyHomePageState extends State<MyHomePage> {
static const _kCurve = Curves.ease;

final List<ImageProvider> _imageProviders = [
Image.network("https://picsum.photos/id/1001/4912/3264").image,
Image.network("https://picsum.photos/id/1003/1181/1772").image,
Image.network("https://picsum.photos/id/1004/4912/3264").image,
Image.network("https://picsum.photos/id/1005/4912/3264").image
const NetworkImage("https://picsum.photos/id/1001/4912/3264"),
const NetworkImage("https://picsum.photos/id/1003/1181/1772"),
const NetworkImage("https://picsum.photos/id/1004/4912/3264"),
const NetworkImage("https://picsum.photos/id/1005/4912/3264")
];

late final _easyEmbeddedImageProvider = MultiImageProvider(_imageProviders);
Expand Down Expand Up @@ -100,6 +101,19 @@ class _MyHomePageState extends State<MyHomePage> {
MaterialPageRoute(builder: (context) => const AsyncDemoPage()),
);
}),
ElevatedButton(
child: const Text("Custom Progress Indicator"),
onPressed: () {
CustomImageWidgetProvider customImageProvider = CustomImageWidgetProvider(
imageUrls: [
"https://picsum.photos/id/1001/4912/3264",
"https://picsum.photos/id/1003/1181/1772",
"https://picsum.photos/id/1004/4912/3264",
"https://picsum.photos/id/1005/4912/3264"
].toList(),
);
showImageViewerPager(context, customImageProvider);
}),
SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height / 2.0,
Expand Down Expand Up @@ -153,3 +167,42 @@ class CustomImageProvider extends EasyImageProvider {
@override
int get imageCount => imageUrls.length;
}

class CustomImageWidgetProvider extends EasyImageProvider {
@override
final int initialIndex;
final List<String> imageUrls;

CustomImageWidgetProvider({required this.imageUrls, this.initialIndex = 0})
: super();

@override
ImageProvider<Object> imageBuilder(BuildContext context, int index) {
return NetworkImage(imageUrls[index]);
}

@override
Widget progressIndicatorWidgetBuilder(BuildContext context, int index, {double? value}) {
// Create a custom linear progress indicator
// with a label showing the progress value
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
LinearProgressIndicator(
value: value,
),
Text(
"${(value ?? 0) * 100}%",
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 20,
),
)
],
);
}

@override
int get imageCount => imageUrls.length;
}
11 changes: 6 additions & 5 deletions example/macos/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 51;
objectVersion = 54;
objects = {

/* Begin PBXAggregateTarget section */
Expand Down Expand Up @@ -182,7 +182,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1300;
LastUpgradeCheck = 1430;
ORGANIZATIONNAME = "";
TargetAttributes = {
33CC10EC2044A3C60003C045 = {
Expand Down Expand Up @@ -235,6 +235,7 @@
/* Begin PBXShellScriptBuildPhase section */
3399D490228B24CF009A79C7 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
Expand Down Expand Up @@ -344,7 +345,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.11;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
Expand Down Expand Up @@ -423,7 +424,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.11;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
Expand Down Expand Up @@ -470,7 +471,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.11;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
LastUpgradeVersion = "1430"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
60 changes: 60 additions & 0 deletions lib/src/easy_image_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,66 @@ abstract class EasyImageProvider {
/// Total count of images
int get imageCount;

/// Animation duration for the image transition (fade in after loading)
Duration animationDuration = const Duration(milliseconds: 300);

/// Animation curve for the image transition (fade in after loading)
Curve animationCurve = Curves.easeOut;

/// Returns the image for the given [index].
ImageProvider imageBuilder(BuildContext context, int index);

/// Returns the image widget for the given [index].
Widget imageWidgetBuilder(BuildContext context, int index) {
return Image(
image: imageBuilder(context, index),
frameBuilder: (BuildContext context, Widget child, int? frame,
bool wasSynchronouslyLoaded) {
if (wasSynchronouslyLoaded) {
return child;
}
return AnimatedOpacity(
opacity: frame == null ? 0 : 1,
duration: animationDuration,
curve: animationCurve,
child: child,
);
},
loadingBuilder: (BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) {
bool shouldShowLoading = loadingProgress != null;
return IndexedStack(
index: shouldShowLoading ? 1 : 0,
alignment: Alignment.center,
children: <Widget>[
child,
progressIndicatorWidgetBuilder(
context,
index,
value: loadingProgress?.expectedTotalBytes != null
? loadingProgress!.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: shouldShowLoading
? null
: 0.0,
),
],
);
},
);
}

/// Returns the progress indicator widget for the given [index].
/// The default implementation returns a [CircularProgressIndicator].
/// Override this method to customize the progress indicator.
/// The [value] parameter is the progress value between 0.0 and 1.0.
/// If [value] is null, the progress indicator is in indeterminate mode.
/// The [index] parameter is the index of the image being loaded.
/// The [context] parameter is the build context.
Widget progressIndicatorWidgetBuilder(BuildContext context, int index,
{double? value}) {
return CircularProgressIndicator(
value: value,
);
}
}
27 changes: 21 additions & 6 deletions lib/src/easy_image_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import 'package:flutter/material.dart';

/// A full-sized view that displays the given image, supporting pinch & zoom
class EasyImageView extends StatefulWidget {
/// The image to display
final ImageProvider imageProvider;
/// The image widget to display
final Widget imageWidget;

/// Minimum scale factor
final double minScale;
Expand All @@ -18,11 +18,27 @@ class EasyImageView extends StatefulWidget {
/// an interaction.
final void Function(double)? onScaleChanged;

/// Create a new instance that accepts an [ImageProvider]
EasyImageView.provider(ImageProvider imageProvider,
{Key? key,
double minScale = 1.0,
double maxScale = 5.0,
bool doubleTapZoomable = false,
void Function(double)? onScaleChanged})
: this(
key: key,
imageWidget: Image(image: imageProvider),
minScale: minScale,
maxScale: maxScale,
doubleTapZoomable: doubleTapZoomable,
onScaleChanged: onScaleChanged,
);

/// Create a new instance
/// The optional [doubleTapZoomable] boolean defaults to false and allows double tap to zoom.
const EasyImageView({
Key? key,
required this.imageProvider,
required this.imageWidget,
this.minScale = 1.0,
this.maxScale = 5.0,
this.doubleTapZoomable = false,
Expand Down Expand Up @@ -52,7 +68,6 @@ class _EasyImageViewState extends State<EasyImageView>

@override
Widget build(BuildContext context) {
final image = Image(image: widget.imageProvider);
return SizedBox.expand(
key: const Key('easy_image_sized_box'),
child: InteractiveViewer(
Expand All @@ -64,8 +79,8 @@ class _EasyImageViewState extends State<EasyImageView>
? GestureDetector(
onDoubleTapDown: _handleDoubleTapDown,
onDoubleTap: _handleDoubleTap,
child: image)
: image,
child: widget.imageWidget)
: widget.imageWidget,
onInteractionEnd: (scaleEndDetails) {
double scale = _transformationController.value.getMaxScaleOnAxis();

Expand Down
Loading

0 comments on commit c11f4ea

Please sign in to comment.