arc_notch_bottom_nav 0.1.1
arc_notch_bottom_nav: ^0.1.1 copied to clipboard
A Flutter bottom navigation package with a configurable arc-notch center action layout.
import 'package:arc_notch_bottom_nav/arc_notch_bottom_nav.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const ExampleApp());
}
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'arc_notch_bottom_nav example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFFF4ECE3)),
scaffoldBackgroundColor: const Color(0xFFF1E7DB),
useMaterial3: true,
),
home: const ExampleHomePage(),
);
}
}
class ExampleHomePage extends StatefulWidget {
const ExampleHomePage({super.key});
@override
State<ExampleHomePage> createState() => _ExampleHomePageState();
}
class _ExampleHomePageState extends State<ExampleHomePage> {
static const double _iphone17ProMaxWidthMm = 78.0;
static const double _iphone17ProMaxHeightMm = 163.4;
static const double _phoneAspectRatio =
_iphone17ProMaxWidthMm / _iphone17ProMaxHeightMm;
static const _surfaceColor = Color(0xFFFFFCF7);
static const _pageBackgroundColor = Color(0xFFF7EFE4);
int _currentIndex = 0;
static const items = [
ArcNotchBottomNavItem(
id: 'bookshelf',
label: 'Bookshelf',
inactiveIcon: CupertinoIcons.book,
activeIcon: CupertinoIcons.book_fill,
tooltip: 'Bookshelf',
),
ArcNotchBottomNavItem(
id: 'stats',
label: 'Stats',
inactiveIcon: CupertinoIcons.chart_bar,
activeIcon: CupertinoIcons.chart_bar_fill,
badgeData: ArcNotchBottomNavBadgeData.count(count: 8),
),
ArcNotchBottomNavItem(
id: 'review',
label: 'Review',
inactiveIcon: Icons.blur_circular_outlined,
activeIcon: Icons.blur_circular,
),
ArcNotchBottomNavItem(
id: 'profile',
label: 'Profile',
inactiveIcon: CupertinoIcons.person,
activeIcon: CupertinoIcons.person_fill,
badgeData: ArcNotchBottomNavBadgeData.dot(),
),
];
static const pages = [
_ExamplePage(
title: 'Bookshelf',
description:
'Book shelf home, recent reads, category navigation, and more.',
accentColor: Color(0xFFB98F54),
icon: CupertinoIcons.book_fill,
itemExamples: [
_ExampleItemData(label: 'Bookshelf', icon: CupertinoIcons.book_fill),
_ExampleItemData(label: 'Recent Reads', icon: CupertinoIcons.time),
_ExampleItemData(
label: 'Categories',
icon: CupertinoIcons.square_grid_2x2,
),
],
),
_ExamplePage(
title: 'Stats',
description:
'Reading time, check-in trends, completion rate, and other stats.',
accentColor: Color(0xFF6D8AA6),
icon: CupertinoIcons.chart_bar_fill,
itemExamples: [
_ExampleItemData(
label: 'Reading Time',
icon: CupertinoIcons.chart_bar_fill,
),
_ExampleItemData(
label: 'Completion Rate',
icon: CupertinoIcons.percent,
),
_ExampleItemData(
label: 'Check-in Trends',
icon: CupertinoIcons.graph_square,
),
],
),
_ExamplePage(
title: 'Review',
description:
'Retrospectives, knowledge review, timeline, and related content.',
accentColor: Color(0xFF7F8C6B),
icon: Icons.blur_circular,
itemExamples: [
_ExampleItemData(label: 'Retrospect', icon: Icons.blur_circular),
_ExampleItemData(label: 'Timeline', icon: CupertinoIcons.clock),
_ExampleItemData(
label: 'Knowledge Cards',
icon: CupertinoIcons.doc_text,
),
],
),
_ExamplePage(
title: 'Profile',
description: 'Profile, settings, membership center, and other modules.',
accentColor: Color(0xFF9C7D7D),
icon: CupertinoIcons.person_fill,
itemExamples: [
_ExampleItemData(label: 'Profile', icon: CupertinoIcons.person_fill),
_ExampleItemData(label: 'Settings', icon: CupertinoIcons.settings),
_ExampleItemData(label: 'Membership', icon: CupertinoIcons.star_fill),
],
),
];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: LayoutBuilder(
builder: (context, constraints) {
final maxWidth = (constraints.maxWidth - 32).clamp(320.0, 430.0);
final maxHeight = constraints.maxHeight - 32;
var phoneWidth = maxWidth;
var phoneHeight = phoneWidth / _phoneAspectRatio;
if (phoneHeight > maxHeight) {
phoneHeight = maxHeight;
phoneWidth = phoneHeight * _phoneAspectRatio;
}
return Center(
child: Container(
width: phoneWidth,
height: phoneHeight,
decoration: BoxDecoration(
color: _surfaceColor,
borderRadius: BorderRadius.circular(42),
border: Border.all(
color: Colors.white.withValues(alpha: 0.72),
width: 1.2,
),
boxShadow: [
BoxShadow(
color: const Color(0x332D2217),
blurRadius: 42,
offset: const Offset(0, 24),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(42),
child: ColoredBox(
color: _pageBackgroundColor,
child: Stack(
fit: StackFit.expand,
children: [
Positioned.fill(
child: IndexedStack(
index: _currentIndex,
children: pages,
),
),
Positioned(
left: 0,
right: 0,
bottom: 15,
child: ArcNotchBottomNav(
items: items,
currentIndex: _currentIndex,
onItemSelected: (index) {
setState(() {
_currentIndex = index;
});
},
centerAction: ArcNotchBottomNavCenterAction(
child: const Icon(
CupertinoIcons.pen,
size: 24,
color: Color(0xFF2E2B27),
),
onTap: () {
debugPrint('center action tapped');
},
),
theme: const ArcNotchBottomNavThemeData(
backgroundColor: _surfaceColor,
),
layout: const ArcNotchBottomNavLayoutData(
height: 94,
shellHeight: 64,
horizontalPadding: 12,
contentPadding: EdgeInsets.fromLTRB(18, 12, 18, 8),
centerButtonTopOffset: 0,
safeAreaMinBottom: 0,
maxBarWidth: 398,
),
),
),
],
),
),
),
),
);
},
),
);
}
}
class _ExamplePage extends StatelessWidget {
const _ExamplePage({
required this.title,
required this.description,
required this.accentColor,
required this.icon,
required this.itemExamples,
});
final String title;
final String description;
final Color accentColor;
final IconData icon;
final List<_ExampleItemData> itemExamples;
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 0),
child: Column(
children: [
const _DynamicIsland(),
const SizedBox(height: 20),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(22, 26, 22, 132),
child: Column(
children: [
Container(
width: 86,
height: 86,
decoration: BoxDecoration(
color: accentColor.withValues(alpha: 0.14),
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Icon(icon, size: 38, color: accentColor),
),
const SizedBox(height: 24),
Text(title, style: textTheme.headlineMedium),
const SizedBox(height: 14),
Text(
description,
textAlign: TextAlign.center,
style: textTheme.bodyLarge?.copyWith(
color: const Color(0xFF5F5A53),
height: 1.65,
),
),
const SizedBox(height: 22),
Container(
width: double.infinity,
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.62),
borderRadius: BorderRadius.circular(22),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Page & Navigation Mapping',
style: textTheme.titleSmall?.copyWith(
color: const Color(0xFF4B4339),
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 12),
Wrap(
spacing: 10,
runSpacing: 10,
children: itemExamples
.map(
(item) => _ExampleItemChip(
item: item,
accentColor: accentColor,
),
)
.toList(),
),
const SizedBox(height: 14),
Text(
'Each example page corresponds to a bottom navigation item. Replace these with your actual business pages and pass them to ArcNotchBottomNavScaffold.',
style: textTheme.bodySmall?.copyWith(
color: const Color(0xFF6A6258),
height: 1.55,
),
),
],
),
),
],
),
),
),
],
),
),
);
}
}
class _DynamicIsland extends StatelessWidget {
const _DynamicIsland();
@override
Widget build(BuildContext context) {
return Center(
child: Container(
width: 126,
height: 34,
decoration: BoxDecoration(
color: const Color(0xFF171717),
borderRadius: BorderRadius.circular(999),
),
),
);
}
}
class _ExampleItemChip extends StatelessWidget {
const _ExampleItemChip({required this.item, required this.accentColor});
final _ExampleItemData item;
final Color accentColor;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.84),
borderRadius: BorderRadius.circular(16),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(item.icon, size: 16, color: accentColor),
const SizedBox(width: 6),
Text(
item.label,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: const Color(0xFF5F5A53),
fontWeight: FontWeight.w500,
),
),
],
),
);
}
}
class _ExampleItemData {
const _ExampleItemData({required this.label, required this.icon});
final String label;
final IconData icon;
}