improved support for disable animations

This commit is contained in:
Fabian Freund
2026-03-12 03:37:59 +01:00
parent e001701b07
commit 57a8b2f013
14 changed files with 224 additions and 110 deletions
@@ -683,6 +683,9 @@ class BrowserScreen extends HookConsumerWidget {
// Layer 5: Page load progress indicator (animates with toolbar visibility) // Layer 5: Page load progress indicator (animates with toolbar visibility)
Consumer( Consumer(
builder: (context, ref, child) { builder: (context, ref, child) {
final disableAnimations = MediaQuery.disableAnimationsOf(
context,
);
final toolbarState = ref.watch( final toolbarState = ref.watch(
toolbarVisibilityControllerProvider(selectedTabId), toolbarVisibilityControllerProvider(selectedTabId),
); );
@@ -691,7 +694,9 @@ class BrowserScreen extends HookConsumerWidget {
(!tabInFullScreen && (!tabInFullScreen &&
toolbarState == ToolbarVisibility.visible); toolbarState == ToolbarVisibility.visible);
return AnimatedPositioned( return AnimatedPositioned(
duration: _AnimatedToolbar._kAnimationDuration, duration: disableAnimations
? Duration.zero
: _AnimatedToolbar._kAnimationDuration,
curve: Curves.easeInOutQuart, curve: Curves.easeInOutQuart,
left: 0, left: 0,
right: 0, right: 0,
@@ -730,6 +735,9 @@ class BrowserScreen extends HookConsumerWidget {
// Layer 6: Find in Page widget (above toolbar or keyboard, whichever is higher) // Layer 6: Find in Page widget (above toolbar or keyboard, whichever is higher)
Consumer( Consumer(
builder: (context, ref, child) { builder: (context, ref, child) {
final disableAnimations = MediaQuery.disableAnimationsOf(
context,
);
final toolbarState = ref.watch( final toolbarState = ref.watch(
toolbarVisibilityControllerProvider(selectedTabId), toolbarVisibilityControllerProvider(selectedTabId),
); );
@@ -738,7 +746,9 @@ class BrowserScreen extends HookConsumerWidget {
(!tabInFullScreen && (!tabInFullScreen &&
toolbarState == ToolbarVisibility.visible); toolbarState == ToolbarVisibility.visible);
return AnimatedPositioned( return AnimatedPositioned(
duration: _AnimatedToolbar._kAnimationDuration, duration: disableAnimations
? Duration.zero
: _AnimatedToolbar._kAnimationDuration,
curve: Curves.easeInOutQuart, curve: Curves.easeInOutQuart,
left: 0, left: 0,
right: 0, right: 0,
@@ -1111,17 +1121,22 @@ class _SiteSettingsSheet extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final disableAnimations = MediaQuery.disableAnimationsOf(context);
final draggableScrollableController = useDraggableScrollableController(); final draggableScrollableController = useDraggableScrollableController();
void handleClearSiteDataExpansion(bool isExpanded) { void handleClearSiteDataExpansion(bool isExpanded) {
if (isExpanded) { if (isExpanded) {
unawaited( if (disableAnimations) {
draggableScrollableController.animateTo( draggableScrollableController.jumpTo(1.0);
1.0, } else {
duration: const Duration(milliseconds: 300), unawaited(
curve: Curves.decelerate, draggableScrollableController.animateTo(
), 1.0,
); duration: const Duration(milliseconds: 300),
curve: Curves.decelerate,
),
);
}
} }
} }
@@ -35,6 +35,7 @@ class TabViewScreen extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final disableAnimations = MediaQuery.disableAnimationsOf(context);
final tabsViewMode = ref.watch(tabsViewModeControllerProvider); final tabsViewMode = ref.watch(tabsViewModeControllerProvider);
final tabsReorderable = ref.watch(tabsReorderableControllerProvider); final tabsReorderable = ref.watch(tabsReorderableControllerProvider);
@@ -51,7 +52,10 @@ class TabViewScreen extends HookConsumerWidget {
final scrollController = useScrollController(keys: [tabsReorderable]); final scrollController = useScrollController(keys: [tabsReorderable]);
// Track FAB visibility based on scroll direction // Track FAB visibility based on scroll direction
final isFabVisible = useScrollVisibility(scrollController); final isFabVisible = useScrollVisibility(
scrollController,
disableAnimations: disableAnimations,
);
return Dialog.fullscreen( return Dialog.fullscreen(
child: Scaffold( child: Scaffold(
@@ -87,11 +91,15 @@ class TabViewScreen extends HookConsumerWidget {
floatingActionButton: isSyncedScope floatingActionButton: isSyncedScope
? null ? null
: AnimatedSlide( : AnimatedSlide(
duration: const Duration(milliseconds: 200), duration: disableAnimations
? Duration.zero
: const Duration(milliseconds: 200),
offset: isFabVisible.value ? Offset.zero : const Offset(0, 2), offset: isFabVisible.value ? Offset.zero : const Offset(0, 2),
curve: Curves.easeInOut, curve: Curves.easeInOut,
child: AnimatedOpacity( child: AnimatedOpacity(
duration: const Duration(milliseconds: 200), duration: disableAnimations
? Duration.zero
: const Duration(milliseconds: 200),
opacity: isFabVisible.value ? 1.0 : 0.0, opacity: isFabVisible.value ? 1.0 : 0.0,
child: FloatingActionButton( child: FloatingActionButton(
onPressed: () async { onPressed: () async {
@@ -462,6 +462,7 @@ class QuickTabSwitcher extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final disableAnimations = MediaQuery.disableAnimationsOf(context);
final showIsolatedTabUi = ref.watch( final showIsolatedTabUi = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.showIsolatedTabUi), generalSettingsWithDefaultsProvider.select((s) => s.showIsolatedTabUi),
); );
@@ -523,11 +524,16 @@ class QuickTabSwitcher extends HookConsumerWidget {
showTitles: showTitles, showTitles: showTitles,
showIsolatedTabUi: showIsolatedTabUi, showIsolatedTabUi: showIsolatedTabUi,
onSelected: (item) async { onSelected: (item) async {
final animation = chipScrollController.animateTo( Future<void>? animation;
0, if (disableAnimations) {
duration: const Duration(milliseconds: 200), chipScrollController.jumpTo(0);
curve: Curves.easeOutBack, } else {
); animation = chipScrollController.animateTo(
0,
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutBack,
);
}
if (item.isHistory) { if (item.isHistory) {
await ref await ref
.read(tabRepositoryProvider.notifier) .read(tabRepositoryProvider.notifier)
@@ -47,6 +47,7 @@ class DraggableFab extends HookConsumerWidget {
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final screenSize = mediaQuery.size; final screenSize = mediaQuery.size;
final padding = mediaQuery.padding; final padding = mediaQuery.padding;
final disableAnimations = mediaQuery.disableAnimations;
final isDragging = useState(false); final isDragging = useState(false);
final customOffset = useState<Offset?>(null); final customOffset = useState<Offset?>(null);
@@ -98,7 +99,9 @@ class DraggableFab extends HookConsumerWidget {
dragStartPosition.value = null; dragStartPosition.value = null;
}, },
child: AnimatedScale( child: AnimatedScale(
duration: const Duration(milliseconds: 150), duration: disableAnimations
? Duration.zero
: const Duration(milliseconds: 150),
scale: isDragging.value ? 1.1 : 1.0, scale: isDragging.value ? 1.1 : 1.0,
child: child, child: child,
), ),
@@ -142,6 +142,7 @@ class _TabGridView extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.of(context).size.width;
final disableAnimations = MediaQuery.disableAnimationsOf(context);
final canManualReorder = ref.watch(canManualTabReorderProvider); final canManualReorder = ref.watch(canManualTabReorderProvider);
final reorderEnabled = tabsReorderable && canManualReorder; final reorderEnabled = tabsReorderable && canManualReorder;
@@ -220,13 +221,17 @@ class _TabGridView extends HookConsumerWidget {
tabStart >= viewportStart && tabEnd <= viewportEnd; tabStart >= viewportStart && tabEnd <= viewportEnd;
if (!isVisible) { if (!isVisible) {
unawaited( if (disableAnimations) {
scrollController.animateTo( scrollController.jumpTo(tabStart);
tabStart, } else {
duration: const Duration(milliseconds: 200), unawaited(
curve: Curves.easeInOut, scrollController.animateTo(
), tabStart,
); duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
),
);
}
didInitialScroll.value = true; didInitialScroll.value = true;
} else { } else {
@@ -470,21 +475,25 @@ class ViewTabGridWidget extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final disableAnimations = MediaQuery.disableAnimationsOf(context);
final isFabVisible = useState(true); final isFabVisible = useState(true);
final lastSheetSize = useRef(0.0); final lastSheetSize = useRef(0.0);
final isInitialized = useRef(false); final isInitialized = useRef(false);
// Delay initialization to ignore initial animations // Delay initialization to ignore initial animations
useEffect(() { useEffect(() {
final timer = Timer(const Duration(milliseconds: 500), () { final timer = Timer(
isInitialized.value = true; disableAnimations ? Duration.zero : const Duration(milliseconds: 500),
// Initialize lastSheetSize with current size () {
if (draggableScrollableController?.isAttached == true) { isInitialized.value = true;
lastSheetSize.value = draggableScrollableController!.size; // Initialize lastSheetSize with current size
} if (draggableScrollableController?.isAttached == true) {
}); lastSheetSize.value = draggableScrollableController!.size;
}
},
);
return timer.cancel; return timer.cancel;
}, []); }, [disableAnimations]);
// Listen to DraggableScrollableController for sheet size changes // Listen to DraggableScrollableController for sheet size changes
useEffect(() { useEffect(() {
@@ -571,11 +580,15 @@ class ViewTabGridWidget extends HookConsumerWidget {
), ),
if (showNewTabFab) if (showNewTabFab)
AnimatedSlide( AnimatedSlide(
duration: const Duration(milliseconds: 200), duration: disableAnimations
? Duration.zero
: const Duration(milliseconds: 200),
offset: isFabVisible.value ? Offset.zero : const Offset(0, 2), offset: isFabVisible.value ? Offset.zero : const Offset(0, 2),
curve: Curves.easeInOut, curve: Curves.easeInOut,
child: AnimatedOpacity( child: AnimatedOpacity(
duration: const Duration(milliseconds: 200), duration: disableAnimations
? Duration.zero
: const Duration(milliseconds: 200),
opacity: isFabVisible.value ? 1.0 : 0.0, opacity: isFabVisible.value ? 1.0 : 0.0,
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
@@ -204,6 +204,7 @@ class _TabListView extends HookConsumerWidget {
} }
Widget _buildLocalTabsView(BuildContext context, WidgetRef ref) { Widget _buildLocalTabsView(BuildContext context, WidgetRef ref) {
final disableAnimations = MediaQuery.disableAnimationsOf(context);
final containerId = ref.watch(selectedContainerProvider); final containerId = ref.watch(selectedContainerProvider);
final canManualReorder = ref.watch(canManualTabReorderProvider); final canManualReorder = ref.watch(canManualTabReorderProvider);
final reorderEnabled = tabsReorderable && canManualReorder; final reorderEnabled = tabsReorderable && canManualReorder;
@@ -263,13 +264,17 @@ class _TabListView extends HookConsumerWidget {
scrollController.position.maxScrollExtent, scrollController.position.maxScrollExtent,
); );
unawaited( if (disableAnimations) {
scrollController.animateTo( scrollController.jumpTo(targetOffset);
targetOffset, } else {
duration: const Duration(milliseconds: 200), unawaited(
curve: Curves.easeInOut, scrollController.animateTo(
), targetOffset,
); duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
),
);
}
didInitialScroll.value = true; didInitialScroll.value = true;
} else { } else {
@@ -465,6 +470,7 @@ class ViewTabListWidget extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final disableAnimations = MediaQuery.disableAnimationsOf(context);
final isSyncedScope = ref.watch( final isSyncedScope = ref.watch(
effectiveTabsTrayScopeProvider.select( effectiveTabsTrayScopeProvider.select(
(scope) => scope == TabsTrayScope.synced, (scope) => scope == TabsTrayScope.synced,
@@ -477,15 +483,18 @@ class ViewTabListWidget extends HookConsumerWidget {
// Delay initialization to ignore initial animations // Delay initialization to ignore initial animations
useEffect(() { useEffect(() {
final timer = Timer(const Duration(milliseconds: 500), () { final timer = Timer(
isInitialized.value = true; disableAnimations ? Duration.zero : const Duration(milliseconds: 500),
// Initialize lastSheetSize with current size () {
if (draggableScrollableController?.isAttached == true) { isInitialized.value = true;
lastSheetSize.value = draggableScrollableController!.size; // Initialize lastSheetSize with current size
} if (draggableScrollableController?.isAttached == true) {
}); lastSheetSize.value = draggableScrollableController!.size;
}
},
);
return timer.cancel; return timer.cancel;
}, []); }, [disableAnimations]);
// Listen to DraggableScrollableController for sheet size changes // Listen to DraggableScrollableController for sheet size changes
useEffect(() { useEffect(() {
@@ -572,11 +581,15 @@ class ViewTabListWidget extends HookConsumerWidget {
), ),
if (showNewTabFab && !isSyncedScope) if (showNewTabFab && !isSyncedScope)
AnimatedSlide( AnimatedSlide(
duration: const Duration(milliseconds: 200), duration: disableAnimations
? Duration.zero
: const Duration(milliseconds: 200),
offset: isFabVisible.value ? Offset.zero : const Offset(0, 2), offset: isFabVisible.value ? Offset.zero : const Offset(0, 2),
curve: Curves.easeInOut, curve: Curves.easeInOut,
child: AnimatedOpacity( child: AnimatedOpacity(
duration: const Duration(milliseconds: 200), duration: disableAnimations
? Duration.zero
: const Duration(milliseconds: 200),
opacity: isFabVisible.value ? 1.0 : 0.0, opacity: isFabVisible.value ? 1.0 : 0.0,
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
@@ -211,6 +211,9 @@ class ViewTabTreesWidget extends HookConsumerWidget {
child: HookConsumer( child: HookConsumer(
builder: (context, ref, child) { builder: (context, ref, child) {
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.of(context).size.width;
final disableAnimations = MediaQuery.disableAnimationsOf(
context,
);
final containerId = ref.watch(selectedContainerProvider); final containerId = ref.watch(selectedContainerProvider);
@@ -265,13 +268,17 @@ class ViewTabTreesWidget extends HookConsumerWidget {
final offset = (index ~/ 2) * itemSize.height; final offset = (index ~/ 2) * itemSize.height;
if (offset != scrollController.offset) { if (offset != scrollController.offset) {
unawaited( if (disableAnimations) {
scrollController.animateTo( scrollController.jumpTo(offset);
offset, } else {
duration: const Duration(milliseconds: 200), unawaited(
curve: Curves.easeInOut, scrollController.animateTo(
), offset,
); duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
),
);
}
} }
} }
@@ -126,6 +126,7 @@ class _Segment extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final disableAnimations = MediaQuery.disableAnimationsOf(context);
final theme = Theme.of(context); final theme = Theme.of(context);
final colorScheme = theme.colorScheme; final colorScheme = theme.colorScheme;
@@ -140,7 +141,9 @@ class _Segment extends StatelessWidget {
onTap: onTap, onTap: onTap,
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 300), duration: disableAnimations
? Duration.zero
: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic, curve: Curves.easeOutCubic,
color: bgColor, color: bgColor,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
@@ -149,7 +152,9 @@ class _Segment extends StatelessWidget {
children: [ children: [
Icon(icon, color: fgColor, size: 18), Icon(icon, color: fgColor, size: 18),
AnimatedSize( AnimatedSize(
duration: const Duration(milliseconds: 300), duration: disableAnimations
? Duration.zero
: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic, curve: Curves.easeOutCubic,
child: isSelected child: isSelected
? Padding( ? Padding(
@@ -54,6 +54,7 @@ class SearchModuleHeader extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final disableAnimations = MediaQuery.disableAnimationsOf(context);
final isCollapsed = displayState == SearchModuleDisplayState.collapsed; final isCollapsed = displayState == SearchModuleDisplayState.collapsed;
final isExpanded = displayState == SearchModuleDisplayState.expanded; final isExpanded = displayState == SearchModuleDisplayState.expanded;
final showTrailing = !isCollapsed && totalCount > previewLimit; final showTrailing = !isCollapsed && totalCount > previewLimit;
@@ -75,7 +76,9 @@ class SearchModuleHeader extends StatelessWidget {
const SizedBox(width: 8), const SizedBox(width: 8),
AnimatedRotation( AnimatedRotation(
turns: isCollapsed ? -0.25 : 0, turns: isCollapsed ? -0.25 : 0,
duration: const Duration(milliseconds: 200), duration: disableAnimations
? Duration.zero
: const Duration(milliseconds: 200),
child: Icon( child: Icon(
Icons.expand_more, Icons.expand_more,
size: 20, size: 20,
@@ -70,6 +70,7 @@ class ContainerEditScreen extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final disableAnimations = MediaQuery.disableAnimationsOf(context);
final selectedColor = useState(initialContainer.color); final selectedColor = useState(initialContainer.color);
final contextualIdentity = useState( final contextualIdentity = useState(
initialContainer.metadata.contextualIdentity, initialContainer.metadata.contextualIdentity,
@@ -136,7 +137,9 @@ class ContainerEditScreen extends HookConsumerWidget {
prefixIcon: Padding( prefixIcon: Padding(
padding: const EdgeInsets.all(10.0), padding: const EdgeInsets.all(10.0),
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 300), duration: disableAnimations
? Duration.zero
: const Duration(milliseconds: 300),
height: 24, height: 24,
width: 24, width: 24,
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -67,6 +67,7 @@ class _MaterialPickerState extends State<MaterialPicker> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final disableAnimations = MediaQuery.disableAnimationsOf(context);
final isPortrait = final isPortrait =
MediaQuery.of(context).orientation == Orientation.portrait || MediaQuery.of(context).orientation == Orientation.portrait ||
widget.portraitOnly; widget.portraitOnly;
@@ -143,7 +144,9 @@ class _MaterialPickerState extends State<MaterialPicker> {
: const EdgeInsets.fromLTRB(7, 0, 7, 0), : const EdgeInsets.fromLTRB(7, 0, 7, 0),
child: Align( child: Align(
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 300), duration: disableAnimations
? Duration.zero
: const Duration(milliseconds: 300),
width: 25, width: 25,
height: 25, height: 25,
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -235,7 +238,9 @@ class _MaterialPickerState extends State<MaterialPicker> {
child: Align( child: Align(
child: AnimatedContainer( child: AnimatedContainer(
curve: Curves.fastOutSlowIn, curve: Curves.fastOutSlowIn,
duration: const Duration(milliseconds: 500), duration: disableAnimations
? Duration.zero
: const Duration(milliseconds: 500),
width: isPortrait width: isPortrait
? (_currentShading == color ? 250 : 230) ? (_currentShading == color ? 250 : 230)
: (_currentShading == color ? 50 : 30), : (_currentShading == color ? 50 : 30),
@@ -308,9 +313,9 @@ class _MaterialPickerState extends State<MaterialPicker> {
], ],
) )
: AnimatedOpacity( : AnimatedOpacity(
duration: const Duration( duration: disableAnimations
milliseconds: 300, ? Duration.zero
), : const Duration(milliseconds: 300),
opacity: _currentShading == color opacity: _currentShading == color
? 1 ? 1
: 0, : 0,
@@ -47,6 +47,7 @@ class OnboardingScreen extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context); final theme = Theme.of(context);
final disableAnimations = MediaQuery.disableAnimationsOf(context);
final pageController = usePageController(); final pageController = usePageController();
@@ -95,10 +96,14 @@ class OnboardingScreen extends HookConsumerWidget {
visible: currentPage.value > 0, visible: currentPage.value > 0,
child: TextButton.icon( child: TextButton.icon(
onPressed: () async { onPressed: () async {
await pageController.previousPage( if (disableAnimations) {
duration: const Duration(milliseconds: 250), pageController.jumpToPage(currentPage.value - 1);
curve: Curves.easeInOut, } else {
); await pageController.previousPage(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
);
}
}, },
icon: const Icon(Icons.chevron_left), icon: const Icon(Icons.chevron_left),
label: const Text('Previous'), label: const Text('Previous'),
@@ -121,10 +126,14 @@ class OnboardingScreen extends HookConsumerWidget {
Expanded( Expanded(
child: TextButton.icon( child: TextButton.icon(
onPressed: () async { onPressed: () async {
await pageController.nextPage( if (disableAnimations) {
duration: const Duration(milliseconds: 250), pageController.jumpToPage(currentPage.value + 1);
curve: Curves.easeInOut, } else {
); await pageController.nextPage(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
);
}
}, },
iconAlignment: IconAlignment.end, iconAlignment: IconAlignment.end,
icon: const Icon(Icons.chevron_right), icon: const Icon(Icons.chevron_right),
@@ -44,43 +44,59 @@ ValueNotifier<bool> useScrollVisibility(
double hideThreshold = 5.0, double hideThreshold = 5.0,
double showThreshold = 5.0, double showThreshold = 5.0,
Duration initializationDelay = const Duration(milliseconds: 1000), Duration initializationDelay = const Duration(milliseconds: 1000),
bool disableAnimations = false,
}) { }) {
final isVisible = useState(true); final isVisible = useState(true);
final lastScrollOffset = useRef(0.0); final lastScrollOffset = useRef(0.0);
final isInitialized = useRef(false); final isInitialized = useRef(false);
useEffect(() { useEffect(
// Start timer to enable scroll listener after initialization delay () {
final timer = Timer(initializationDelay, () { // Start timer to enable scroll listener after initialization delay
isInitialized.value = true; final timer = Timer(
}); disableAnimations ? Duration.zero : initializationDelay,
() {
if (scrollController.hasClients) {
lastScrollOffset.value = scrollController.offset;
}
isInitialized.value = true;
},
);
void scrollListener() { void scrollListener() {
// Ignore scroll events until initialization is complete // Ignore scroll events until initialization is complete
if (!isInitialized.value) return; if (!isInitialized.value) return;
final currentOffset = scrollController.offset; final currentOffset = scrollController.offset;
final difference = currentOffset - lastScrollOffset.value; final difference = currentOffset - lastScrollOffset.value;
// Hide when scrolling down beyond threshold // Hide when scrolling down beyond threshold
if (difference > hideThreshold && isVisible.value) { if (difference > hideThreshold && isVisible.value) {
isVisible.value = false; isVisible.value = false;
} }
// Show when scrolling up beyond threshold or at top // Show when scrolling up beyond threshold or at top
else if ((difference < -showThreshold || currentOffset <= 0) && else if ((difference < -showThreshold || currentOffset <= 0) &&
!isVisible.value) { !isVisible.value) {
isVisible.value = true; isVisible.value = true;
}
lastScrollOffset.value = currentOffset;
} }
lastScrollOffset.value = currentOffset; scrollController.addListener(scrollListener);
} return () {
timer.cancel();
scrollController.addListener(scrollListener); scrollController.removeListener(scrollListener);
return () { };
timer.cancel(); },
scrollController.removeListener(scrollListener); [
}; scrollController,
}, [scrollController, hideThreshold, showThreshold, initializationDelay]); hideThreshold,
showThreshold,
initializationDelay,
disableAnimations,
],
);
return isVisible; return isVisible;
} }
+15 -7
View File
@@ -24,14 +24,19 @@ void useSyncPageWithTab(
TabController tabController, TabController tabController,
PageController pageController, { PageController pageController, {
void Function(int index)? onIndexChanged, void Function(int index)? onIndexChanged,
bool disableAnimations = false,
}) { }) {
useEffect(() { useEffect(() {
Future<void> syncPage() async { Future<void> syncPage() async {
await pageController.animateToPage( if (disableAnimations) {
tabController.index, pageController.jumpToPage(tabController.index);
curve: Curves.linear, } else {
duration: const Duration(milliseconds: 300), await pageController.animateToPage(
); tabController.index,
curve: Curves.linear,
duration: const Duration(milliseconds: 300),
);
}
onIndexChanged?.call(tabController.index); onIndexChanged?.call(tabController.index);
} }
@@ -39,7 +44,10 @@ void useSyncPageWithTab(
if (!tabController.indexIsChanging) { if (!tabController.indexIsChanging) {
final index = pageController.page!.round(); final index = pageController.page!.round();
tabController.animateTo(index); tabController.animateTo(
index,
duration: disableAnimations ? Duration.zero : kTabScrollDuration,
);
onIndexChanged?.call(index); onIndexChanged?.call(index);
} }
} }
@@ -51,5 +59,5 @@ void useSyncPageWithTab(
tabController.removeListener(syncPage); tabController.removeListener(syncPage);
pageController.removeListener(syncTab); pageController.removeListener(syncTab);
}; };
}, [tabController, pageController]); }, [tabController, pageController, disableAnimations]);
} }