TLDR: how to fitWidth on an image inside an InteractiveViewer
I am using an InteractiveViewer in my flutter app to display a photo of the page of a book.
Previously, I was just using a SingleChildScrollview to let the user pan across the image vertically.
Now, I want to implement zooming in on the image as a UX improvement.
When the viewport is taller (e.g. higher aspect ratio) than the image, the image is shrunk so that it fits the height of the image, meaning that the user needs to pan to see all of the width of the image.
Essentially, I want to use BoxFit.fitWidth behaviour for the image inside the InteractiveViewer to guarantee that the full width of the image is always shown. In my app, the full width means every word in the sentence, in the example, it corresponds with every car lane in the road. However, adding fit:BoxFit.fitWidth does not fix the issue.
Below is a minimal reproducible example that you can paste straight into https://dartpad.dev: To observe the effects, resize the window by making it thinner, until the message text changes. Then, make sure that you are zoomed out all the way (if you're zoomed in, you can always pan in both directions).
Things that I have tried to no success:
- using
fit:BoxFit.fitWidth(it's in the example, but doesn't change anything) - making the bottom
boundaryMarginreally big (this came close, but it also meant that the user could pan far away from the image)
import 'package:flutter/material.dart';
void main() {
runApp(const InteractiveImageViewerApp());
}
class InteractiveImageViewerApp extends StatelessWidget {
const InteractiveImageViewerApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Minimal InteractiveViewer Example',
home: ImageZoomPage(),
);
}
}
class ImageZoomPage extends StatelessWidget {
const ImageZoomPage({super.key});
static const Size imageSize = Size(200, 300);
// An image of a street in Barcelona to demonstrate that we want to see each all car lanes without panning
static final String imageUrl =
'https://picsum.photos/id/88/${imageSize.width.toInt()}/${imageSize.height.toInt()}';
static final double imageAspectRatio = imageSize.height / imageSize.width;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('InteractiveViewer'),
),
// Use LayoutBuilder to get the aspectRatio of the area that the image will scale to fill
body: LayoutBuilder(builder: (context, constraints) {
final double areaAspectRatio =
constraints.maxHeight / constraints.maxWidth;
// The
final String message = areaAspectRatio > imageAspectRatio
? "ONLY HORIZONTAL: not desired, since user needs to pan to see the lanes"
: "ONLY VERTICAL: desired, because all lanes are fully shown";
// The stack is only here to show the text on top of the interactiveviewer
return Stack(
children: [
InteractiveViewer(
constrained: false,
minScale: 0.5,
maxScale: 5.0,
child: Image.network(imageUrl, fit:BoxFit.fitWidth),
),
Align(
alignment: Alignment.topLeft,
child: Text(
message,
style: TextStyle(
backgroundColor: Colors.black, color: Colors.white),
),
),
],
);
}),
);
}
}