flutter_floating_window 1.1.0
flutter_floating_window: ^1.1.0 copied to clipboard
A comprehensive Flutter plugin for creating and managing floating window overlays on Android devices. Features include interactive UI elements, real-time data synchronization, persistent storage, and [...]
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_floating_window/flutter_floating_window.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Floating Window Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const FloatingWindowDemo(),
);
}
}
class FloatingWindowDemo extends StatefulWidget {
const FloatingWindowDemo({super.key});
@override
State<FloatingWindowDemo> createState() => _FloatingWindowDemoState();
}
class _FloatingWindowDemoState extends State<FloatingWindowDemo> {
final FloatingWindowManager _windowManager = FloatingWindowManager.instance;
List<String> _activeWindows = [];
bool _hasPermission = false;
bool _isServiceRunning = false;
String _lastEvent = 'No events yet';
int _counter = 0;
SharedPreferences? _prefs;
@override
void initState() {
super.initState();
_initializePlugin();
_listenToEvents();
}
Future<void> _initializePlugin() async {
try {
// Initialize SharedPreferences
_prefs = await SharedPreferences.getInstance();
// Load saved counter value
final savedCounter = _prefs?.getInt('counter') ?? 0;
// Check overlay permission
final hasPermission = await _windowManager.hasOverlayPermission();
// Check if service is running
final isServiceRunning = await _windowManager.isServiceRunning();
// Get active windows
final activeWindows = await _windowManager.getActiveWindows();
setState(() {
_counter = savedCounter;
_hasPermission = hasPermission;
_isServiceRunning = isServiceRunning;
_activeWindows = activeWindows;
});
} catch (e) {
_showError('Failed to initialize: $e');
}
}
void _listenToEvents() {
_windowManager.eventStream.listen((event) {
setState(() {
_lastEvent = '${event.type} - ${event.windowId} at ${event.timestamp}';
});
// Handle specific events
switch (event.type) {
case FloatingWindowEventType.windowCreated:
_refreshActiveWindows();
_showSnackBar('Window created: ${event.windowId}');
break;
case FloatingWindowEventType.windowClosed:
_refreshActiveWindows();
_showSnackBar('Window closed: ${event.windowId}');
break;
case FloatingWindowEventType.windowClicked:
_showSnackBar('Window clicked: ${event.windowId}');
break;
case FloatingWindowEventType.windowMoved:
final data = event.data;
if (data != null) {
_showSnackBar('Window moved to (${data['x']}, ${data['y']})');
}
break;
case FloatingWindowEventType.error:
final data = event.data;
if (data != null) {
_showError('Error: ${data['errorMessage']}');
}
break;
default:
break;
}
});
}
Future<void> _refreshActiveWindows() async {
try {
final windows = await _windowManager.getActiveWindows();
setState(() {
_activeWindows = windows;
});
} catch (e) {
_showError('Failed to refresh windows: $e');
}
}
Future<void> _requestPermission() async {
try {
await _windowManager.requestOverlayPermission();
// Wait a bit for user to grant permission, then check again
await Future.delayed(const Duration(seconds: 2));
final hasPermission = await _windowManager.hasOverlayPermission();
setState(() {
_hasPermission = hasPermission;
});
if (hasPermission) {
_showSnackBar('Overlay permission granted!');
// Automatically start service when permission is granted
await _windowManager.startService();
final isRunning = await _windowManager.isServiceRunning();
setState(() {
_isServiceRunning = isRunning;
});
} else {
_showSnackBar('Please grant overlay permission in settings');
}
} catch (e) {
_showError('Failed to request permission: $e');
}
}
Future<void> _startService() async {
try {
await _windowManager.startService();
final isRunning = await _windowManager.isServiceRunning();
setState(() {
_isServiceRunning = isRunning;
});
_showSnackBar('Service started');
} catch (e) {
_showError('Failed to start service: $e');
}
}
Future<void> _stopService() async {
try {
await _windowManager.stopService();
setState(() {
_isServiceRunning = false;
_activeWindows.clear();
});
_showSnackBar('Service stopped');
} catch (e) {
_showError('Failed to stop service: $e');
}
}
Future<void> _createSimpleWindow() async {
try {
final windowId = await _windowManager.createSimpleWindow(
width: 300,
height: 200,
title: 'Counter: $_counter',
isDraggable: true,
);
_showSnackBar('Counter window created: $windowId');
await _refreshActiveWindows();
await _syncCounterToWindows();
} catch (e) {
_showError('Failed to create counter window: $e');
}
}
Future<void> _createCustomWindow() async {
if (!_hasPermission) {
_showError('Overlay permission required');
return;
}
if (!_isServiceRunning) {
await _startService();
}
try {
final config = FloatingWindowConfig(
width: 300,
height: 200,
title: 'Interactive Counter',
isDraggable: true,
isResizable: false,
showCloseButton: true,
opacity: 0.9,
initialX: 100,
initialY: 100,
backgroundColor: 0xFF2196F3, // Colors.blue
);
final windowId = await _windowManager.createWindow(config);
_showSnackBar('Interactive counter window created: $windowId');
await _refreshActiveWindows();
// Send initial counter data to the new window
await Future.delayed(const Duration(milliseconds: 500));
await _syncCounterToWindows();
} catch (e) {
_showError('Failed to create interactive window: $e');
}
}
Future<void> _sendDataToWindow(String windowId) async {
try {
final data = {
'title': 'Updated Title',
'content': 'Data sent at ${DateTime.now()}',
'timestamp': DateTime.now().millisecondsSinceEpoch,
};
await _windowManager.sendDataToWindow(windowId, data);
_showSnackBar('Data sent to window: $windowId');
} catch (e) {
_showError('Failed to send data: $e');
}
}
Future<void> _moveWindow(String windowId) async {
try {
await _windowManager.moveWindow(windowId, 200, 200);
_showSnackBar('Moved window: $windowId');
} catch (e) {
_showError('Failed to move window: $e');
}
}
Future<void> _resizeWindow(String windowId) async {
try {
await _windowManager.resizeWindow(windowId, 500, 400);
_showSnackBar('Resized window: $windowId');
} catch (e) {
_showError('Failed to resize window: $e');
}
}
Future<void> _closeWindow(String windowId) async {
try {
await _windowManager.closeWindow(windowId);
_showSnackBar('Closed window: $windowId');
} catch (e) {
_showError('Failed to close window: $e');
}
}
Future<void> _closeAllWindows() async {
try {
await _windowManager.closeAllWindows();
_showSnackBar('All windows closed');
} catch (e) {
_showError('Failed to close all windows: $e');
}
}
Future<void> _incrementCounter() async {
setState(() {
_counter++;
});
await _saveCounter();
await _syncCounterToWindows();
}
Future<void> _decrementCounter() async {
setState(() {
_counter = _counter > 0 ? _counter - 1 : 0;
});
await _saveCounter();
await _syncCounterToWindows();
}
Future<void> _saveCounter() async {
await _prefs?.setInt('counter', _counter);
}
Future<void> _syncCounterToWindows() async {
final data = {
'counter': _counter,
'timestamp': DateTime.now().millisecondsSinceEpoch,
};
for (final windowId in _activeWindows) {
try {
await _windowManager.sendDataToWindow(windowId, data);
} catch (e) {
// Ignore errors for individual windows
}
}
}
void _showSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
duration: const Duration(seconds: 2),
),
);
}
void _showError(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
duration: const Duration(seconds: 3),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Floating Window Demo'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Counter Display Card
Card(
color: Colors.blue.shade50,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
const Text(
'Counter Value',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Text(
'$_counter',
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: _decrementCounter,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
shape: const CircleBorder(),
padding: const EdgeInsets.all(16),
),
child: const Icon(Icons.remove, size: 24),
),
ElevatedButton(
onPressed: _incrementCounter,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
shape: const CircleBorder(),
padding: const EdgeInsets.all(16),
),
child: const Icon(Icons.add, size: 24),
),
],
),
],
),
),
),
const SizedBox(height: 16),
// Status Cards
Row(
children: [
Expanded(
child: Card(
color: _hasPermission ? Colors.green.shade100 : Colors.red.shade100,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Icon(
_hasPermission ? Icons.check_circle : Icons.cancel,
color: _hasPermission ? Colors.green : Colors.red,
size: 32,
),
const SizedBox(height: 8),
const Text(
'Overlay Permission',
style: TextStyle(fontWeight: FontWeight.bold),
),
Text(_hasPermission ? 'Granted' : 'Not Granted'),
],
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: Card(
color: _isServiceRunning ? Colors.green.shade100 : Colors.orange.shade100,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Icon(
_isServiceRunning ? Icons.play_circle : Icons.pause_circle,
color: _isServiceRunning ? Colors.green : Colors.orange,
size: 32,
),
const SizedBox(height: 8),
const Text(
'Service Status',
style: TextStyle(fontWeight: FontWeight.bold),
),
Text(_isServiceRunning ? 'Running' : 'Stopped'),
],
),
),
),
),
],
),
const SizedBox(height: 16),
// Permission and Service Controls
if (!_hasPermission)
ElevatedButton.icon(
onPressed: _requestPermission,
icon: const Icon(Icons.security),
label: const Text('Request Overlay Permission'),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _isServiceRunning ? null : _startService,
icon: const Icon(Icons.play_arrow),
label: const Text('Start Service'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: _isServiceRunning ? _stopService : null,
icon: const Icon(Icons.stop),
label: const Text('Stop Service'),
),
),
],
),
const SizedBox(height: 16),
// Window Creation Controls
Text(
'Create Counter Windows',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _hasPermission && _isServiceRunning ? _createSimpleWindow : null,
icon: const Icon(Icons.add_box),
label: const Text('Simple Counter'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(16),
),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton.icon(
onPressed: _hasPermission && _isServiceRunning ? _createCustomWindow : null,
icon: const Icon(Icons.dashboard_customize),
label: const Text('Interactive Counter'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(16),
),
),
),
],
),
const SizedBox(height: 16),
// Active Windows
Text(
'Active Windows (${_activeWindows.length})',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
if (_activeWindows.isEmpty)
const Card(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Text(
'No active windows',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
),
)
else
Expanded(
child: ListView.builder(
itemCount: _activeWindows.length,
itemBuilder: (context, index) {
final windowId = _activeWindows[index];
return Card(
child: ListTile(
leading: const Icon(Icons.window),
title: Text('Window ${index + 1}'),
subtitle: Text(windowId),
trailing: PopupMenuButton<String>(
onSelected: (action) {
switch (action) {
case 'send_data':
_sendDataToWindow(windowId);
break;
case 'move':
_moveWindow(windowId);
break;
case 'resize':
_resizeWindow(windowId);
break;
case 'close':
_closeWindow(windowId);
break;
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'send_data',
child: Text('Send Data'),
),
const PopupMenuItem(
value: 'move',
child: Text('Move Window'),
),
const PopupMenuItem(
value: 'resize',
child: Text('Resize Window'),
),
const PopupMenuItem(
value: 'close',
child: Text('Close Window'),
),
],
),
),
);
},
),
),
const SizedBox(height: 16),
// Close All Button
if (_activeWindows.isNotEmpty)
ElevatedButton.icon(
onPressed: _closeAllWindows,
icon: const Icon(Icons.clear_all),
label: const Text('Close All Windows'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
),
const SizedBox(height: 16),
// Last Event
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Last Event:',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 4),
Text(
_lastEvent,
style: const TextStyle(fontFamily: 'monospace'),
),
],
),
),
),
],
),
),
);
}
}