wolt_modal_sheet 0.4.0 wolt_modal_sheet: ^0.4.0 copied to clipboard
This package provides a responsive modal with multiple pages, motion animation for page transitions, and scrollable content within each page.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:wolt_modal_sheet/wolt_modal_sheet.dart';
void main() {
runApp(const MainApp());
}
const double _bottomPaddingForButton = 150.0;
const double _buttonHeight = 56.0;
const double _buttonWidth = 200.0;
const double _pagePadding = 16.0;
const double _pageBreakpoint = 768.0;
const double _heroImageHeight = 250.0;
const Color _lightThemeShadowColor = Color(0xFFE4E4E4);
const Color _darkThemeShadowColor = Color(0xFF121212);
const Color _darkSabGradientColor = Color(0xFF313236);
final materialColorsInGrid = allMaterialColors.take(20).toList();
final materialColorsInSliverList = allMaterialColors.sublist(20, 25);
final materialColorsInSpinner = allMaterialColors.sublist(30, 50);
class MainApp extends StatefulWidget {
const MainApp({super.key});
@override
State<MainApp> createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> {
bool _isLightTheme = true;
@override
Widget build(BuildContext context) {
final pageIndexNotifier = ValueNotifier(0);
SliverWoltModalSheetPage page1(
BuildContext modalSheetContext, TextTheme textTheme) {
return WoltModalSheetPage(
hasSabGradient: false,
stickyActionBar: Padding(
padding: const EdgeInsets.all(_pagePadding),
child: Column(
children: [
ElevatedButton(
onPressed: () => Navigator.of(modalSheetContext).pop(),
child: const SizedBox(
height: _buttonHeight,
width: double.infinity,
child: Center(child: Text('Cancel')),
),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () =>
pageIndexNotifier.value = pageIndexNotifier.value + 1,
child: const SizedBox(
height: _buttonHeight,
width: double.infinity,
child: Center(child: Text('Next page')),
),
),
],
),
),
topBarTitle: Text('Pagination', style: textTheme.titleSmall),
isTopBarLayerAlwaysVisible: true,
trailingNavBarWidget: IconButton(
padding: const EdgeInsets.all(_pagePadding),
icon: const Icon(Icons.close),
onPressed: Navigator.of(modalSheetContext).pop,
),
child: const Padding(
padding: EdgeInsets.fromLTRB(
_pagePadding,
_pagePadding,
_pagePadding,
_bottomPaddingForButton,
),
child: Text(
'''
Pagination involves a sequence of screens the user navigates sequentially. We chose a lateral motion for these transitions. When proceeding forward, the next screen emerges from the right; moving backward, the screen reverts to its original position. We felt that sliding the next screen entirely from the right could be overly distracting. As a result, we decided to move and fade in the next page using 30% of the modal side.
''',
)),
);
}
SliverWoltModalSheetPage page2(
BuildContext modalSheetContext, TextTheme textTheme) {
return SliverWoltModalSheetPage(
pageTitle: Padding(
padding: const EdgeInsets.all(_pagePadding),
child: Text(
'Material Colors',
style:
textTheme.headlineMedium!.copyWith(fontWeight: FontWeight.bold),
),
),
heroImage: Image(
image: AssetImage(
'lib/assets/images/material_colors_hero${_isLightTheme ? '_light' : '_dark'}.png',
),
fit: BoxFit.cover,
),
leadingNavBarWidget: IconButton(
padding: const EdgeInsets.all(_pagePadding),
icon: const Icon(Icons.arrow_back_rounded),
onPressed: () =>
pageIndexNotifier.value = pageIndexNotifier.value - 1,
),
trailingNavBarWidget: IconButton(
padding: const EdgeInsets.all(_pagePadding),
icon: const Icon(Icons.close),
onPressed: () {
Navigator.of(modalSheetContext).pop();
pageIndexNotifier.value = 0;
},
),
mainContentSlivers: [
SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 2.0,
),
delegate: SliverChildBuilderDelegate(
(_, index) => ColorTile(color: materialColorsInGrid[index]),
childCount: materialColorsInGrid.length,
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(_, index) => ColorTile(color: materialColorsInSliverList[index]),
childCount: materialColorsInSliverList.length,
),
),
...materialColorsInSpinner
.map((e) => Shifter(child: ColorTile(color: e)))
.toList(),
SliverPadding(
padding: const EdgeInsets.all(_pagePadding),
sliver: SliverToBoxAdapter(
child: TextButton(
onPressed: Navigator.of(modalSheetContext).pop,
child: const Text('Close'),
),
),
),
],
);
}
return MaterialApp(
themeMode: _isLightTheme ? ThemeMode.light : ThemeMode.dark,
theme: ThemeData.light(useMaterial3: true).copyWith(
extensions: const <ThemeExtension>[
WoltModalSheetThemeData(
heroImageHeight: _heroImageHeight,
topBarShadowColor: _lightThemeShadowColor,
modalBarrierColor: Colors.black54,
mainContentScrollPhysics: ClampingScrollPhysics(),
),
],
),
darkTheme: ThemeData.dark(useMaterial3: true).copyWith(
extensions: const <ThemeExtension>[
WoltModalSheetThemeData(
topBarShadowColor: _darkThemeShadowColor,
modalBarrierColor: Colors.white12,
sabGradientColor: _darkSabGradientColor,
dialogShape: BeveledRectangleBorder(),
bottomSheetShape: BeveledRectangleBorder(),
mainContentScrollPhysics: ClampingScrollPhysics(),
),
],
),
home: Scaffold(
body: Builder(
builder: (context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Light Theme'),
Padding(
padding: const EdgeInsets.all(_pagePadding),
child: Switch(
value: !_isLightTheme,
onChanged: (_) =>
setState(() => _isLightTheme = !_isLightTheme),
),
),
const Text('Dark Theme'),
],
),
ElevatedButton(
onPressed: () {
WoltModalSheet.show<void>(
pageIndexNotifier: pageIndexNotifier,
context: context,
pageListBuilder: (modalSheetContext) {
final textTheme = Theme.of(context).textTheme;
return [
page1(modalSheetContext, textTheme),
page2(modalSheetContext, textTheme),
];
},
modalTypeBuilder: (context) {
final size = MediaQuery.of(context).size.width;
if (size < _pageBreakpoint) {
return WoltModalType.bottomSheet;
} else {
return WoltModalType.dialog;
}
},
onModalDismissedWithBarrierTap: () {
debugPrint('Closed modal sheet with barrier tap');
Navigator.of(context).pop();
pageIndexNotifier.value = 0;
},
maxDialogWidth: 560,
minDialogWidth: 400,
minPageHeight: 0.0,
maxPageHeight: 0.9,
);
},
child: const SizedBox(
height: _buttonHeight,
width: _buttonWidth,
child: Center(child: Text('Show Modal Sheet')),
),
),
],
);
},
),
),
);
}
}
class ColorTile extends StatelessWidget {
final Color color;
const ColorTile({super.key, required this.color});
@override
Widget build(BuildContext context) {
return Container(
color: color,
height: 600,
child: Center(
child: Text(
color.toString(),
style: TextStyle(
color: color.computeLuminance() > 0.5 ? Colors.black : Colors.white,
fontWeight: FontWeight.bold,
),
),
),
);
}
}
List<Color> get allMaterialColors {
List<Color> allMaterialColorsWithShades = [];
for (MaterialColor color in Colors.primaries) {
allMaterialColorsWithShades.add(color.shade100);
allMaterialColorsWithShades.add(color.shade200);
allMaterialColorsWithShades.add(color.shade300);
allMaterialColorsWithShades.add(color.shade400);
allMaterialColorsWithShades.add(color.shade500);
allMaterialColorsWithShades.add(color.shade600);
allMaterialColorsWithShades.add(color.shade700);
allMaterialColorsWithShades.add(color.shade800);
allMaterialColorsWithShades.add(color.shade900);
}
return allMaterialColorsWithShades;
}
class Shifter extends SingleChildRenderObjectWidget {
/// Creates an instance of [Shifter].
const Shifter({
Key? key,
required Widget child,
}) : super(key: key, child: child);
@override
RenderObject createRenderObject(BuildContext context) {
return _SpinnerRenderSliver();
}
}
class _SpinnerRenderSliver extends RenderSliver
with RenderObjectWithChildMixin<RenderBox> {
final LayerHandle<TransformLayer> _transformLayer =
LayerHandle<TransformLayer>();
Matrix4? _paintTransform;
@override
void setupParentData(RenderObject child) {
if (child.parentData is! SliverPhysicalParentData) {
child.parentData = SliverPhysicalParentData();
}
}
@override
void performLayout() {
_paintTransform = null;
final constraints = this.constraints;
child!.layout(constraints.asBoxConstraints(), parentUsesSize: true);
final double childExtent;
final childSizeWidth = child!.size.width;
switch (constraints.axis) {
case Axis.horizontal:
childExtent = childSizeWidth;
break;
case Axis.vertical:
childExtent = child!.size.height;
break;
}
final paintedChildSize = calculatePaintOffset(
constraints,
from: 0.0,
to: childExtent,
);
final cacheExtent = calculateCacheOffset(
constraints,
from: 0.0,
to: childExtent,
);
final scrollOffset = constraints.scrollOffset;
if (scrollOffset > 0 && paintedChildSize > 0) {
final shift = (1 - paintedChildSize / childExtent) * childSizeWidth;
_paintTransform = Matrix4.identity()..translate(shift, 0.0);
}
assert(paintedChildSize.isFinite);
assert(paintedChildSize >= 0.0);
geometry = SliverGeometry(
scrollExtent: childExtent,
paintExtent: paintedChildSize,
cacheExtent: cacheExtent,
maxPaintExtent: childExtent,
hitTestExtent: paintedChildSize,
hasVisualOverflow: childExtent > constraints.remainingPaintExtent ||
constraints.scrollOffset > 0.0,
);
_setChildParentData(child!, constraints, geometry!);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null && geometry!.visible) {
_transformLayer.layer = context.pushTransform(
needsCompositing,
offset,
_paintTransform ?? Matrix4.identity(),
_paintChild,
oldLayer: _transformLayer.layer,
);
} else {
_transformLayer.layer = null;
}
}
@override
void applyPaintTransform(covariant RenderObject child, Matrix4 transform) {
if (_paintTransform != null) {
transform.multiply(_paintTransform!);
}
final childParentData = child.parentData! as SliverPhysicalParentData;
// for make it more readable
// ignore: cascade_invocations
childParentData.applyPaintTransform(transform);
}
@override
void dispose() {
_transformLayer.layer = null;
super.dispose();
}
void _paintChild(PaintingContext context, Offset offset) {
final childParentData = child!.parentData! as SliverPhysicalParentData;
context.paintChild(child!, offset + childParentData.paintOffset);
}
void _setChildParentData(
RenderObject child,
SliverConstraints constraints,
SliverGeometry geometry,
) {
final childParentData = child.parentData! as SliverPhysicalParentData;
var dx = 0.0;
var dy = 0.0;
switch (applyGrowthDirectionToAxisDirection(
constraints.axisDirection,
constraints.growthDirection,
)) {
case AxisDirection.up:
dy = -(geometry.scrollExtent -
(geometry.paintExtent + constraints.scrollOffset));
break;
case AxisDirection.right:
dx = -constraints.scrollOffset;
break;
case AxisDirection.down:
dy = -constraints.scrollOffset;
break;
case AxisDirection.left:
dx = -(geometry.scrollExtent -
(geometry.paintExtent + constraints.scrollOffset));
break;
}
childParentData.paintOffset = Offset(dx, dy);
}
}