dynamic_glass_glmv 0.2.1
dynamic_glass_glmv: ^0.2.1 copied to clipboard
A Flutter package for reusable dynamic glass UI surfaces and adaptive platform bottom navigation.
example/lib/main.dart
import 'package:dynamic_glass_glmv/dynamic_glass_glmv.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
void main() => runApp(const DynamicGlassExampleApp());
class DynamicGlassExampleApp extends StatelessWidget {
const DynamicGlassExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'dynamic_glass example',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorSchemeSeed: const Color(0xFF0A84FF),
),
home: const _HomeShell(),
);
}
}
class _HomeShell extends StatefulWidget {
const _HomeShell();
@override
State<_HomeShell> createState() => _HomeShellState();
}
class _HomeShellState extends State<_HomeShell> {
int _ordersBadge = 5;
@override
Widget build(BuildContext context) {
return DynamicGlassNavigationScaffold(
selectedItemColor: const Color(0xFF0A84FF),
minimizeBehavior: TabBarMinimizeBehavior.never,
split: true,
rightCount: 1,
onIndexChanged: (i) {
if (i == 1 && _ordersBadge > 0) {
// Clear the badge once the user opens Orders.
setState(() => _ordersBadge = 0);
}
},
items: [
AdaptiveNavigationDestination(
icon: SvgPicture.asset('assets/icons/box_outlined.svg'),
selectedIcon: SvgPicture.asset('assets/icons/box_filled.svg'),
label: 'Home',
),
AdaptiveNavigationDestination(
icon: SvgPicture.asset('assets/icons/profile.svg'),
label: 'Orders',
badgeCount: _ordersBadge > 0 ? _ordersBadge : null,
),
AdaptiveNavigationDestination(
icon: SvgPicture.asset('assets/icons/profile.svg'),
label: 'Profile',
),
const AdaptiveNavigationDestination(
icon: 'magnifyingglass',
label: 'Search',
isSearch: true,
hideLabel: true,
),
],
screens: const [
_ScrollingPage(
title: 'Home',
subtitle: 'Scroll to see the iOS 26 tab bar minimize.',
accent: Color(0xFF0A84FF),
),
_ScrollingPage(
title: 'Orders',
subtitle:
'Tapping this tab cleared the badge — re-run the app to see it again.',
accent: Color(0xFFFF9F0A),
),
_GlassShowcasePage(),
_SearchPage(),
],
);
}
}
/// A page with vertical scrolling content so the iOS 26 minimize-on-scroll
/// behavior has something to respond to.
class _ScrollingPage extends StatelessWidget {
const _ScrollingPage({
required this.title,
required this.subtitle,
required this.accent,
});
final String title;
final String subtitle;
final Color accent;
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
SliverAppBar.large(
title: Text(title),
backgroundColor: accent.withValues(alpha: 0.15),
pinned: true,
),
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 120),
sliver: SliverList.builder(
itemCount: 40,
itemBuilder: (context, i) {
if (i == 0) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text(
subtitle,
style: Theme.of(context).textTheme.bodyLarge,
),
);
}
return Card(
child: ListTile(
leading: CircleAvatar(
backgroundColor: accent.withValues(alpha: 0.2),
child: Text('${i + 1}'),
),
title: Text('$title item ${i + 1}'),
subtitle: const Text('Tap me — or just scroll.'),
),
);
},
),
),
],
);
}
}
/// Minimal search surface — pairs with `isSearch: true` so the rightmost tab
/// renders as the detached `UISearchTab` pill on iOS 26.
class _SearchPage extends StatelessWidget {
const _SearchPage();
@override
Widget build(BuildContext context) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 120),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Search',
style: Theme.of(context).textTheme.headlineLarge,
),
const SizedBox(height: 16),
const CupertinoSearchTextField(
placeholder: 'Type to search…',
),
const SizedBox(height: 24),
Text(
'On iOS 26 the rightmost tab is a UISearchTab, drawn as a detached '
'liquid-glass pill next to the main bar.',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
);
}
}
/// Demonstrates the DynamicGlass surface widget plus the native adaptive
/// controls (switch, slider, segmented control, buttons, popup menu, icon)
/// over a colorful background.
class _GlassShowcasePage extends StatefulWidget {
const _GlassShowcasePage();
@override
State<_GlassShowcasePage> createState() => _GlassShowcasePageState();
}
class _GlassShowcasePageState extends State<_GlassShowcasePage> {
bool _switchOn = true;
double _sliderValue = 0.4;
int _segment = 0;
int _tapCount = 0;
String _menuChoice = 'None';
static const _accent = Color(0xFF0A84FF);
@override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: [
const DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0xFFFF375F),
Color(0xFF0A84FF),
Color(0xFF30D158),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
),
SafeArea(
child: ListView(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 120),
children: [
Text(
'DynamicGlass surface',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: CupertinoColors.white,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 24),
DynamicGlass(
blur: 24,
opacity: 0.18,
borderRadius: BorderRadius.circular(24),
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Native adaptive controls — UIKit/AppKit on Apple '
'platforms (liquid glass on iOS 26), Flutter elsewhere.',
style: TextStyle(
color: CupertinoColors.white,
fontSize: 15,
height: 1.4,
),
),
const SizedBox(height: 20),
// Switch
_ControlRow(
label: 'DynamicSwitch',
trailing: DynamicSwitch(
value: _switchOn,
color: _accent,
onChanged: (v) => setState(() => _switchOn = v),
),
),
const SizedBox(height: 16),
// Slider
const _ControlLabel('DynamicSlider'),
DynamicSlider(
value: _sliderValue,
color: _accent,
// Disabled when the switch is off, to show the
// nullable-onChanged disabled convention.
onChanged: _switchOn
? (v) => setState(() => _sliderValue = v)
: null,
),
const SizedBox(height: 16),
// Segmented control
const _ControlLabel('DynamicSegmentedControl'),
DynamicSegmentedControl(
labels: const ['Day', 'Week', 'Month'],
selectedIndex: _segment,
color: _accent,
onValueChanged: (i) => setState(() => _segment = i),
),
const SizedBox(height: 20),
// Buttons
Row(
children: [
DynamicButton(
label: 'Tap me ($_tapCount)',
tint: _accent,
style: DynamicButtonStyle.glass,
onPressed: () => setState(() => _tapCount++),
),
const SizedBox(width: 12),
DynamicButton.icon(
icon: const DynamicSymbol('heart.fill'),
tint: const Color(0xFFFF375F),
onPressed: () => setState(() => _tapCount++),
),
],
),
const SizedBox(height: 20),
// Popup menu + icon
_ControlRow(
label: 'Menu: $_menuChoice',
trailing: DynamicPopupMenuButton.icon(
buttonIcon: const DynamicSymbol('ellipsis.circle'),
items: const [
DynamicPopupMenuItem(
label: 'Share',
icon: DynamicSymbol('square.and.arrow.up'),
),
DynamicPopupMenuItem(
label: 'Duplicate',
icon: DynamicSymbol('plus.square.on.square'),
),
DynamicPopupMenuDivider(),
DynamicPopupMenuItem(
label: 'Delete',
icon: DynamicSymbol('trash'),
),
],
onSelected: (i) => setState(() {
_menuChoice =
const ['Share', 'Duplicate', 'Delete'][i];
}),
),
),
const SizedBox(height: 16),
// Icon
_ControlRow(
label: 'DynamicIcon',
trailing: const DynamicIcon(
symbol: DynamicSymbol(
'sparkles',
size: 28,
color: CupertinoColors.white,
),
),
),
],
),
),
],
),
),
],
);
}
}
/// A left-aligned white label used inside the glass showcase card.
class _ControlLabel extends StatelessWidget {
const _ControlLabel(this.text);
final String text;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
text,
style: const TextStyle(
color: CupertinoColors.white,
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
);
}
}
/// A label paired with a trailing control on the same row.
class _ControlRow extends StatelessWidget {
const _ControlRow({required this.label, required this.trailing});
final String label;
final Widget trailing;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
label,
style: const TextStyle(
color: CupertinoColors.white,
fontSize: 15,
fontWeight: FontWeight.w500,
),
),
),
trailing,
],
);
}
}