From 7ed6bbf9a989168ca63fe2610be151b8eb8ed627 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Sun, 1 Feb 2026 10:50:11 +0100 Subject: [PATCH] improve initial item scroll for list/grid --- .../widgets/tab_view/tab_grid_view.dart | 50 ++++++++++------- .../widgets/tab_view/tab_list_view.dart | 54 ++++++++++++------- 2 files changed, 66 insertions(+), 38 deletions(-) diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_grid_view.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_grid_view.dart index 20f7a193..c0c9c90a 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_grid_view.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_grid_view.dart @@ -187,37 +187,49 @@ class _TabGridView extends HookConsumerWidget { [screenWidth, crossAxisCount], ); - final lastScroll = useRef(null); + final didInitialScroll = useRef(false); useEffect(() { + if (didInitialScroll.value) return null; + WidgetsBinding.instance.addPostFrameCallback((_) { - if (scrollController.hasClients) { - if (lastScroll.value != activeTab) { - final index = filteredTabEntities.value.indexWhere( - (entity) => entity.tabId == activeTab, - ); + if (scrollController.hasClients && activeTab != null) { + final index = filteredTabEntities.value.indexWhere( + (entity) => entity.tabId == activeTab, + ); - if (index > -1) { - final offset = (index ~/ 2) * itemSize.height; + if (index > -1) { + final row = index ~/ crossAxisCount; + final viewportStart = scrollController.offset; + final viewportEnd = + viewportStart + scrollController.position.viewportDimension; + final tabStart = row * itemSize.height; + final tabEnd = tabStart + itemSize.height; - if (offset != scrollController.offset) { - lastScroll.value = activeTab; + final isVisible = + tabStart >= viewportStart && tabEnd <= viewportEnd; - unawaited( - scrollController.animateTo( - offset, - duration: const Duration(milliseconds: 200), - curve: Curves.easeInOut, - ), - ); - } + if (!isVisible) { + unawaited( + scrollController.animateTo( + tabStart, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + ), + ); + + didInitialScroll.value = true; + } else { + didInitialScroll.value = true; } + } else { + didInitialScroll.value = true; } } }); return null; - }, [filteredTabEntities, activeTab]); + }, [activeTab]); return Padding( padding: const EdgeInsets.symmetric(horizontal: 4.0), diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_list_view.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_list_view.dart index 8c33c976..2340646f 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_list_view.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_list_view.dart @@ -175,37 +175,53 @@ class _TabListView extends HookConsumerWidget { final activeTab = ref.watch(selectedTabProvider); - final lastScroll = useRef(null); + final didInitialScroll = useRef(false); useEffect(() { + if (didInitialScroll.value) return null; + WidgetsBinding.instance.addPostFrameCallback((_) { - if (scrollController.hasClients) { - if (lastScroll.value != activeTab) { - final index = filteredTabEntities.value.indexWhere( - (entity) => entity.tabId == activeTab, - ); + if (scrollController.hasClients && activeTab != null) { + final index = filteredTabEntities.value.indexWhere( + (entity) => entity.tabId == activeTab, + ); - if (index > -1) { - final offset = index * _itemHeight; + if (index > -1) { + final viewportStart = scrollController.offset; + final viewportEnd = + viewportStart + scrollController.position.viewportDimension; + final tabStart = index * _itemHeight; + final tabEnd = tabStart + _itemHeight; - if (offset != scrollController.offset) { - lastScroll.value = activeTab; + final isVisible = + tabStart >= viewportStart && tabEnd <= viewportEnd; - unawaited( - scrollController.animateTo( - offset, - duration: const Duration(milliseconds: 200), - curve: Curves.easeInOut, - ), - ); - } + if (!isVisible) { + final targetOffset = (tabStart - _itemHeight).clamp( + 0.0, + scrollController.position.maxScrollExtent, + ); + + unawaited( + scrollController.animateTo( + targetOffset, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + ), + ); + + didInitialScroll.value = true; + } else { + didInitialScroll.value = true; } + } else { + didInitialScroll.value = true; } } }); return null; - }, [filteredTabEntities, activeTab]); + }, [activeTab]); return Padding( padding: const EdgeInsets.symmetric(horizontal: 4.0),