notch 1.0.0
notch: ^1.0.0 copied to clipboard
Detect the position and shape of the device's camera cutout (notch, hole-punch, Dynamic Island) and use it as the origin of overlays and animations. Android and iOS.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:notch/notch.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
// Draw edge-to-edge so the cutout region belongs to the app.
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
runApp(const NotchDemoApp());
}
class NotchDemoApp extends StatelessWidget {
const NotchDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'notch demo',
theme: ThemeData.dark(useMaterial3: true),
home: const DemoPage(),
);
}
}
class DemoPage extends StatefulWidget {
const DemoPage({super.key});
@override
State<DemoPage> createState() => _DemoPageState();
}
class _DemoPageState extends State<DemoPage> {
int _emergeKey = 0;
int _resolveGeneration = 0;
Notch? _resolved;
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Re-resolves on MediaQuery changes (e.g. rotation) for the exact path.
// The generation guard drops stale results if a newer resolve finishes
// first.
final generation = ++_resolveGeneration;
Notch.resolve(context).then((notch) {
if (mounted && generation == _resolveGeneration) {
setState(() => _resolved = notch);
}
});
}
@override
Widget build(BuildContext context) {
final notch = _resolved ?? Notch.of(context);
return Scaffold(
body: NotchOverlay(
notch: notch,
color: Colors.lightBlueAccent,
child: Stack(
children: [
SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Camera cutout',
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 12),
_InfoCard(notch: notch),
const Spacer(),
Center(
child: FilledButton.icon(
onPressed: () => setState(() => _emergeKey++),
icon: const Icon(Icons.animation),
label: const Text('Emerge from cutout'),
),
),
const SizedBox(height: 32),
],
),
),
),
if (_emergeKey > 0)
NotchEmerge(
key: ValueKey(_emergeKey),
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 16,
),
decoration: BoxDecoration(
color: Colors.lightBlueAccent,
borderRadius: BorderRadius.circular(24),
),
child: const Text(
'Hello from the cutout 👋',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
),
);
}
}
class _InfoCard extends StatelessWidget {
const _InfoCard({required this.notch});
final Notch notch;
@override
Widget build(BuildContext context) {
final rect = notch.primaryBounds;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Type: ${notch.type.name}'),
if (rect != null)
Text(
'Bounds: left=${rect.left.toStringAsFixed(0)}, '
'top=${rect.top.toStringAsFixed(0)}, '
'right=${rect.right.toStringAsFixed(0)}, '
'bottom=${rect.bottom.toStringAsFixed(0)}',
),
Text('Exact path: ${notch.isExactPath}'),
Text(
'Safe insets: top=${notch.safeInsets.top.toStringAsFixed(0)}, '
'left=${notch.safeInsets.left.toStringAsFixed(0)}, '
'right=${notch.safeInsets.right.toStringAsFixed(0)}, '
'bottom=${notch.safeInsets.bottom.toStringAsFixed(0)}',
),
],
),
),
);
}
}