double_sheet 0.0.2
double_sheet: ^0.0.2 copied to clipboard
A Flutter package that provides a highly customizable bottom sheet with a synchronized header that moves smoothly as the sheet expands and contracts.
import 'package:double_sheet/double_sheet.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Double Sheet Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF6366F1)),
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
void _showDoubleSheet() {
showDoubleSheet(
context: context,
title: "freeze card",
headerRadius: const BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(32),
topRight: Radius.circular(32),
),
initialChildSize: 0.45,
maxChildSize: 0.7,
minChildSize: 0.4,
child: const Padding(
padding: EdgeInsets.all(24.0),
child: _SheetContentWidget(),
),
);
}
void _showTextFieldsSheet() {
showDoubleSheet(
context: context,
title: "Form with 20 Fields (Synchronized Scrolling)",
headerRadius: const BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(32),
topRight: Radius.circular(32),
),
initialChildSize: 0.7,
maxChildSize: 0.95,
minChildSize: 0.6,
enableSynchronizedScrolling: true,
child: const Padding(
padding: EdgeInsets.all(24.0),
child: _TextFieldsSheetContent(),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8F9FA),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
const SizedBox(height: 40),
const _CreditCard(),
const SizedBox(height: 32),
const _BottomNavigationIcons(),
const Spacer(),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _showDoubleSheet,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6366F1),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Text(
'Tap to freeze card',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: _showTextFieldsSheet,
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF6366F1),
side: const BorderSide(color: Color(0xFF6366F1)),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Text(
'Show 20 Text Fields',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
),
),
const SizedBox(height: 24),
],
),
),
),
);
}
}
class _SheetContentWidget extends StatelessWidget {
const _SheetContentWidget();
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"are you sure?",
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
const SizedBox(height: 16),
const Text(
"freezing the card means ayden can't use it to pay for things until you unfreeze it",
style: TextStyle(
fontSize: 16,
height: 1.5,
color: Colors.black54,
),
),
const Spacer(),
const _ActionButtons(),
],
),
),
),
);
},
);
}
}
class _ActionButtons extends StatelessWidget {
const _ActionButtons();
@override
Widget build(BuildContext context) {
return Column(
children: [
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
Navigator.pop(context);
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text("Card frozen!")));
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6366F1),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Text(
"freeze",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF6366F1),
side: const BorderSide(color: Color(0xFF6366F1)),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Text(
"nevermind",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
),
),
],
);
}
}
class _CreditCard extends StatelessWidget {
const _CreditCard();
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
height: 200,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF4C1D95), Color(0xFF6366F1)],
),
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Ayden",
style: TextStyle(
color: Colors.white70,
fontSize: 16,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 8),
Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.credit_card,
color: Colors.white70,
size: 20,
),
),
const Spacer(),
const Text(
"•••• 2345",
style: TextStyle(
color: Colors.white70,
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
],
),
const Spacer(),
const Text(
"total balance",
style: TextStyle(
color: Colors.white70,
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 4),
const Text(
"AED 1,258.00",
style: TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
const Text(
"AED 529.00 available | locked this month",
style: TextStyle(
color: Colors.white70,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
],
),
);
}
}
class _BottomNavigationIcons extends StatelessWidget {
const _BottomNavigationIcons();
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildNavIcon(Icons.account_balance_wallet, true),
_buildNavIcon(Icons.email_outlined, false),
_buildNavIcon(Icons.info_outline, false),
_buildNavIcon(Icons.credit_card_outlined, false),
_buildNavIcon(Icons.more_horiz, false),
],
),
);
}
Widget _buildNavIcon(IconData icon, bool isActive) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isActive ? const Color(0xFF6366F1) : Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
color: isActive ? Colors.white : Colors.grey.shade600,
size: 24,
),
);
}
}
class _TextFieldsSheetContent extends StatefulWidget {
const _TextFieldsSheetContent();
@override
State<_TextFieldsSheetContent> createState() =>
_TextFieldsSheetContentState();
}
class _TextFieldsSheetContentState extends State<_TextFieldsSheetContent> {
final List<TextEditingController> _controllers = List.generate(
20,
(index) => TextEditingController(),
);
@override
void dispose() {
for (final controller in _controllers) {
controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Form Example",
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
const SizedBox(height: 8),
const Text(
"Fill out all 20 fields below",
style: TextStyle(fontSize: 16, color: Colors.black54),
),
const SizedBox(height: 24),
Expanded(
child: ListView.builder(
itemCount: 20,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: TextField(
controller: _controllers[index],
decoration: InputDecoration(
labelText: 'Field ${index + 1}',
hintText: 'Enter value for field ${index + 1}',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFF6366F1)),
),
),
),
);
},
),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
Navigator.pop(context);
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text("Form submitted!")));
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6366F1),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Text(
"Submit",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
),
),
],
);
}
}