diff --git a/lib/src/content/components/main_content/wolt_modal_sheet_top_bar_flow.dart b/lib/src/content/components/main_content/wolt_modal_sheet_top_bar_flow.dart index 662faeef..7a42d8d5 100644 --- a/lib/src/content/components/main_content/wolt_modal_sheet_top_bar_flow.dart +++ b/lib/src/content/components/main_content/wolt_modal_sheet_top_bar_flow.dart @@ -13,7 +13,7 @@ import 'package:wolt_modal_sheet/wolt_modal_sheet.dart'; /// scrolls through the content. Utilizing the [Flow] widget, it listens to the current scroll /// position and soft keyboard closing events, then, performs transformations to achieve the /// desired effects on the top bar, such as fading in/out and translating vertically. -class WoltModalSheetTopBarFlow extends StatelessWidget { +class WoltModalSheetTopBarFlow extends StatefulWidget { final ScrollController scrollController; final GlobalKey titleKey; final SliverWoltModalSheetPage page; @@ -29,16 +29,70 @@ class WoltModalSheetTopBarFlow extends StatelessWidget { Key? key, }) : super(key: key); + @override + _WoltModalSheetTopBarFlowState createState() => + _WoltModalSheetTopBarFlowState(); +} + +class _WoltModalSheetTopBarFlowState extends State { + double _currentScrollOffset = 0.0; + bool _scrollUpdateScheduled = false; + + @override + void initState() { + super.initState(); + // Initialize with current offset if possible + if (widget.scrollController.hasClients) { + _currentScrollOffset = widget.scrollController.position.pixels; + } + widget.scrollController.addListener(_scrollListener); + } + + @override + void didUpdateWidget(covariant WoltModalSheetTopBarFlow oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.scrollController != oldWidget.scrollController) { + oldWidget.scrollController.removeListener(_scrollListener); + // Initialize with new controller's offset if possible + if (widget.scrollController.hasClients) { + _currentScrollOffset = widget.scrollController.position.pixels; + } else { + _currentScrollOffset = 0.0; // Reset if no clients + } + widget.scrollController.addListener(_scrollListener); + } + // Update offset immediately if controller has clients and offset differs + if (widget.scrollController.hasClients && + _currentScrollOffset != widget.scrollController.position.pixels) { + _scrollListener(); + } + } + + void _scrollListener() { + // Throttle updates to post-frame to avoid layout dirties during hit tests + if (!widget.scrollController.hasClients) return; + final newOffset = widget.scrollController.position.pixels; + if (_currentScrollOffset == newOffset) return; + _currentScrollOffset = newOffset; + if (!_scrollUpdateScheduled) { + _scrollUpdateScheduled = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) setState(() {}); + _scrollUpdateScheduled = false; + }); + } + } + @override Widget build(BuildContext context) { final themeData = Theme.of(context).extension(); final defaultThemeData = WoltModalSheetDefaultThemeData(context); - final topBarHeight = page.navBarHeight ?? + final topBarHeight = widget.page.navBarHeight ?? themeData?.navBarHeight ?? defaultThemeData.navBarHeight; - final heroImageHeight = page.heroImage == null + final heroImageHeight = widget.page.heroImage == null ? 0.0 - : (page.heroImageHeight ?? + : (widget.page.heroImageHeight ?? themeData?.heroImageHeight ?? defaultThemeData.heroImageHeight); @@ -46,12 +100,13 @@ class WoltModalSheetTopBarFlow extends StatelessWidget { delegate: _TopBarFlowDelegate( topBarHeight: topBarHeight, heroImageHeight: heroImageHeight, - scrollController: scrollController, - titleKey: titleKey, - softKeyboardClosedListenable: softKeyboardClosedListenable, - scrollAnimationStyle: scrollAnimationStyle, + scrollController: widget.scrollController, + titleKey: widget.titleKey, + softKeyboardClosedListenable: widget.softKeyboardClosedListenable, + scrollAnimationStyle: widget.scrollAnimationStyle, + currentScrollOffset: _currentScrollOffset, ), - children: [WoltModalSheetTopBar(page: page)], + children: [WoltModalSheetTopBar(page: widget.page)], ); } } @@ -63,6 +118,7 @@ class _TopBarFlowDelegate extends FlowDelegate { final GlobalKey titleKey; final ValueListenable softKeyboardClosedListenable; final WoltModalSheetScrollAnimationStyle scrollAnimationStyle; + final double currentScrollOffset; _TopBarFlowDelegate({ required this.topBarHeight, @@ -71,6 +127,7 @@ class _TopBarFlowDelegate extends FlowDelegate { required this.titleKey, required this.scrollAnimationStyle, required this.softKeyboardClosedListenable, + required this.currentScrollOffset, }) : super( repaint: Listenable.merge([ scrollController, @@ -78,8 +135,6 @@ class _TopBarFlowDelegate extends FlowDelegate { ]), ); - double get currentScrollOffset => scrollController.position.pixels; - @override void paintChildren(FlowPaintingContext context) { final pageTitleHeight = titleKey.currentContext!.size!.height; diff --git a/lib/src/content/components/paginating_group/main_content_animated_builder.dart b/lib/src/content/components/paginating_group/main_content_animated_builder.dart index 30057f13..dd0013da 100644 --- a/lib/src/content/components/paginating_group/main_content_animated_builder.dart +++ b/lib/src/content/components/paginating_group/main_content_animated_builder.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:wolt_modal_sheet/src/content/components/paginating_group/wolt_modal_sheet_page_transition_state.dart'; import 'package:wolt_modal_sheet/wolt_modal_sheet.dart'; +import 'package:wolt_modal_sheet/src/widgets/pixel_slide_transition.dart'; class MainContentAnimatedBuilder extends StatefulWidget { final AnimationController controller; @@ -70,7 +71,7 @@ class _MainContentAnimatedBuilderState opacity: pageTransitionState .mainContentOpacity(controller, widget.paginationAnimationStyle) .value, - child: SlideTransition( + child: PixelSlideTransition( position: pageTransitionState.mainContentSlidePosition( controller, widget.paginationAnimationStyle, diff --git a/lib/src/modal_type/wolt_bottom_sheet_type.dart b/lib/src/modal_type/wolt_bottom_sheet_type.dart index ba448edd..1117760d 100644 --- a/lib/src/modal_type/wolt_bottom_sheet_type.dart +++ b/lib/src/modal_type/wolt_bottom_sheet_type.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:wolt_modal_sheet/src/widgets/pixel_slide_transition.dart'; import 'package:wolt_modal_sheet/wolt_modal_sheet.dart'; /// A customizable bottom sheet modal that extends [WoltModalType]. @@ -124,7 +125,7 @@ class WoltBottomSheetType extends WoltModalType { /// [secondaryAnimation] coordinates with the transitions of other routes. /// [child] is the content widget to be animated. /// - /// Returns a `SlideTransition` widget that manages the modal's entrance animation. + /// Returns a `PixelSlideTransition` widget that manages the modal's entrance animation. @override Widget buildTransitions( BuildContext context, @@ -151,7 +152,8 @@ class WoltBottomSheetType extends WoltModalType { ), ); - return SlideTransition(position: positionAnimation, child: child); + // Use PixelSlideTransition to avoid RenderFractionalTranslation layout asserts + return PixelSlideTransition(position: positionAnimation, child: child); } /// Provides a way to create a new `WoltBottomSheetType` instance with modified properties. diff --git a/lib/src/modal_type/wolt_dialog_type.dart b/lib/src/modal_type/wolt_dialog_type.dart index 0a7b8c62..d23322e2 100644 --- a/lib/src/modal_type/wolt_dialog_type.dart +++ b/lib/src/modal_type/wolt_dialog_type.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:wolt_modal_sheet/src/utils/wolt_breakpoints.dart'; import 'package:wolt_modal_sheet/wolt_modal_sheet.dart'; +import 'package:wolt_modal_sheet/src/widgets/pixel_slide_transition.dart'; /// A customizable dialog modal that extends [WoltModalType]. class WoltDialogType extends WoltModalType { @@ -150,7 +151,7 @@ class WoltDialogType extends WoltModalType { return FadeTransition( opacity: alphaAnimation, - child: SlideTransition(position: positionAnimation, child: child), + child: PixelSlideTransition(position: positionAnimation, child: child), ); } diff --git a/lib/src/modal_type/wolt_side_sheet_type.dart b/lib/src/modal_type/wolt_side_sheet_type.dart index 88be9d05..8dfe6bf4 100644 --- a/lib/src/modal_type/wolt_side_sheet_type.dart +++ b/lib/src/modal_type/wolt_side_sheet_type.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:wolt_modal_sheet/src/utils/wolt_breakpoints.dart'; import 'package:wolt_modal_sheet/wolt_modal_sheet.dart'; +import 'package:wolt_modal_sheet/src/widgets/pixel_slide_transition.dart'; /// A customizable side sheet modal that extends [WoltModalType]. class WoltSideSheetType extends WoltModalType { @@ -168,7 +169,7 @@ class WoltSideSheetType extends WoltModalType { return FadeTransition( opacity: alphaAnimation, - child: SlideTransition(position: positionAnimation, child: child), + child: PixelSlideTransition(position: positionAnimation, child: child), ); } diff --git a/lib/src/widgets/pixel_slide_transition.dart b/lib/src/widgets/pixel_slide_transition.dart new file mode 100644 index 00000000..b760632f --- /dev/null +++ b/lib/src/widgets/pixel_slide_transition.dart @@ -0,0 +1,40 @@ +import 'package:flutter/widgets.dart'; + +/// A slide transition that uses pixel-based translation (Transform.translate) +/// instead of FractionalTranslation, avoiding debugNeedsLayout asserts. +class PixelSlideTransition extends StatelessWidget { + /// An offset animation where values represent fractional (0.0 to 1.0) positions. + final Animation position; + + /// The widget child to be transformed. + final Widget child; + + const PixelSlideTransition({ + required this.position, + required this.child, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + return AnimatedBuilder( + animation: position, + builder: (context, child) { + final offset = position.value; + // Convert fractional offsets to pixel values + final dx = offset.dx * constraints.maxWidth; + final dy = offset.dy * constraints.maxHeight; + return Transform.translate( + offset: Offset(dx, dy), + transformHitTests: false, + child: child, + ); + }, + child: child, + ); + }, + ); + } +} diff --git a/lib/src/wolt_modal_sheet_route.dart b/lib/src/wolt_modal_sheet_route.dart index 2babc4cf..4d0f11aa 100644 --- a/lib/src/wolt_modal_sheet_route.dart +++ b/lib/src/wolt_modal_sheet_route.dart @@ -113,12 +113,22 @@ class WoltModalSheetRoute extends PageRoute { Animation secondaryAnimation, Widget child, ) { - final modalType = _determineCurrentModalType(context); - return modalType.buildTransitions( - context, - animation, - secondaryAnimation, - child, + // Prevent pointer events (e.g., mouse scroll) during entrance/exit animations + return AnimatedBuilder( + animation: animation, + child: child, + builder: (context, child) { + final modalType = _determineCurrentModalType(context); + final transition = modalType.buildTransitions( + context, + animation, + secondaryAnimation, + child!, + ); + final isAnimating = animation.status != AnimationStatus.completed && + animation.status != AnimationStatus.dismissed; + return AbsorbPointer(absorbing: isAnimating, child: transition); + }, ); }