Skip to content

Commit e7c434a

Browse files
lightbox: Support thumbnail to original image hero transition
Fixes: #799
1 parent 940636a commit e7c434a

File tree

2 files changed

+63
-3
lines changed

2 files changed

+63
-3
lines changed

lib/widgets/content.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@ class MessageImage extends StatelessWidget {
587587
context: context,
588588
message: message,
589589
src: resolvedSrcUrl,
590+
thumbnailUrl: resolvedThumbnailUrl,
590591
mediaType: MediaType.image));
591592
},
592593
child: node.loading
@@ -617,6 +618,7 @@ class MessageInlineVideo extends StatelessWidget {
617618
context: context,
618619
message: message,
619620
src: resolvedSrc,
621+
thumbnailUrl: null,
620622
mediaType: MediaType.video));
621623
},
622624
child: Container(

lib/widgets/lightbox.dart

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:flutter/material.dart';
2+
import 'package:flutter/scheduler.dart';
23
import 'package:flutter/services.dart';
34
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
45
import 'package:intl/intl.dart';
@@ -91,12 +92,17 @@ class _LightboxPageLayout extends StatefulWidget {
9192
const _LightboxPageLayout({
9293
required this.routeEntranceAnimation,
9394
required this.message,
95+
required this.buildAppBarBottom,
9496
required this.buildBottomAppBar,
9597
required this.child,
9698
});
9799

98100
final Animation<double> routeEntranceAnimation;
99101
final Message message;
102+
103+
/// For [AppBar.bottom].
104+
final PreferredSizeWidget? Function(BuildContext context) buildAppBarBottom;
105+
100106
final Widget? Function(
101107
BuildContext context, Color color, double elevation) buildBottomAppBar;
102108
final Widget child;
@@ -171,7 +177,8 @@ class _LightboxPageLayoutState extends State<_LightboxPageLayout> {
171177

172178
// Make smaller, like a subtitle
173179
style: themeData.textTheme.titleSmall!.copyWith(color: appBarForegroundColor)),
174-
])));
180+
])),
181+
bottom: widget.buildAppBarBottom(context));
175182
}
176183

177184
Widget? bottomAppBar;
@@ -209,17 +216,30 @@ class _ImageLightboxPage extends StatefulWidget {
209216
required this.routeEntranceAnimation,
210217
required this.message,
211218
required this.src,
219+
required this.thumbnailUrl,
212220
});
213221

214222
final Animation<double> routeEntranceAnimation;
215223
final Message message;
216224
final Uri src;
225+
final Uri? thumbnailUrl;
217226

218227
@override
219228
State<_ImageLightboxPage> createState() => _ImageLightboxPageState();
220229
}
221230

222231
class _ImageLightboxPageState extends State<_ImageLightboxPage> {
232+
double? _loadingProgress;
233+
234+
PreferredSizeWidget? _buildAppBarBottom(BuildContext context) {
235+
if (_loadingProgress == null) {
236+
return null;
237+
}
238+
return PreferredSize(
239+
preferredSize: const Size.fromHeight(4.0),
240+
child: LinearProgressIndicator(minHeight: 4.0, value: _loadingProgress));
241+
}
242+
223243
Widget _buildBottomAppBar(BuildContext context, Color color, double elevation) {
224244
return BottomAppBar(
225245
color: color,
@@ -232,19 +252,54 @@ class _ImageLightboxPageState extends State<_ImageLightboxPage> {
232252
);
233253
}
234254

255+
Widget _frameBuilder(BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
256+
if (widget.thumbnailUrl == null) return child;
257+
258+
// The full image is available, so display it.
259+
if (frame != null) return child;
260+
261+
// Display the thumbnail image while original image is downloading.
262+
return RealmContentNetworkImage(widget.thumbnailUrl!,
263+
filterQuality: FilterQuality.medium);
264+
}
265+
266+
Widget _loadingBuilder(BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
267+
if (widget.thumbnailUrl == null) return child;
268+
269+
// `loadingProgress` becomes null when Image has finished downloading.
270+
final double? progress = loadingProgress?.expectedTotalBytes == null ? null
271+
: loadingProgress!.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!;
272+
273+
if (progress != _loadingProgress) {
274+
_loadingProgress = progress;
275+
// This function is called in a build method and setState
276+
// can't be called in a build method, so delay it.
277+
SchedulerBinding.instance.scheduleFrameCallback((_) { if (mounted) setState(() {}); });
278+
}
279+
return child;
280+
}
281+
235282
@override
236283
Widget build(BuildContext context) {
237284
return _LightboxPageLayout(
238285
routeEntranceAnimation: widget.routeEntranceAnimation,
239286
message: widget.message,
287+
buildAppBarBottom: _buildAppBarBottom,
240288
buildBottomAppBar: _buildBottomAppBar,
241289
child: SizedBox.expand(
242290
child: InteractiveViewer(
243291
child: SafeArea(
244292
child: LightboxHero(
245293
message: widget.message,
246294
src: widget.src,
247-
child: RealmContentNetworkImage(widget.src, filterQuality: FilterQuality.medium))))));
295+
child: RealmContentNetworkImage(widget.src,
296+
filterQuality: FilterQuality.medium,
297+
frameBuilder: _frameBuilder,
298+
loadingBuilder: _loadingBuilder),
299+
),
300+
),
301+
),
302+
));
248303
}
249304
}
250305

@@ -457,6 +512,7 @@ class _VideoLightboxPageState extends State<VideoLightboxPage> with PerAccountSt
457512
return _LightboxPageLayout(
458513
routeEntranceAnimation: widget.routeEntranceAnimation,
459514
message: widget.message,
515+
buildAppBarBottom: (context) => null,
460516
buildBottomAppBar: _buildBottomAppBar,
461517
child: SafeArea(
462518
child: Center(
@@ -484,6 +540,7 @@ Route<void> getLightboxRoute({
484540
BuildContext? context,
485541
required Message message,
486542
required Uri src,
543+
required Uri? thumbnailUrl,
487544
required MediaType mediaType,
488545
}) {
489546
return AccountPageRouteBuilder(
@@ -500,7 +557,8 @@ Route<void> getLightboxRoute({
500557
MediaType.image => _ImageLightboxPage(
501558
routeEntranceAnimation: animation,
502559
message: message,
503-
src: src),
560+
src: src,
561+
thumbnailUrl: thumbnailUrl),
504562
MediaType.video => VideoLightboxPage(
505563
routeEntranceAnimation: animation,
506564
message: message,

0 commit comments

Comments
 (0)