flutter_custom_overlay 0.2.1
flutter_custom_overlay: ^0.2.1 copied to clipboard
A Flutter plugin for creating customizable overlay windows on Android with bidirectional data communication and dynamic control.
example/lib/main.dart
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_custom_overlay/flutter_custom_overlay.dart';
void main() {
runApp(const MyApp());
}
/// Entry point for the overlay
@pragma('vm:entry-point')
void overlayMain() {
runApp(const OverlayApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Custom Overlay 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> {
bool _hasPermission = false;
bool _isOverlayActive = false;
int _overlayX = 0;
int _overlayY = 100;
int _overlayWidth = 300;
int _overlayHeight = 200;
OverlayAlignment _overlayAlignment = OverlayAlignment.topLeft;
String _lastReceivedData = 'No data yet';
@override
void initState() {
super.initState();
_checkPermission();
_listenToOverlayData();
// Check if overlay is already active (auto-initialized)
_checkOverlayStatus();
}
Future<void> _checkOverlayStatus() async {
final isActive = await FlutterCustomOverlay.isOverlayActive();
if (isActive) {
setState(() {
_isOverlayActive = true;
});
// Optionally show a snackbar
// Removed per user request
}
}
Future<void> _checkPermission() async {
final hasPermission = await FlutterCustomOverlay.hasOverlayPermission();
setState(() {
_hasPermission = hasPermission;
});
}
Future<void> _requestPermission() async {
await FlutterCustomOverlay.requestOverlayPermission();
// Wait a bit and check again
await Future.delayed(const Duration(seconds: 1));
await _checkPermission();
}
void _listenToOverlayData() {
FlutterCustomOverlay.overlayStream.listen((data) {
setState(() {
_lastReceivedData = data.data.toString();
});
});
}
Future<void> _showOverlay() async {
final config = OverlayConfig(
width: _overlayWidth,
height: _overlayHeight,
alignment: _overlayAlignment,
isDraggable: true,
isClickThrough: false,
);
// No need to pass entryPoint if auto-initialized or defaulting to 'overlayMain'
final success = await FlutterCustomOverlay.showOverlay(
config: config,
data: {
'message': 'Hello from showOverlay!',
'timestamp': DateTime.now().toIso8601String(),
},
);
if (success) {
final isActive = await FlutterCustomOverlay.isOverlayActive();
setState(() {
_isOverlayActive = isActive;
});
}
}
Future<void> _hideOverlay() async {
final success = await FlutterCustomOverlay.hideOverlay();
if (success) {
setState(() {
// We keep it active in background, just window hidden?
// Example UI logic: "Overlay Visible" vs "Overlay Hidden"
// _isOverlayActive tracks visibility usually?
// The checkOverlayStatus tracks SERVICE status.
// Let's assume _isOverlayActive tracks SERVICE status in this refactor?
// No, user UI says "Overlay Visible/Hidden".
// But the icon uses _isOverlayActive.
// I will decouple if needed, but for now let's assume Hide -> Visible=False.
// But Service is still running.
});
}
}
Future<void> _hideAndRestartOverlay() async {
final success = await FlutterCustomOverlay.hideAndRestartOverlay();
if (success) {
setState(() {
// UI assumes hidden
});
if (mounted) {
// Removed snackbar per user request
}
}
}
Future<void> _updatePosition() async {
await FlutterCustomOverlay.updatePosition(x: _overlayX, y: _overlayY);
}
Future<void> _updateSize() async {
await FlutterCustomOverlay.updateSize(
width: _overlayWidth,
height: _overlayHeight,
);
}
Future<void> _sendDataToOverlay() async {
final data = {
'message': 'Hello from main app!',
'timestamp': DateTime.now().toIso8601String(),
'counter': DateTime.now().millisecondsSinceEpoch,
};
await FlutterCustomOverlay.shareData(data);
}
// Removed _initializeOverlay as it's no longer needed
Future<void> _closeOverlay() async {
await FlutterCustomOverlay.closeOverlay();
setState(() {
_isOverlayActive = false;
});
if (mounted) {
// Removed snackbar per user request
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Flutter Custom Overlay Demo'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Permission section
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Permission Status',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Row(
children: [
Icon(
_hasPermission ? Icons.check_circle : Icons.cancel,
color: _hasPermission ? Colors.green : Colors.red,
),
const SizedBox(width: 8),
Text(
_hasPermission
? 'Overlay permission granted'
: 'Overlay permission not granted',
),
],
),
if (!_hasPermission) ...[
const SizedBox(height: 8),
ElevatedButton(
onPressed: _requestPermission,
child: const Text('Request Permission'),
),
],
],
),
),
),
const SizedBox(height: 16),
// Overlay control section
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Overlay Control',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Row(
children: [
Icon(
_isOverlayActive // Use logical OR if we track window separately, but assuming isOverlayActive tracks service
? Icons.visibility
: Icons.visibility_off,
color: _isOverlayActive ? Colors.green : Colors.grey,
),
const SizedBox(width: 8),
Text(
_isOverlayActive
? 'Overlay Active'
: 'Overlay Inactive',
),
],
),
const SizedBox(height: 16),
Wrap(
children: [
ElevatedButton(
onPressed: _showOverlay,
child: const Text('Show Overlay'),
),
ElevatedButton(
onPressed: _hideOverlay,
child: const Text('Hide (Keep State)'),
),
ElevatedButton(
onPressed: _hideAndRestartOverlay,
child: const Text('Hide (Reset)'),
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () async {
final isVisible =
await FlutterCustomOverlay.isOverlayVisible();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
isVisible
? 'Overlay is VISIBLE'
: 'Overlay is HIDDEN',
),
backgroundColor: isVisible
? Colors.green
: Colors.grey,
),
);
}
},
child: const Text('Check Visibility'),
),
ElevatedButton(
onPressed: _closeOverlay,
child: const Text('Shutdown Service'),
),
],
),
],
),
),
),
const SizedBox(height: 16),
// Position and size controls
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Position & Size',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
Text('X: $_overlayX'),
Slider(
value: _overlayX.toDouble(),
min: -500,
max: 500,
divisions: 100,
label: _overlayX.toString(),
onChanged: (value) {
setState(() {
_overlayX = value.toInt();
});
},
),
Text('Y: $_overlayY'),
Slider(
value: _overlayY.toDouble(),
min: -500,
max: 1000,
divisions: 100,
label: _overlayY.toString(),
onChanged: (value) {
setState(() {
_overlayY = value.toInt();
});
},
),
Text('Width: $_overlayWidth'),
Slider(
value: _overlayWidth.toDouble(),
min: 100,
max: 3000,
divisions: 50,
label: _overlayWidth.toString(),
onChanged: (value) {
setState(() {
_overlayWidth = value.toInt();
});
},
),
Text('Height: $_overlayHeight'),
Slider(
value: _overlayHeight.toDouble(),
min: 100,
max: 3000,
divisions: 50,
label: _overlayHeight.toString(),
onChanged: (value) {
setState(() {
_overlayHeight = value.toInt();
});
},
),
const SizedBox(height: 16),
Text(
'Alignment: ${_overlayAlignment.name}',
style: Theme.of(context).textTheme.titleSmall,
),
DropdownButton<OverlayAlignment>(
value: _overlayAlignment,
isExpanded: true,
onChanged: (OverlayAlignment? value) {
if (value != null) {
setState(() {
_overlayAlignment = value;
});
}
},
items: OverlayAlignment.values.map((
OverlayAlignment alignment,
) {
return DropdownMenuItem<OverlayAlignment>(
value: alignment,
child: Text(alignment.name),
);
}).toList(),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: _isOverlayActive
? _updatePosition
: null,
child: const Text('Update Position'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: _isOverlayActive ? _updateSize : null,
child: const Text('Update Size'),
),
),
],
),
],
),
),
),
const SizedBox(height: 16),
// Data sharing section
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Data Sharing',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: _isOverlayActive ? _sendDataToOverlay : null,
icon: const Icon(Icons.send),
label: const Text('Send Data to Overlay'),
),
const SizedBox(height: 16),
Text(
'Last Received from Overlay:',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: Text(_lastReceivedData),
),
],
),
),
),
],
),
),
);
}
}
// Overlay App
class OverlayApp extends StatelessWidget {
const OverlayApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: OverlayWidget(),
);
}
}
class OverlayWidget extends StatefulWidget {
const OverlayWidget({super.key});
@override
State<OverlayWidget> createState() => _OverlayWidgetState();
}
class _OverlayWidgetState extends State<OverlayWidget> {
String _receivedData = 'Waiting for data...';
int _counter = 0;
@override
void initState() {
super.initState();
// Initialize listener for overlay communication
log('OverlayWidget initState');
OverlayMessenger.listen();
_listenToMainApp();
}
void _listenToMainApp() {
// Listen to data from main app via stream
OverlayMessenger.onDataReceived.listen((data) {
if (data is Map || data is String) {
log("data received: $data");
setState(() {
_receivedData = data.toString();
});
}
});
}
Future<void> _sendDataToMainApp() async {
// Send data back to main app using the helper
final success = await OverlayMessenger.sendToMainApp({
'message': 'Hello from overlay!',
'counter': _counter++,
'timestamp': DateTime.now().toIso8601String(),
});
if (!success) {
print('Failed to send data to main app');
}
}
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.purple.shade400, Colors.blue.shade400],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.layers, color: Colors.white, size: 40),
const SizedBox(height: 8),
const Text(
'Custom Overlay',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Text(
_receivedData,
style: const TextStyle(color: Colors.white, fontSize: 12),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _sendDataToMainApp,
icon: const Icon(Icons.send),
label: const Text('Send to Main App'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.purple,
),
),
const SizedBox(height: 8),
const Text(
'Drag me around!',
style: TextStyle(
color: Colors.white70,
fontSize: 12,
fontStyle: FontStyle.italic,
),
),
],
),
),
),
);
}
}