refactor: enhance chat page auto-scrolling behavior
- Introduced new state variables to manage auto-scrolling functionality. - Implemented a method to calculate the distance from the bottom of the chat. - Improved the logic for auto-scrolling to the bottom when new messages arrive or when the user is near the bottom. - Refactored the scroll-to-bottom logic to enhance performance and user experience. - Ensured that the auto-scroll behavior is only triggered when appropriate, preventing unnecessary scrolls.
This commit is contained in:
@@ -65,6 +65,9 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
double _inputHeight = 0; // dynamic input height to position scroll button
|
double _inputHeight = 0; // dynamic input height to position scroll button
|
||||||
bool _lastKeyboardVisible = false; // track keyboard visibility transitions
|
bool _lastKeyboardVisible = false; // track keyboard visibility transitions
|
||||||
bool _didStartupFocus = false; // one-time auto-focus on startup
|
bool _didStartupFocus = false; // one-time auto-focus on startup
|
||||||
|
String? _lastConversationId;
|
||||||
|
bool _shouldAutoScrollToBottom = true;
|
||||||
|
bool _autoScrollCallbackScheduled = false;
|
||||||
|
|
||||||
String _formatModelDisplayName(String name, {required bool omitProvider}) {
|
String _formatModelDisplayName(String name, {required bool omitProvider}) {
|
||||||
var display = name.trim();
|
var display = name.trim();
|
||||||
@@ -99,6 +102,9 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
if (_scrollController.hasClients) {
|
if (_scrollController.hasClients) {
|
||||||
_scrollController.jumpTo(0);
|
_scrollController.jumpTo(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_shouldAutoScrollToBottom = true;
|
||||||
|
_scheduleAutoScrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _checkAndAutoSelectModel() async {
|
Future<void> _checkAndAutoSelectModel() async {
|
||||||
@@ -274,6 +280,8 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
// Listen to scroll events to show/hide scroll to bottom button
|
// Listen to scroll events to show/hide scroll to bottom button
|
||||||
_scrollController.addListener(_onScroll);
|
_scrollController.addListener(_onScroll);
|
||||||
|
|
||||||
|
_scheduleAutoScrollToBottom();
|
||||||
|
|
||||||
// Initialize chat page components
|
// Initialize chat page components
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -371,12 +379,10 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
|
|
||||||
// Scroll to bottom after enqueuing (only if user was near bottom)
|
// Scroll to bottom after enqueuing (only if user was near bottom)
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (_scrollController.hasClients) {
|
// Only auto-scroll if user was already near the bottom (within 300 px)
|
||||||
final currentScroll = _scrollController.position.pixels;
|
final distanceFromBottom = _distanceFromBottom();
|
||||||
// Only auto-scroll if user was already near the bottom (within 300px)
|
if (distanceFromBottom <= 300) {
|
||||||
if (currentScroll <= 300) {
|
_scrollToBottom();
|
||||||
_scrollToBottom();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -542,18 +548,20 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
_scrollDebounceTimer = Timer(const Duration(milliseconds: 80), () {
|
_scrollDebounceTimer = Timer(const Duration(milliseconds: 80), () {
|
||||||
if (!mounted || _isDeactivated || !_scrollController.hasClients) return;
|
if (!mounted || _isDeactivated || !_scrollController.hasClients) return;
|
||||||
|
|
||||||
final currentScroll = _scrollController.position.pixels;
|
|
||||||
final maxScroll = _scrollController.position.maxScrollExtent;
|
final maxScroll = _scrollController.position.maxScrollExtent;
|
||||||
|
final distanceFromBottom = _distanceFromBottom();
|
||||||
|
|
||||||
const double showThreshold = 300.0;
|
const double showThreshold = 300.0;
|
||||||
const double hideThreshold = 150.0;
|
const double hideThreshold = 150.0;
|
||||||
|
|
||||||
final bool farFromBottom = currentScroll > showThreshold;
|
final bool farFromBottom = distanceFromBottom > showThreshold;
|
||||||
final bool nearBottom = currentScroll <= hideThreshold;
|
final bool nearBottom = distanceFromBottom <= hideThreshold;
|
||||||
|
final bool hasScrollableContent =
|
||||||
|
maxScroll.isFinite && maxScroll > showThreshold;
|
||||||
|
|
||||||
final bool showButton = _showScrollToBottom
|
final bool showButton = _showScrollToBottom
|
||||||
? !nearBottom && maxScroll > showThreshold
|
? !nearBottom && hasScrollableContent
|
||||||
: farFromBottom && maxScroll > showThreshold;
|
: farFromBottom && hasScrollableContent;
|
||||||
|
|
||||||
if (showButton != _showScrollToBottom && mounted && !_isDeactivated) {
|
if (showButton != _showScrollToBottom && mounted && !_isDeactivated) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -563,10 +571,39 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double _distanceFromBottom() {
|
||||||
|
if (!_scrollController.hasClients) {
|
||||||
|
return double.infinity;
|
||||||
|
}
|
||||||
|
final position = _scrollController.position;
|
||||||
|
final maxScroll = position.maxScrollExtent;
|
||||||
|
if (!maxScroll.isFinite) {
|
||||||
|
return double.infinity;
|
||||||
|
}
|
||||||
|
final distance = maxScroll - position.pixels;
|
||||||
|
return distance >= 0 ? distance : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scheduleAutoScrollToBottom() {
|
||||||
|
if (_autoScrollCallbackScheduled) return;
|
||||||
|
_autoScrollCallbackScheduled = true;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_autoScrollCallbackScheduled = false;
|
||||||
|
if (!mounted || !_shouldAutoScrollToBottom) return;
|
||||||
|
if (!_scrollController.hasClients) {
|
||||||
|
_scheduleAutoScrollToBottom();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_scrollToBottom(smooth: false);
|
||||||
|
_shouldAutoScrollToBottom = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void _scrollToBottom({bool smooth = true}) {
|
void _scrollToBottom({bool smooth = true}) {
|
||||||
if (!_scrollController.hasClients) return;
|
if (!_scrollController.hasClients) return;
|
||||||
|
final position = _scrollController.position;
|
||||||
final target = 0.0;
|
final maxScroll = position.maxScrollExtent;
|
||||||
|
final target = maxScroll.isFinite ? maxScroll : 0.0;
|
||||||
if (smooth) {
|
if (smooth) {
|
||||||
_scrollController.animateTo(
|
_scrollController.animateTo(
|
||||||
target,
|
target,
|
||||||
@@ -726,12 +763,25 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
|
|
||||||
final apiService = ref.watch(apiServiceProvider);
|
final apiService = ref.watch(apiServiceProvider);
|
||||||
|
|
||||||
|
if (_shouldAutoScrollToBottom) {
|
||||||
|
_scheduleAutoScrollToBottom();
|
||||||
|
} else {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
const double keepPinnedThreshold = 60.0;
|
||||||
|
final distanceFromBottom = _distanceFromBottom();
|
||||||
|
if (distanceFromBottom > 0 &&
|
||||||
|
distanceFromBottom <= keepPinnedThreshold) {
|
||||||
|
_scrollToBottom(smooth: false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return OptimizedList<ChatMessage>(
|
return OptimizedList<ChatMessage>(
|
||||||
key: const ValueKey('actual_messages'),
|
key: const ValueKey('actual_messages'),
|
||||||
scrollController: _scrollController,
|
scrollController: _scrollController,
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
items: messages,
|
items: messages,
|
||||||
reverse: true,
|
|
||||||
padding: const EdgeInsets.fromLTRB(
|
padding: const EdgeInsets.fromLTRB(
|
||||||
Spacing.lg,
|
Spacing.lg,
|
||||||
Spacing.md,
|
Spacing.md,
|
||||||
@@ -977,6 +1027,14 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
(settings) => settings.omitProviderInModelName,
|
(settings) => settings.omitProviderInModelName,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
final conversationId = ref.watch(
|
||||||
|
activeConversationProvider.select((conv) => conv?.id),
|
||||||
|
);
|
||||||
|
if (conversationId != _lastConversationId) {
|
||||||
|
_lastConversationId = conversationId;
|
||||||
|
_shouldAutoScrollToBottom = true;
|
||||||
|
_scheduleAutoScrollToBottom();
|
||||||
|
}
|
||||||
final conversationTitle = ref.watch(
|
final conversationTitle = ref.watch(
|
||||||
activeConversationProvider.select((conv) => conv?.title),
|
activeConversationProvider.select((conv) => conv?.title),
|
||||||
);
|
);
|
||||||
@@ -1018,11 +1076,9 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
if (keyboardVisible && !_lastKeyboardVisible) {
|
if (keyboardVisible && !_lastKeyboardVisible) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (_scrollController.hasClients) {
|
final distanceFromBottom = _distanceFromBottom();
|
||||||
final currentScroll = _scrollController.position.pixels;
|
if (distanceFromBottom <= 300) {
|
||||||
if (currentScroll <= 300) {
|
_scrollToBottom(smooth: true);
|
||||||
_scrollToBottom(smooth: true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
1
tmp/flutter_ai_repo
Submodule
1
tmp/flutter_ai_repo
Submodule
Submodule tmp/flutter_ai_repo added at 79187cf7e3
Reference in New Issue
Block a user