image_marker_viewer 1.0.3
image_marker_viewer: ^1.0.3 copied to clipboard
A Flutter package for adding interactive markers on images with custom colors, titles, and notes. Supports web, mobile, and desktop platforms.
example/lib/main.dart
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:file_picker/file_picker.dart';
import 'package:image_marker_viewer/widgets/image_marker_viewer.dart';
import 'package:image_marker_viewer/controllers/image_marker_controller.dart';
import 'package:image_marker_viewer/models/image_marker.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return GetMaterialApp(
debugShowCheckedModeBanner: false,
title: 'Image Marker Viewer Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MarkerExample(),
);
}
}
class MarkerExample extends StatefulWidget {
const MarkerExample({super.key});
@override
State<MarkerExample> createState() => _MarkerExampleState();
}
class _MarkerExampleState extends State<MarkerExample>
with SingleTickerProviderStateMixin {
// Her görsel için ayrı controller tag'leri
final List<String> _imageTags = [];
final Map<String, Uint8List> _images = {}; // tag -> image bytes
final Map<String, String> _imageNames = {}; // tag -> image name
int _selectedImageIndex = 0;
ImageMarker? selectedMarker;
String? _currentImageTag;
TabController? _tabController;
@override
void initState() {
super.initState();
_updateTabController();
}
@override
void dispose() {
_tabController?.dispose();
super.dispose();
}
void _updateTabController() {
_tabController?.dispose();
if (_imageTags.isNotEmpty) {
_tabController = TabController(
length: _imageTags.length,
initialIndex: _selectedImageIndex.clamp(0, _imageTags.length - 1),
vsync: this,
);
_tabController!.addListener(() {
if (!_tabController!.indexIsChanging) {
setState(() {
_selectedImageIndex = _tabController!.index;
_currentImageTag = _imageTags[_selectedImageIndex];
selectedMarker = null;
});
}
});
}
}
Future<void> pickImage() async {
final result = await FilePicker.platform.pickFiles(
type: FileType.image,
allowMultiple: true,
);
if (result != null && result.files.isNotEmpty) {
setState(() {
for (var file in result.files) {
if (file.bytes != null) {
// Her görsel için benzersiz tag oluştur
final tag =
'image_${DateTime.now().millisecondsSinceEpoch}_${_imageTags.length}';
_imageTags.add(tag);
_images[tag] = file.bytes!;
_imageNames[tag] = file.name;
// Controller'ı tag ile oluştur
if (!Get.isRegistered<ImageMarkerController>(tag: tag)) {
Get.put(ImageMarkerController(), tag: tag);
}
}
}
// Yeni eklenen ilk görseli seç
if (_imageTags.isNotEmpty) {
_selectedImageIndex = _imageTags.length - 1;
_currentImageTag = _imageTags[_selectedImageIndex];
}
selectedMarker = null;
_updateTabController();
});
}
}
void _onMarkerAdded(ImageMarker marker) {
setState(() {
// Marker eklendiğinde UI güncellenir (Obx ile zaten reaktif)
});
}
void _onMarkerSelected(ImageMarker marker) {
setState(() {
selectedMarker = marker;
});
}
ImageMarkerController _getCurrentController() {
if (_currentImageTag == null) {
return Get.put(ImageMarkerController());
}
if (Get.isRegistered<ImageMarkerController>(tag: _currentImageTag)) {
return Get.find<ImageMarkerController>(tag: _currentImageTag);
}
return Get.put(ImageMarkerController(), tag: _currentImageTag);
}
@override
Widget build(BuildContext context) {
// Mevcut görsel tag'ini güncelle
if (_imageTags.isNotEmpty && _selectedImageIndex < _imageTags.length) {
_currentImageTag = _imageTags[_selectedImageIndex];
} else {
_currentImageTag = null;
}
return Scaffold(
appBar: AppBar(
title: Text(_currentImageTag != null
? 'Image Marker Viewer - ${_imageNames[_currentImageTag] ?? "Görsel ${_selectedImageIndex + 1}"}'
: 'Image Marker Viewer'),
actions: [
if (_currentImageTag != null)
IconButton(
icon: const Icon(Icons.download),
onPressed: () {
final controller = _getCurrentController();
final json = controller.exportToJson();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('JSON export:\n$json'),
duration: const Duration(seconds: 5),
),
);
},
tooltip: 'Markerları JSON olarak export et',
),
],
bottom: _imageTags.length > 1 && _tabController != null
? PreferredSize(
preferredSize: const Size.fromHeight(48),
child: Obx(() => TabBar(
controller: _tabController,
isScrollable: true,
tabs: _imageTags.asMap().entries.map((entry) {
final index = entry.key;
final tag = entry.value;
if (Get.isRegistered<ImageMarkerController>(tag: tag)) {
final controller =
Get.find<ImageMarkerController>(tag: tag);
return Tab(
text:
'${_imageNames[tag] ?? 'Görsel ${index + 1}'} (${controller.markers.length})',
icon: const Icon(Icons.image),
);
} else {
return Tab(
text: _imageNames[tag] ?? 'Görsel ${index + 1}',
icon: const Icon(Icons.image),
);
}
}).toList(),
)),
)
: null,
),
floatingActionButton: FloatingActionButton(
onPressed: pickImage,
child: const Icon(Icons.add_photo_alternate),
tooltip: 'Görsel Ekle',
),
body: _imageTags.isEmpty
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.image, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'Henüz bir görsel seçilmedi.',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
SizedBox(height: 8),
Text(
'Yukarıdaki butona tıklayarak görsel ekleyin',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
],
),
)
: Row(
children: [
// Sol taraf - Görsel
Expanded(
flex: 2,
child: Container(
margin: const EdgeInsets.all(16),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: _currentImageTag != null
? ImageMarkerViewer(
key: ValueKey(
_currentImageTag), // Her görsel için farklı key
image: MemoryImage(_images[_currentImageTag]!),
tag: _currentImageTag, // Her görsel için ayrı tag
onMarkerAdded: _onMarkerAdded,
onMarkerSelected: _onMarkerSelected,
)
: const Center(child: Text('Görsel yükleniyor...')),
),
),
),
// Sağ taraf - Marker Bilgileri
Expanded(
flex: 1,
child: Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[300]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.info_outline, color: Colors.grey[700]),
const SizedBox(width: 8),
Text(
'Marker Bilgileri',
style: Theme.of(context)
.textTheme
.titleLarge
?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 20),
// Seçili Marker Bilgisi
if (selectedMarker != null)
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: selectedMarker!.color
.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: selectedMarker!.color,
width: 2,
),
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: selectedMarker!.color,
shape: BoxShape.circle,
border: Border.all(
color: Colors.white,
width: 2,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
selectedMarker!.markerTitle ??
'Başlıksız Marker',
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(
fontWeight:
FontWeight.bold,
),
),
),
],
),
if (selectedMarker!.markerTitle !=
null &&
selectedMarker!
.markerTitle!.isNotEmpty) ...[
const SizedBox(height: 12),
Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Icon(
Icons.title,
size: 18,
color: Colors.grey[600],
),
const SizedBox(width: 8),
Expanded(
child: Text(
selectedMarker!.markerTitle!,
style: TextStyle(
fontSize: 14,
color: Colors.grey[800],
),
),
),
],
),
],
if (selectedMarker!.note != null &&
selectedMarker!
.note!.isNotEmpty) ...[
const SizedBox(height: 12),
Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Icon(
Icons.note,
size: 18,
color: Colors.grey[600],
),
const SizedBox(width: 8),
Expanded(
child: Text(
selectedMarker!.note!,
style: TextStyle(
fontSize: 14,
color: Colors.grey[800],
),
),
),
],
),
],
const SizedBox(height: 12),
Row(
children: [
Icon(
Icons.palette,
size: 18,
color: Colors.grey[600],
),
const SizedBox(width: 8),
Text(
'Renk: ',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: selectedMarker!.color,
shape: BoxShape.circle,
border: Border.all(
color: Colors.grey[300]!,
),
),
),
const SizedBox(width: 8),
Text(
'#${selectedMarker!.color.value.toRadixString(16).substring(2).toUpperCase()}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontFamily: 'monospace',
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Icon(
Icons.location_on,
size: 18,
color: Colors.grey[600],
),
const SizedBox(width: 8),
Text(
'Pozisyon: ',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
Text(
'(${selectedMarker!.x.toStringAsFixed(2)}, ${selectedMarker!.y.toStringAsFixed(2)})',
style: TextStyle(
fontSize: 12,
color: Colors.grey[800],
fontFamily: 'monospace',
),
),
],
),
],
),
),
],
),
),
)
else
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.touch_app,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'Marker\'a tıklayın',
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
Text(
'Bilgileri burada görüntüleyin',
style: TextStyle(
fontSize: 14,
color: Colors.grey[500],
),
),
],
),
),
),
const Divider(),
// Tüm Markerlar Listesi
Obx(() {
final controller = _getCurrentController();
final markers = controller.markers;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Tüm Markerlar (${markers.length})',
style: Theme.of(context)
.textTheme
.titleSmall
?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
if (markers.isEmpty)
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Henüz marker eklenmedi',
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
),
)
else
SizedBox(
height: 100,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: markers.length,
itemBuilder: (context, index) {
final marker = markers[index];
return GestureDetector(
onTap: () {
setState(() {
selectedMarker = marker;
});
},
child: Container(
width: 80,
margin:
const EdgeInsets.only(right: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: marker == selectedMarker
? marker.color.withOpacity(0.2)
: Colors.white,
borderRadius:
BorderRadius.circular(8),
border: Border.all(
color: marker == selectedMarker
? marker.color
: Colors.grey[300]!,
width: marker == selectedMarker
? 2
: 1,
),
),
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: marker.color,
shape: BoxShape.circle,
border: Border.all(
color: Colors.white,
width: 2,
),
),
),
const SizedBox(height: 8),
Text(
marker.markerTitle ??
'Marker ${index + 1}',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
},
),
),
],
);
}),
],
),
),
),
],
),
);
}
}