Skip to content

Commit 9d8590d

Browse files
committed
Enhance ImageHotspot package with new features and tests
- Refactor ImageHotspot widget for improved customization - Add tooltip functionality to hotspots - Implement custom icon support for hotspots - Update example app to showcase new features - Add unit tests for ImageHotspot widget
1 parent 6eefd75 commit 9d8590d

File tree

11 files changed

+334
-105
lines changed

11 files changed

+334
-105
lines changed

assets/images/sample.jpg

-10.9 KB
Binary file not shown.

assets/test_image.jpg

21.8 KB
Loading

example/assets/sample_image.jpg

39.4 KB
Loading

example/lib/main.dart

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,38 @@
11
import 'package:flutter/material.dart';
22
import 'package:image_hotspot/image_hotspot.dart';
33

4-
void main() => runApp(MyApp());
4+
void main() => runApp(const MyApp());
55

66
class MyApp extends StatelessWidget {
7+
const MyApp({super.key});
8+
79
@override
810
Widget build(BuildContext context) {
911
return MaterialApp(
1012
home: Scaffold(
11-
appBar: AppBar(title: Text('Image Hotspot Example')),
13+
appBar: AppBar(title: const Text('Image Hotspot Example')),
1214
body: Center(
1315
child: ImageHotspot(
1416
imagePath: 'assets/sample_image.jpg',
17+
imageWidth: 300,
18+
imageHeight: 200,
19+
imageFit: BoxFit.cover,
20+
showTooltip: true,
1521
hotspots: [
1622
Hotspot(
17-
x: 50.0,
18-
y: 100.0,
19-
onTap: () {
20-
print('Hotspot 1 tapped!');
21-
},
23+
x: 10,
24+
y: 20,
25+
onTap: () => print('Hotspot 1 tapped'),
26+
tooltip: 'This is hotspot 1',
27+
color: Colors.blue,
28+
size: 30,
2229
),
2330
Hotspot(
24-
x: 150.0,
25-
y: 200.0,
26-
onTap: () {
27-
print('Hotspot 2 tapped!');
28-
},
31+
x: 200,
32+
y: 150,
33+
onTap: () => print('Hotspot 2 tapped'),
34+
// tooltip: 'This is hotspot 2',
35+
icon: Icon(Icons.star, color: Colors.yellow),
2936
),
3037
],
3138
),

example/pubspec.lock

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ packages:
7575
description: flutter
7676
source: sdk
7777
version: "0.0.0"
78+
image_hotspot:
79+
dependency: "direct main"
80+
description:
81+
path: ".."
82+
relative: true
83+
source: path
84+
version: "0.0.1"
7885
leak_tracker:
7986
dependency: transitive
8087
description:

example/pubspec.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ dependencies:
3535
# The following adds the Cupertino Icons font to your application.
3636
# Use with the CupertinoIcons class for iOS style icons.
3737
cupertino_icons: ^1.0.6
38-
image_hotspot: ^0.0.1
38+
image_hotspot:
39+
path: ../
3940

4041
dev_dependencies:
4142
flutter_test:
@@ -60,9 +61,8 @@ flutter:
6061
uses-material-design: true
6162

6263
# To add assets to your application, add an assets section, like this:
63-
# assets:
64-
# - images/a_dot_burr.jpeg
65-
# - images/a_dot_ham.jpeg
64+
assets:
65+
- assets/sample_image.jpg
6666

6767
# An image asset can refer to one or more resolution-specific "variants", see
6868
# https://flutter.dev/assets-and-images/#resolution-aware

example/test/widget_test.dart

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,6 @@
55
// gestures. You can also use WidgetTester to find child widgets in the widget
66
// tree, read text, and verify that the values of widget properties are correct.
77

8-
import 'package:flutter/material.dart';
9-
import 'package:flutter_test/flutter_test.dart';
10-
11-
import 'package:example/main.dart';
12-
138
void main() {
14-
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
15-
// Build our app and trigger a frame.
16-
await tester.pumpWidget(const MyApp());
17-
18-
// Verify that our counter starts at 0.
19-
expect(find.text('0'), findsOneWidget);
20-
expect(find.text('1'), findsNothing);
21-
22-
// Tap the '+' icon and trigger a frame.
23-
await tester.tap(find.byIcon(Icons.add));
24-
await tester.pump();
25-
26-
// Verify that our counter has incremented.
27-
expect(find.text('0'), findsNothing);
28-
expect(find.text('1'), findsOneWidget);
29-
});
9+
// TODO: Write widget tests
3010
}

lib/image_hotspot.dart

Lines changed: 1 addition & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,3 @@
11
library image_hotspot;
2-
import 'package:flutter/material.dart';
32

4-
5-
class ImageHotspot extends StatelessWidget {
6-
final String imagePath;
7-
final List<Hotspot> hotspots;
8-
9-
const ImageHotspot({super.key, required this.imagePath, required this.hotspots});
10-
11-
@override
12-
Widget build(BuildContext context) {
13-
return Stack(
14-
children: [
15-
Image.asset(imagePath),
16-
...hotspots.map((hotspot) {
17-
return Positioned(
18-
left: hotspot.x,
19-
top: hotspot.y,
20-
child: GestureDetector(
21-
onTap: hotspot.onTap,
22-
child: Icon(
23-
Icons.circle,
24-
color: Colors.red.withOpacity(0.5),
25-
size: 24.0,
26-
),
27-
),
28-
);
29-
}),
30-
],
31-
);
32-
}
33-
}
34-
35-
class Hotspot {
36-
final double x;
37-
final double y;
38-
final VoidCallback onTap;
39-
40-
Hotspot({required this.x, required this.y, required this.onTap});
41-
}
3+
export 'src/image_hotspot_widget.dart';

lib/src/image_hotspot_widget.dart

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import 'package:flutter/material.dart';
2+
3+
/// A widget that displays an image with interactive hotspots.
4+
///
5+
/// This widget allows you to create clickable areas (hotspots) on an image.
6+
/// Each hotspot can have a custom icon, tooltip, and action when tapped.
7+
class ImageHotspot extends StatefulWidget {
8+
/// The path to the image asset.
9+
final String imagePath;
10+
11+
/// A list of [Hotspot] objects representing clickable areas on the image.
12+
final List<Hotspot> hotspots;
13+
14+
/// How the image should be inscribed into the space allocated during layout.
15+
final BoxFit imageFit;
16+
17+
/// The width of the image. Defaults to [double.infinity].
18+
final double imageWidth;
19+
20+
/// The height of the image. Defaults to [double.infinity].
21+
final double imageHeight;
22+
23+
/// Whether to show tooltips when a hotspot is tapped. Defaults to true.
24+
final bool showTooltip;
25+
26+
/// Creates an [ImageHotspot] widget.
27+
///
28+
/// The [imagePath] and [hotspots] parameters are required.
29+
const ImageHotspot({
30+
super.key,
31+
required this.imagePath,
32+
required this.hotspots,
33+
this.imageFit = BoxFit.cover,
34+
this.imageWidth = double.infinity,
35+
this.imageHeight = double.infinity,
36+
this.showTooltip = true,
37+
});
38+
39+
@override
40+
_ImageHotspotState createState() => _ImageHotspotState();
41+
}
42+
43+
class _ImageHotspotState extends State<ImageHotspot> {
44+
/// The currently active hotspot, if any.
45+
Hotspot? _activeHotspot;
46+
47+
@override
48+
Widget build(BuildContext context) {
49+
return Stack(
50+
children: [
51+
Image.asset(
52+
widget.imagePath,
53+
fit: widget.imageFit,
54+
width: widget.imageWidth,
55+
height: widget.imageHeight,
56+
),
57+
...widget.hotspots.map((hotspot) {
58+
return Positioned(
59+
left: hotspot.x,
60+
top: hotspot.y,
61+
child: GestureDetector(
62+
onTap: () {
63+
setState(() {
64+
_activeHotspot = hotspot;
65+
});
66+
hotspot.onTap();
67+
},
68+
child: hotspot.icon ?? _defaultHotspotIcon(hotspot),
69+
),
70+
);
71+
}),
72+
if (widget.showTooltip && _activeHotspot != null)
73+
_buildTooltip(context, _activeHotspot!),
74+
],
75+
);
76+
}
77+
78+
/// Builds a tooltip for the given [hotspot].
79+
///
80+
/// The tooltip is positioned relative to the hotspot and image size.
81+
Widget _buildTooltip(BuildContext context, Hotspot hotspot) {
82+
final tooltipText = hotspot.tooltip ?? 'You tapped here';
83+
final iconSize = hotspot.size;
84+
85+
// Calculate tooltip position relative to image size
86+
final imageSize = Size(widget.imageWidth, widget.imageHeight);
87+
double tooltipLeft = hotspot.x + iconSize - 8; // 8 is padding
88+
double tooltipTop = hotspot.y - iconSize - 12; // 8 is padding
89+
90+
const tooltipWidth = 150.0; // Adjust as needed
91+
92+
// Adjust tooltip position to stay within image bounds
93+
if (tooltipLeft + tooltipWidth > imageSize.width) {
94+
tooltipLeft = imageSize.width - tooltipWidth - 16; // Adjust for padding
95+
}
96+
if (tooltipTop < 0) {
97+
tooltipTop = hotspot.y + iconSize + 8; // Adjust for padding
98+
}
99+
100+
return Positioned(
101+
left: tooltipLeft,
102+
top: tooltipTop,
103+
child: Container(
104+
padding: const EdgeInsets.all(8),
105+
decoration: BoxDecoration(
106+
color: Colors.black.withOpacity(0.8),
107+
borderRadius: BorderRadius.circular(8),
108+
),
109+
child: Text(
110+
tooltipText,
111+
style: const TextStyle(color: Colors.white),
112+
),
113+
),
114+
);
115+
}
116+
117+
/// Creates a default icon for a hotspot if no custom icon is provided.
118+
Widget _defaultHotspotIcon(Hotspot hotspot) {
119+
return Container(
120+
width: hotspot.size,
121+
height: hotspot.size,
122+
decoration: BoxDecoration(
123+
shape: BoxShape.circle,
124+
color: hotspot.color.withOpacity(0.5),
125+
border: Border.all(color: hotspot.color, width: 2),
126+
),
127+
);
128+
}
129+
}
130+
131+
/// Represents a clickable area on an image.
132+
class Hotspot {
133+
/// The x-coordinate of the hotspot.
134+
final double x;
135+
136+
/// The y-coordinate of the hotspot.
137+
final double y;
138+
139+
/// The function to call when the hotspot is tapped.
140+
final VoidCallback onTap;
141+
142+
/// The text to display in the tooltip when the hotspot is tapped.
143+
final String? tooltip;
144+
145+
/// A custom icon to display for the hotspot.
146+
final Widget? icon;
147+
148+
/// The color of the default hotspot icon.
149+
final Color color;
150+
151+
/// The size of the hotspot icon.
152+
final double size;
153+
154+
/// Creates a [Hotspot].
155+
///
156+
/// The [x], [y], and [onTap] parameters are required.
157+
Hotspot({
158+
required this.x,
159+
required this.y,
160+
required this.onTap,
161+
this.tooltip,
162+
this.icon,
163+
this.color = Colors.red,
164+
this.size = 24.0,
165+
});
166+
}

pubspec.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: image_hotspot
2-
description: A Flutter package to create interactive hotspots on images.
2+
description: "Create interactive hotspots on images in Flutter apps. Add clickable regions with custom shapes, tooltips, and actions for engaging user experiences."
33
version: 0.0.1
44
homepage: https://github.com/vishalxtyagi/image_hotspot
55

@@ -24,7 +24,7 @@ flutter:
2424

2525
# To add assets to your package, add an assets section, like this:
2626
assets:
27-
- assets/images/sample.jpg
27+
- assets/test_image.jpg
2828

2929
# For details regarding assets in packages, see
3030
# https://flutter.dev/assets-and-images/#from-packages

0 commit comments

Comments
 (0)