Skip to content

Commit

Permalink
feat(ui_storage): add shimmer for loading images (#11237)
Browse files Browse the repository at this point in the history
  • Loading branch information
lesnitsky committed Jul 13, 2023
1 parent d00c3ed commit e2ad900
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ class StorageImageApp extends StatelessWidget implements App {
loadingStateVariant: LoadingStateVariant.blurHash(),
),
),
AspectRatio(
aspectRatio: 4 / 3,
child: StorageImage(
ref: FirebaseStorage.instance.ref().child('dash_and_sparky.png'),
fit: BoxFit.cover,
loadingStateVariant: LoadingStateVariant.shimmer(),
),
),
],
);
}
Expand Down
170 changes: 153 additions & 17 deletions packages/firebase_ui_storage/lib/src/widgets/image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ abstract class LoadingStateVariant {
Color? color,
}) = _LoadingIndicatorLoadingStateVariant;

factory LoadingStateVariant.shimmer({
Curve? curve,
Duration? animationDuration,
double? initialProgress,
}) = _ShimmerLoadingStateVariant;

/// {@template ui.storage.image.loadingStateVariant.animationDuration}
/// The duration of the transtion between loading placeholder and the actual
/// image.
Expand Down Expand Up @@ -108,6 +114,16 @@ class _LoadingIndicatorLoadingStateVariant extends LoadingStateVariant {
);
}

class _ShimmerLoadingStateVariant extends LoadingStateVariant {
final double? initialProgress;

const _ShimmerLoadingStateVariant({
super.curve = Curves.linear,
super.animationDuration = const Duration(milliseconds: 800),
this.initialProgress,
});
}

/// A widget that downloads and displays an image from Firebase Storage.
class StorageImage extends StatefulWidget {
/// A reference to the image in Firebase Storage.
Expand Down Expand Up @@ -247,19 +263,41 @@ class _StorageImageState extends State<StorageImage>
ctrl!.forward();
}

bool animationsCompleted = false;

@override
void initState() {
super.initState();
opacity.addStatusListener(_onOpacityStatus);
}

void _onOpacityStatus(AnimationStatus status) {
if (status != AnimationStatus.completed) return;
if (animationsCompleted) return;

setState(() {
animationsCompleted = true;
});
}

GlobalKey placeholderKey = GlobalKey();

Widget loadingBuilder(
Widget frameBuilder(
BuildContext context,
Widget child,
ImageChunkEvent? loadingProgress,
int? frame,
bool wasSynchronouslyLoaded,
) {
if (loadingProgress == null || loadingProgress.complete()) {
if (animationsCompleted) {
return child;
}

if (wasSynchronouslyLoaded || frame != null) {
maybeAnimate();
}

if (loadingStateVariant is _SolidColorLoadingStateVariant) {
final Widget placeholder = _SolidColorLoadingStateVariantPlaceholder(
final placeholder = _SolidColorLoadingStateVariantPlaceholder(
key: placeholderKey,
color: (loadingStateVariant as _SolidColorLoadingStateVariant).color,
child: child,
Expand All @@ -268,14 +306,32 @@ class _StorageImageState extends State<StorageImage>
}

if (loadingStateVariant is _BlurHashLoadingStateVariant) {
Widget placeholder = _BlurHashLoadingStateVariantPlaceholder(
final placeholder = _BlurHashLoadingStateVariantPlaceholder(
key: placeholderKey,
ref: ref,
value: (loadingStateVariant as _BlurHashLoadingStateVariant).value,
curve: loadingStateVariant.curve,
duration: loadingStateVariant.animationDuration,
child: child,
);

return placeholder;
}

if (loadingStateVariant is _ShimmerLoadingStateVariant) {
final _ShimmerLoadingStateVariant(
:initialProgress,
) = loadingStateVariant as _ShimmerLoadingStateVariant;

final placeholder = _ShimmerLoadingStateVariantPlaceholder(
key: placeholderKey,
curve: loadingStateVariant.curve,
duration: loadingStateVariant.animationDuration,
initialProgress: initialProgress ?? 0,
showContent: frame != null,
child: child,
);

return placeholder;
}

Expand All @@ -288,7 +344,7 @@ class _StorageImageState extends State<StorageImage>
alignment: Alignment.center,
children: [
Positioned.fill(child: child),
if (loadingProgress != null && !loadingProgress.complete())
if (frame == null)
LoadingIndicator(
size: config.size,
borderWidth: config.strokeWidth,
Expand Down Expand Up @@ -326,12 +382,12 @@ class _StorageImageState extends State<StorageImage>
excludeFromSemantics: widget.excludeFromSemantics,
filterQuality: widget.filterQuality,
fit: widget.fit,
frameBuilder: widget.frameBuilder,
frameBuilder: widget.frameBuilder ?? frameBuilder,
gaplessPlayback: widget.gaplessPlayback,
headers: widget.headers,
height: widget.height,
isAntiAlias: widget.isAntiAlias,
loadingBuilder: widget.loadingBuilder ?? loadingBuilder,
loadingBuilder: widget.loadingBuilder,
matchTextDirection: widget.matchTextDirection,
repeat: widget.repeat,
opacity: opacity,
Expand All @@ -340,13 +396,11 @@ class _StorageImageState extends State<StorageImage>
);
}

return (widget.loadingBuilder ?? loadingBuilder).call(
return (widget.frameBuilder ?? frameBuilder).call(
context,
Container(),
const ImageChunkEvent(
cumulativeBytesLoaded: 0,
expectedTotalBytes: 9007199254740992,
),
null,
false,
);
},
);
Expand All @@ -364,6 +418,7 @@ class _StorageImageState extends State<StorageImage>
@override
void dispose() {
ctrl?.dispose();
opacity.removeStatusListener(_onOpacityStatus);
super.dispose();
}
}
Expand All @@ -383,7 +438,7 @@ class _SolidColorLoadingStateVariantPlaceholder extends StatelessWidget {
return color!;
}

return Theme.of(context).colorScheme.onSurface.withOpacity(0.12);
return Theme.of(context).colorScheme.surfaceTint.withOpacity(0.12);
}

@override
Expand Down Expand Up @@ -486,8 +541,89 @@ class _BlurHashLoadingStateVariantPlaceholderState
}
}

extension on ImageChunkEvent {
bool complete() {
return cumulativeBytesLoaded == expectedTotalBytes;
class _ShimmerLoadingStateVariantPlaceholder extends StatefulWidget {
final Curve curve;
final Duration duration;
final Widget child;
final double initialProgress;
final bool showContent;

const _ShimmerLoadingStateVariantPlaceholder({
super.key,
required this.curve,
required this.duration,
required this.child,
required this.showContent,
this.initialProgress = 0.0,
});

@override
State<_ShimmerLoadingStateVariantPlaceholder> createState() =>
__ShimmerLoadingStateVariantPlaceholderState();
}

class __ShimmerLoadingStateVariantPlaceholderState
extends State<_ShimmerLoadingStateVariantPlaceholder>
with SingleTickerProviderStateMixin {
late AnimationController ctrl = AnimationController(
vsync: this,
duration: widget.duration,
value: widget.initialProgress,
)..repeat();

late final animation = Tween(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: ctrl,
curve: widget.curve,
));

Alignment getAlignment(double animationProgress) {
return Alignment(
-2 + animationProgress * 4,
-2 + animationProgress * 4,
);
}

@override
Widget build(BuildContext context) {
final a = Theme.of(context).colorScheme.surfaceTint.withOpacity(0.12);
final b = Theme.of(context).colorScheme.surfaceTint.withOpacity(0.24);

final (lighter, darker) = switch (Theme.of(context).brightness) {
Brightness.light => (a, b),
Brightness.dark => (b, a),
};

return AnimatedBuilder(
animation: ctrl,
builder: (context, child) {
final alignment = getAlignment(animation.value);

return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
darker,
lighter,
darker,
],
stops: const [0.0, 0.5, 1.0],
begin: getAlignment(animation.value),
end: alignment + const Alignment(1, 1),
),
),
child: child,
);
},
child: widget.showContent ? widget.child : null,
);
}

@override
void dispose() {
ctrl.dispose();
super.dispose();
}
}

0 comments on commit e2ad900

Please sign in to comment.