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