better auto scroll in tab list/grid/tree and quick tab switcher
This commit is contained in:
+80
-2
@@ -22,6 +22,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
@@ -542,11 +543,79 @@ class QuickTabSwitcher extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
final chipScrollController = useScrollController();
|
||||
final activeItemKey = useRef(GlobalKey());
|
||||
final isUserScrolling = useRef(false);
|
||||
final userScrollTimer = useRef<Timer?>(null);
|
||||
|
||||
return QuickTabSwitcherView(
|
||||
useEffect(() {
|
||||
return userScrollTimer.value?.cancel;
|
||||
}, []);
|
||||
|
||||
useEffect(() {
|
||||
if (isUserScrolling.value) return null;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final context = activeItemKey.value.currentContext;
|
||||
if (context != null) {
|
||||
Scrollable.ensureVisible(
|
||||
context,
|
||||
alignment: 0.5,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
} else if (chipScrollController.hasClients) {
|
||||
final activeIndex = availableItems.indexWhere(
|
||||
(item) => item.id == selectedTabId,
|
||||
);
|
||||
if (activeIndex < 0) return;
|
||||
|
||||
final totalItems = availableItems.length;
|
||||
final maxExtent = chipScrollController.position.maxScrollExtent;
|
||||
if (totalItems > 0 && maxExtent > 0) {
|
||||
chipScrollController.jumpTo(
|
||||
(activeIndex / totalItems * maxExtent).clamp(0.0, maxExtent),
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final retryContext = activeItemKey.value.currentContext;
|
||||
if (retryContext != null) {
|
||||
Scrollable.ensureVisible(
|
||||
retryContext,
|
||||
alignment: 0.5,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
}, [selectedTabId]);
|
||||
|
||||
return NotificationListener<UserScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
if (notification.direction != ScrollDirection.idle) {
|
||||
isUserScrolling.value = true;
|
||||
userScrollTimer.value?.cancel();
|
||||
userScrollTimer.value = Timer(const Duration(milliseconds: 1500), () {
|
||||
isUserScrolling.value = false;
|
||||
});
|
||||
} else {
|
||||
userScrollTimer.value?.cancel();
|
||||
userScrollTimer.value = Timer(const Duration(milliseconds: 1500), () {
|
||||
isUserScrolling.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
child: QuickTabSwitcherView(
|
||||
availableItems: availableItems,
|
||||
activeItem: activeItem.isActive ? activeItem : null,
|
||||
scrollController: chipScrollController,
|
||||
activeItemKey: activeItemKey.value,
|
||||
showTitles: showTitles,
|
||||
showIsolatedTabUi: showIsolatedTabUi,
|
||||
onSelected: (item) async {
|
||||
@@ -556,7 +625,11 @@ class QuickTabSwitcher extends HookConsumerWidget {
|
||||
if (item.isHistory) {
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.addTab(url: item.url, tabMode: TabMode.regular, selectTab: true);
|
||||
.addTab(
|
||||
url: item.url,
|
||||
tabMode: TabMode.regular,
|
||||
selectTab: true,
|
||||
);
|
||||
} else {
|
||||
await ref.read(tabRepositoryProvider.notifier).selectTab(item.id);
|
||||
}
|
||||
@@ -590,6 +663,7 @@ class QuickTabSwitcher extends HookConsumerWidget {
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -600,6 +674,7 @@ class QuickTabSwitcherView extends StatelessWidget {
|
||||
required this.availableItems,
|
||||
required this.activeItem,
|
||||
required this.scrollController,
|
||||
this.activeItemKey,
|
||||
required this.showTitles,
|
||||
required this.showIsolatedTabUi,
|
||||
required this.onSelected,
|
||||
@@ -609,6 +684,7 @@ class QuickTabSwitcherView extends StatelessWidget {
|
||||
final List<QuickTabSwitcherItem> availableItems;
|
||||
final QuickTabSwitcherItem? activeItem;
|
||||
final ScrollController scrollController;
|
||||
final GlobalKey? activeItemKey;
|
||||
final bool showTitles;
|
||||
final bool showIsolatedTabUi;
|
||||
final Future<void> Function(QuickTabSwitcherItem item) onSelected;
|
||||
@@ -634,6 +710,8 @@ class QuickTabSwitcherView extends StatelessWidget {
|
||||
sortSelectedFirst: false,
|
||||
maxCount: null,
|
||||
scrollController: scrollController,
|
||||
activeItemKey: activeItemKey,
|
||||
cacheExtent: 500,
|
||||
itemId: (item) => item.id,
|
||||
selectedItem: activeItem,
|
||||
selectedBorderColor: Theme.of(context).colorScheme.primary,
|
||||
|
||||
+10
-21
@@ -286,51 +286,40 @@ class _TabGridView extends HookConsumerWidget {
|
||||
[screenWidth, crossAxisCount],
|
||||
);
|
||||
|
||||
final didInitialScroll = useRef(false);
|
||||
|
||||
useEffect(() {
|
||||
if (didInitialScroll.value) return null;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (scrollController.hasClients && activeTab != null) {
|
||||
final index = primaryRows.indexWhere((row) => row.tabId == activeTab);
|
||||
|
||||
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;
|
||||
final viewportDimension =
|
||||
scrollController.position.viewportDimension;
|
||||
|
||||
final isVisible =
|
||||
tabStart >= viewportStart && tabEnd <= viewportEnd;
|
||||
final targetOffset =
|
||||
(tabStart - viewportDimension / 2 + itemSize.height / 2).clamp(
|
||||
0.0,
|
||||
scrollController.position.maxScrollExtent,
|
||||
);
|
||||
|
||||
if (!isVisible) {
|
||||
if (disableAnimations) {
|
||||
scrollController.jumpTo(tabStart);
|
||||
scrollController.jumpTo(targetOffset);
|
||||
} else {
|
||||
unawaited(
|
||||
scrollController.animateTo(
|
||||
tabStart,
|
||||
targetOffset,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
didInitialScroll.value = true;
|
||||
} else {
|
||||
didInitialScroll.value = true;
|
||||
}
|
||||
} else {
|
||||
didInitialScroll.value = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
}, [activeTab]);
|
||||
}, [activeTab, primaryRows.length]);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
|
||||
+5
-21
@@ -325,27 +325,18 @@ class _TabListView extends HookConsumerWidget {
|
||||
|
||||
final activeTab = ref.watch(selectedTabProvider);
|
||||
|
||||
final didInitialScroll = useRef(false);
|
||||
|
||||
useEffect(() {
|
||||
if (didInitialScroll.value) return null;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (scrollController.hasClients && activeTab != null) {
|
||||
final index = primaryRows.indexWhere((row) => row.tabId == activeTab);
|
||||
|
||||
if (index > -1) {
|
||||
final viewportStart = scrollController.offset;
|
||||
final viewportEnd =
|
||||
viewportStart + scrollController.position.viewportDimension;
|
||||
final viewportDimension =
|
||||
scrollController.position.viewportDimension;
|
||||
final tabStart = index * _itemHeight;
|
||||
final tabEnd = tabStart + _itemHeight;
|
||||
|
||||
final isVisible =
|
||||
tabStart >= viewportStart && tabEnd <= viewportEnd;
|
||||
|
||||
if (!isVisible) {
|
||||
final targetOffset = (tabStart - _itemHeight).clamp(
|
||||
final targetOffset =
|
||||
(tabStart - viewportDimension / 2 + _itemHeight / 2).clamp(
|
||||
0.0,
|
||||
scrollController.position.maxScrollExtent,
|
||||
);
|
||||
@@ -361,19 +352,12 @@ class _TabListView extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
didInitialScroll.value = true;
|
||||
} else {
|
||||
didInitialScroll.value = true;
|
||||
}
|
||||
} else {
|
||||
didInitialScroll.value = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
}, [activeTab]);
|
||||
}, [activeTab, primaryRows.length]);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
|
||||
+25
-7
@@ -243,27 +243,45 @@ class ViewTabTreesWidget extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!scrollController.hasClients || activeTab == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final index = filteredTabEntities.value.indexWhere(
|
||||
(entity) => entity.tabId == activeTab,
|
||||
);
|
||||
|
||||
if (index > -1) {
|
||||
final offset = (index ~/ 2) * itemSize.height;
|
||||
if (index < 0) return;
|
||||
|
||||
final row = index ~/ 2;
|
||||
final tabStart = row * itemSize.height;
|
||||
final viewportDimension =
|
||||
scrollController.position.viewportDimension;
|
||||
|
||||
final targetOffset =
|
||||
(tabStart -
|
||||
viewportDimension / 2 +
|
||||
itemSize.height / 2)
|
||||
.clamp(
|
||||
0.0,
|
||||
scrollController.position.maxScrollExtent,
|
||||
);
|
||||
|
||||
if (targetOffset == scrollController.offset) return;
|
||||
|
||||
if (offset != scrollController.offset) {
|
||||
if (disableAnimations) {
|
||||
scrollController.jumpTo(offset);
|
||||
scrollController.jumpTo(targetOffset);
|
||||
} else {
|
||||
unawaited(
|
||||
scrollController.animateTo(
|
||||
offset,
|
||||
targetOffset,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
}, [filteredTabEntities, activeTab]);
|
||||
|
||||
@@ -63,6 +63,8 @@ class SelectableChips<T extends S, S, K> extends StatelessWidget {
|
||||
final bool sortSelectedFirst;
|
||||
|
||||
final ScrollController? scrollController;
|
||||
final GlobalKey? activeItemKey;
|
||||
final double? cacheExtent;
|
||||
|
||||
final K Function(S item) itemId;
|
||||
final Widget Function(T item) itemLabel;
|
||||
@@ -98,6 +100,8 @@ class SelectableChips<T extends S, S, K> extends StatelessWidget {
|
||||
this.onLongPress,
|
||||
this.sortSelectedFirst = true,
|
||||
this.scrollController,
|
||||
this.activeItemKey,
|
||||
this.cacheExtent = 0,
|
||||
this.labelPadding,
|
||||
super.key,
|
||||
});
|
||||
@@ -127,8 +131,7 @@ class SelectableChips<T extends S, S, K> extends StatelessWidget {
|
||||
builder: (context, controller) {
|
||||
return ListView.builder(
|
||||
controller: controller,
|
||||
//Improve list performance by not rendering outside screen at all
|
||||
cacheExtent: 0,
|
||||
cacheExtent: cacheExtent,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: prefixListItems.length + items.length,
|
||||
itemBuilder: (context, index) {
|
||||
@@ -180,7 +183,13 @@ class SelectableChips<T extends S, S, K> extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
|
||||
return (itemWrap != null) ? itemWrap!(child, item) : child;
|
||||
final wrappedChild = (itemWrap != null)
|
||||
? itemWrap!(child, item)
|
||||
: child;
|
||||
|
||||
return isSelected && activeItemKey != null
|
||||
? KeyedSubtree(key: activeItemKey, child: wrappedChild)
|
||||
: wrappedChild;
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user