Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save callmephil/911606cfa175dd6f2d0093d981b701dd to your computer and use it in GitHub Desktop.
Save callmephil/911606cfa175dd6f2d0093d981b701dd to your computer and use it in GitHub Desktop.
InteractiveViewer with CustomMultiChildLayout
import 'dart:convert';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:image_size_getter/file_input.dart' as image_size;
import 'package:image_size_getter/image_size_getter.dart' as image_size;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
late final File _imageFile;
late final List<FileHotspot> _hotspots;
image_size.Size? _size;
@override
void initState() {
super.initState();
_getImageSize();
}
Future<void> _getImageSize() async {
_imageFile = File('/Users/long1eu/Downloads/parts.png');
final image_size.Size size =
image_size.ImageSizeGetter.getSize(image_size.FileInput(_imageFile));
final String data =
await File('/Users/long1eu/Downloads/hotspots.json').readAsString();
final hotspots = (jsonDecode(data) as List<dynamic>)
.map((dynamic json) => FileHotspot.fromJson(json))
.toList();
if (mounted) {
setState(() {
_size = size;
_hotspots = hotspots;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Builder(builder: (context) {
if (_size == null) {
return const Center(
child: CircularProgressIndicator(),
);
}
return InteractiveViewer(
clipBehavior: Clip.none,
child: CustomMultiChildLayout(
delegate: FileHotspotLayoutDelegate(
path: _imageFile.path,
hotspots: _hotspots,
size: _size!,
),
children: <Widget>[
LayoutId(
id: _imageFile.path,
child: Image.file(_imageFile),
),
for (final FileHotspot hotspot in _hotspots)
LayoutId(
id: hotspot,
child: Container(
color: Colors.blue.withOpacity(.5),
),
),
],
),
);
}),
);
}
}
class FileHotspotLayoutDelegate extends MultiChildLayoutDelegate {
FileHotspotLayoutDelegate({
required this.path,
required this.hotspots,
required this.size,
super.relayout,
});
final String path;
final List<FileHotspot> hotspots;
final image_size.Size size;
@override
void performLayout(Size size) {
if (hasChild(path)) {
layoutChild(path,
BoxConstraints.tightFor(width: size.width, height: size.height));
positionChild(path, Offset.zero);
}
final double widthRatio = size.width / this.size.width;
final double heightRatio = size.height / this.size.height;
for (final FileHotspot hotspot in hotspots) {
final double x1 = hotspot.x1.toDouble();
final double y1 = hotspot.y1.toDouble();
final double x2 = hotspot.x2.toDouble();
final double y2 = hotspot.y2.toDouble();
if (hasChild(hotspot)) {
layoutChild(
hotspot,
BoxConstraints.tightFor(
width: (x2 - x1) * widthRatio,
height: (y2 - y1) * heightRatio));
positionChild(hotspot, Offset(x1 * widthRatio, y1 * heightRatio));
}
}
}
@override
bool shouldRelayout(covariant FileHotspotLayoutDelegate oldDelegate) {
return path != oldDelegate.path ||
const DeepCollectionEquality().equals(hotspots, oldDelegate.hotspots) ||
size != oldDelegate.size;
}
}
class FileHotspot {
FileHotspot.fromJson(Map<String, dynamic> json)
: x1 = (json['x1'] as num).toDouble(),
x2 = (json['x2'] as num).toDouble(),
y1 = (json['y1'] as num).toDouble(),
y2 = (json['y2'] as num).toDouble(),
value = json['linkValue'];
final double x1;
final double x2;
final double y1;
final double y2;
final String value;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment