Skip to content

Instantly share code, notes, and snippets.

@long1eu
Last active April 11, 2024 07:58
Show Gist options
  • Save long1eu/e59e3674871d3d63c9fe7b177fe43ca8 to your computer and use it in GitHub Desktop.
Save long1eu/e59e3674871d3d63c9fe7b177fe43ca8 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;
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(size.width.toDouble(), size.height.toDouble());
_hotspots = hotspots;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
backgroundColor: Colors.yellow,
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,
imageSize: _size!,
direction: Directionality.of(context),
),
children: <Widget>[
LayoutId(
id: _imageFile.path,
child: Image.file(_imageFile),
),
for (final FileHotspot hotspot in _hotspots)
LayoutId(
id: hotspot,
child: GestureDetector(
onTap: () {
print(hotspot.value);
},
child: Container(
color: Colors.blue.withOpacity(.5),
),
),
),
],
),
);
},
),
);
}
}
class FileHotspotLayoutDelegate extends MultiChildLayoutDelegate {
FileHotspotLayoutDelegate({
required this.path,
required this.hotspots,
required this.imageSize,
required this.direction,
this.alignment = Alignment.center,
this.padding = EdgeInsets.zero,
this.fit = BoxFit.contain,
super.relayout,
});
final String path;
final List<FileHotspot> hotspots;
final Size imageSize;
final TextDirection direction;
final AlignmentGeometry alignment;
final EdgeInsetsGeometry padding;
final BoxFit fit;
@override
void performLayout(Size size) {
final FittedSizes sizes = applyBoxFit(
fit,
imageSize,
padding.resolve(direction).deflateRect(Offset.zero & size).size,
);
layoutChild(
path,
BoxConstraints.tightFor(
width: sizes.destination.width,
height: sizes.destination.height,
),
);
final Rect rect = alignment.resolve(direction).inscribe(
sizes.destination,
Offset.zero & size,
);
positionChild(path, rect.topLeft);
final double widthRatio = rect.width / imageSize.width;
final double heightRatio = rect.height / imageSize.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();
layoutChild(
hotspot,
BoxConstraints.tightFor(
width: (x2 - x1) * widthRatio,
height: (y2 - y1) * heightRatio,
),
);
positionChild(
hotspot,
rect.topLeft + Offset(x1 * widthRatio, y1 * heightRatio),
);
}
}
@override
bool shouldRelayout(covariant FileHotspotLayoutDelegate oldDelegate) {
return path != oldDelegate.path ||
const DeepCollectionEquality().equals(hotspots, oldDelegate.hotspots) ||
imageSize != oldDelegate.imageSize;
}
}
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