floating_app_icon 0.0.1
floating_app_icon: ^0.0.1 copied to clipboard
A Flutter plugin that displays the application's launcher icon as a floating overlay above other apps on Android.
example/lib/main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:floating_app_icon/floating_app_icon.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Floating App Icon Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.dark,
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.deepPurple,
brightness: Brightness.dark,
),
),
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
bool _hasPermission = false;
bool _isShowing = false;
bool _isLoading = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_refreshStates();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// Automatically refresh permission and visibility states when the app is resumed
if (state == AppLifecycleState.resumed) {
_refreshStates();
}
}
Future<void> _refreshStates() async {
setState(() => _isLoading = true);
try {
final granted = await FloatingAppIcon.hasPermission();
final showing = await FloatingAppIcon.isShowing();
setState(() {
_hasPermission = granted;
_isShowing = showing;
});
} catch (e) {
_showErrorSnackBar('Failed to load status: $e');
} finally {
setState(() => _isLoading = false);
}
}
Future<void> _requestPermission() async {
try {
await FloatingAppIcon.requestPermission();
// Settings screen is opened; didChangeAppLifecycleState will refresh state on resume.
} catch (e) {
_showErrorSnackBar('Failed to request permission: $e');
}
}
Future<void> _showOverlay() async {
if (!_hasPermission) {
_showErrorSnackBar('Please grant overlay permission first!');
return;
}
setState(() => _isLoading = true);
try {
await FloatingAppIcon.show();
await _refreshStates();
} catch (e) {
_showErrorSnackBar('Failed to show overlay: $e');
} finally {
setState(() => _isLoading = false);
}
}
Future<void> _hideOverlay() async {
setState(() => _isLoading = true);
try {
await FloatingAppIcon.hide();
await _refreshStates();
} catch (e) {
_showErrorSnackBar('Failed to hide overlay: $e');
} finally {
setState(() => _isLoading = false);
}
}
void _showErrorSnackBar(String message) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.redAccent,
behavior: SnackBarBehavior.floating,
),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
theme.colorScheme.surface,
theme.colorScheme.primaryContainer.withOpacity(0.15),
theme.colorScheme.surface,
],
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 32),
// App Header
Center(
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.colorScheme.primary.withOpacity(0.1),
shape: BoxShape.circle,
border: Border.all(
color: theme.colorScheme.primary.withOpacity(0.3),
width: 2,
),
),
child: Icon(
Icons.layers_outlined,
size: 48,
color: theme.colorScheme.primary,
),
),
const SizedBox(height: 16),
Text(
'Floating App Icon',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
const SizedBox(height: 8),
Text(
'Messenger-style overlay controller',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
const SizedBox(height: 48),
// Status Dashboard Card
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceVariant.withOpacity(0.3),
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: theme.colorScheme.onSurface.withOpacity(0.08),
),
),
child: Column(
children: [
_StatusRow(
title: 'Overlay Permission',
isActive: _hasPermission,
activeLabel: 'Granted',
inactiveLabel: 'Denied',
activeColor: Colors.green,
inactiveColor: Colors.amber,
),
const Divider(height: 32, thickness: 0.5),
_StatusRow(
title: 'Overlay Visibility',
isActive: _isShowing,
activeLabel: 'Showing',
inactiveLabel: 'Hidden',
activeColor: theme.colorScheme.primary,
inactiveColor: theme.colorScheme.onSurfaceVariant.withOpacity(0.5),
),
],
),
),
const SizedBox(height: 48),
// Action Controls
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ElevatedButton.icon(
onPressed: _isLoading ? null : _requestPermission,
icon: const Icon(Icons.security_outlined),
label: const Text('Request Overlay Permission'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _isLoading ? null : _showOverlay,
icon: const Icon(Icons.play_arrow_outlined),
label: const Text('Show Icon'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
side: BorderSide(color: theme.colorScheme.primary),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: OutlinedButton.icon(
onPressed: _isLoading ? null : _hideOverlay,
icon: const Icon(Icons.stop_outlined),
label: const Text('Hide Icon'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
side: BorderSide(color: theme.colorScheme.error.withOpacity(0.5)),
foregroundColor: theme.colorScheme.error,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
),
),
],
),
],
),
const Spacer(),
// Refresh Button
Center(
child: TextButton.icon(
onPressed: _isLoading ? null : _refreshStates,
icon: _isLoading
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: const Icon(Icons.refresh_outlined),
label: const Text('Manual Refresh'),
),
),
const SizedBox(height: 16),
],
),
),
),
),
);
}
}
class _StatusRow extends StatelessWidget {
final String title;
final bool isActive;
final String activeLabel;
final String inactiveLabel;
final Color activeColor;
final Color inactiveColor;
const _StatusRow({
required this.title,
required this.isActive,
required this.activeLabel,
required this.inactiveLabel,
required this.activeColor,
required this.inactiveColor,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final statusColor = isActive ? activeColor : inactiveColor;
final statusLabel = isActive ? activeLabel : inactiveLabel;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.12),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: statusColor.withOpacity(0.3),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: statusColor,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Text(
statusLabel,
style: theme.textTheme.bodyMedium?.copyWith(
color: statusColor,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
);
}
}