Skip to content

Commit

Permalink
lightbox: Support thumbnail to original image hero transition
Browse files Browse the repository at this point in the history
Fixes: zulip#799
  • Loading branch information
rajveermalviya committed Jul 18, 2024
1 parent 940636a commit e7c434a
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 3 deletions.
2 changes: 2 additions & 0 deletions lib/widgets/content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,7 @@ class MessageImage extends StatelessWidget {
context: context,
message: message,
src: resolvedSrcUrl,
thumbnailUrl: resolvedThumbnailUrl,
mediaType: MediaType.image));
},
child: node.loading
Expand Down Expand Up @@ -617,6 +618,7 @@ class MessageInlineVideo extends StatelessWidget {
context: context,
message: message,
src: resolvedSrc,
thumbnailUrl: null,
mediaType: MediaType.video));
},
child: Container(
Expand Down
64 changes: 61 additions & 3 deletions lib/widgets/lightbox.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
import 'package:intl/intl.dart';
Expand Down Expand Up @@ -91,12 +92,17 @@ class _LightboxPageLayout extends StatefulWidget {
const _LightboxPageLayout({
required this.routeEntranceAnimation,
required this.message,
required this.buildAppBarBottom,
required this.buildBottomAppBar,
required this.child,
});

final Animation<double> routeEntranceAnimation;
final Message message;

/// For [AppBar.bottom].
final PreferredSizeWidget? Function(BuildContext context) buildAppBarBottom;

final Widget? Function(
BuildContext context, Color color, double elevation) buildBottomAppBar;
final Widget child;
Expand Down Expand Up @@ -171,7 +177,8 @@ class _LightboxPageLayoutState extends State<_LightboxPageLayout> {

// Make smaller, like a subtitle
style: themeData.textTheme.titleSmall!.copyWith(color: appBarForegroundColor)),
])));
])),
bottom: widget.buildAppBarBottom(context));
}

Widget? bottomAppBar;
Expand Down Expand Up @@ -209,17 +216,30 @@ class _ImageLightboxPage extends StatefulWidget {
required this.routeEntranceAnimation,
required this.message,
required this.src,
required this.thumbnailUrl,
});

final Animation<double> routeEntranceAnimation;
final Message message;
final Uri src;
final Uri? thumbnailUrl;

@override
State<_ImageLightboxPage> createState() => _ImageLightboxPageState();
}

class _ImageLightboxPageState extends State<_ImageLightboxPage> {
double? _loadingProgress;

PreferredSizeWidget? _buildAppBarBottom(BuildContext context) {
if (_loadingProgress == null) {
return null;
}
return PreferredSize(
preferredSize: const Size.fromHeight(4.0),
child: LinearProgressIndicator(minHeight: 4.0, value: _loadingProgress));
}

Widget _buildBottomAppBar(BuildContext context, Color color, double elevation) {
return BottomAppBar(
color: color,
Expand All @@ -232,19 +252,54 @@ class _ImageLightboxPageState extends State<_ImageLightboxPage> {
);
}

Widget _frameBuilder(BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
if (widget.thumbnailUrl == null) return child;

// The full image is available, so display it.
if (frame != null) return child;

// Display the thumbnail image while original image is downloading.
return RealmContentNetworkImage(widget.thumbnailUrl!,
filterQuality: FilterQuality.medium);
}

Widget _loadingBuilder(BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
if (widget.thumbnailUrl == null) return child;

// `loadingProgress` becomes null when Image has finished downloading.
final double? progress = loadingProgress?.expectedTotalBytes == null ? null
: loadingProgress!.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!;

if (progress != _loadingProgress) {
_loadingProgress = progress;
// This function is called in a build method and setState
// can't be called in a build method, so delay it.
SchedulerBinding.instance.scheduleFrameCallback((_) { if (mounted) setState(() {}); });
}
return child;
}

@override
Widget build(BuildContext context) {
return _LightboxPageLayout(
routeEntranceAnimation: widget.routeEntranceAnimation,
message: widget.message,
buildAppBarBottom: _buildAppBarBottom,
buildBottomAppBar: _buildBottomAppBar,
child: SizedBox.expand(
child: InteractiveViewer(
child: SafeArea(
child: LightboxHero(
message: widget.message,
src: widget.src,
child: RealmContentNetworkImage(widget.src, filterQuality: FilterQuality.medium))))));
child: RealmContentNetworkImage(widget.src,
filterQuality: FilterQuality.medium,
frameBuilder: _frameBuilder,
loadingBuilder: _loadingBuilder),
),
),
),
));
}
}

Expand Down Expand Up @@ -457,6 +512,7 @@ class _VideoLightboxPageState extends State<VideoLightboxPage> with PerAccountSt
return _LightboxPageLayout(
routeEntranceAnimation: widget.routeEntranceAnimation,
message: widget.message,
buildAppBarBottom: (context) => null,
buildBottomAppBar: _buildBottomAppBar,
child: SafeArea(
child: Center(
Expand Down Expand Up @@ -484,6 +540,7 @@ Route<void> getLightboxRoute({
BuildContext? context,
required Message message,
required Uri src,
required Uri? thumbnailUrl,
required MediaType mediaType,
}) {
return AccountPageRouteBuilder(
Expand All @@ -500,7 +557,8 @@ Route<void> getLightboxRoute({
MediaType.image => _ImageLightboxPage(
routeEntranceAnimation: animation,
message: message,
src: src),
src: src,
thumbnailUrl: thumbnailUrl),
MediaType.video => VideoLightboxPage(
routeEntranceAnimation: animation,
message: message,
Expand Down

0 comments on commit e7c434a

Please sign in to comment.