优化正在播放的自滑动刹车动画动作中
This commit is contained in:
@@ -14,12 +14,11 @@ class _PlaylistPageState extends State<PlaylistPage> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final GlobalKey _currentTagKey = GlobalKey();
|
||||
|
||||
bool _hasScrolledToCurrent = false;
|
||||
int _retryCount = 0;
|
||||
static const int _maxRetries = 3;
|
||||
// ⭐ 定位事务 ID(防止旧任务污染)
|
||||
int _scrollGeneration = 0;
|
||||
|
||||
// ⭐ 统一滚动时长 1.5 秒
|
||||
static const Duration _scrollDuration = Duration(milliseconds: 1500);
|
||||
// ⭐ 是否已完成定位
|
||||
bool _hasScrolledToCurrent = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -35,18 +34,7 @@ class _PlaylistPageState extends State<PlaylistPage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
double _calculateAverageItemHeight(int itemCount) {
|
||||
final maxExtent = _scrollController.position.maxScrollExtent;
|
||||
if (maxExtent <= 0 || itemCount <= 0) return 72.0;
|
||||
return maxExtent / itemCount;
|
||||
}
|
||||
|
||||
double _calculateTargetOffset(
|
||||
int currentIndex, int totalCount, double avgHeight) {
|
||||
final viewportHeight = _scrollController.position.viewportDimension;
|
||||
return (currentIndex * avgHeight) - (viewportHeight / 2) + (avgHeight / 2);
|
||||
}
|
||||
|
||||
/// ⭐ 核心:一次计算 + 一次动画
|
||||
Future<void> _scrollToCurrentSong() async {
|
||||
if (_hasScrolledToCurrent) return;
|
||||
|
||||
@@ -58,97 +46,66 @@ class _PlaylistPageState extends State<PlaylistPage> {
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成新的事务 ID,使旧任务失效
|
||||
final generation = ++_scrollGeneration;
|
||||
|
||||
// 等待页面稳定
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
if (!mounted) return;
|
||||
|
||||
final totalCount = queue.length;
|
||||
final avgHeight = _calculateAverageItemHeight(totalCount);
|
||||
final maxExtent = _scrollController.position.maxScrollExtent;
|
||||
if (!mounted || generation != _scrollGeneration) return;
|
||||
|
||||
// 获取目标 item 的当前布局状态
|
||||
var tagContext = _currentTagKey.currentContext;
|
||||
if (tagContext != null) {
|
||||
await _calibrateWithTag(tagContext);
|
||||
_hasScrolledToCurrent = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ⭐ 粗定位:1.5 秒平滑滚动
|
||||
final targetOffset =
|
||||
_calculateTargetOffset(currentIndex, totalCount, avgHeight);
|
||||
final clampedOffset = targetOffset.clamp(0.0, maxExtent);
|
||||
|
||||
await _scrollController.animateTo(
|
||||
clampedOffset,
|
||||
duration: _scrollDuration,
|
||||
curve: Curves.easeOutCubic,
|
||||
);
|
||||
if (!mounted) return;
|
||||
|
||||
await WidgetsBinding.instance.endOfFrame;
|
||||
if (!mounted) return;
|
||||
|
||||
tagContext = _currentTagKey.currentContext;
|
||||
if (tagContext != null) {
|
||||
await _calibrateWithTag(tagContext);
|
||||
_hasScrolledToCurrent = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ⭐ 迭代修正:每次 1.5 秒平滑滚动
|
||||
_retryCount = 0;
|
||||
var currentOffset = _scrollController.offset;
|
||||
var lastOffset = currentOffset;
|
||||
|
||||
while (_retryCount < _maxRetries) {
|
||||
_retryCount++;
|
||||
|
||||
// ⭐ 如果 Tag 还没被构建,用不可见的 jumpTo 触发布局
|
||||
if (tagContext == null) {
|
||||
// 用 maxExtent 计算平均高度,做一次瞬时定位
|
||||
final maxExtent = _scrollController.position.maxScrollExtent;
|
||||
final avgHeight = maxExtent / queue.length;
|
||||
final viewportHeight = _scrollController.position.viewportDimension;
|
||||
final step = viewportHeight * 0.5;
|
||||
final direction = (currentIndex > totalCount / 2) ? -1 : 1;
|
||||
final newOffset =
|
||||
(currentOffset + direction * step).clamp(0.0, maxExtent);
|
||||
final roughOffset =
|
||||
(currentIndex * avgHeight) - (viewportHeight / 2) + (avgHeight / 2);
|
||||
final clamped = roughOffset.clamp(0.0, maxExtent);
|
||||
|
||||
if ((newOffset - currentOffset).abs() < 50) {
|
||||
final bigStep = viewportHeight * 0.8 * direction;
|
||||
final forcedOffset = (currentOffset + bigStep).clamp(0.0, maxExtent);
|
||||
await _scrollController.animateTo(
|
||||
forcedOffset,
|
||||
duration: _scrollDuration,
|
||||
curve: Curves.easeOutCubic,
|
||||
);
|
||||
} else {
|
||||
await _scrollController.animateTo(
|
||||
newOffset,
|
||||
duration: _scrollDuration,
|
||||
curve: Curves.easeOutCubic,
|
||||
);
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
// ⭐ 瞬时跳转(用户不可见)
|
||||
_scrollController.jumpTo(clamped);
|
||||
|
||||
// 等待一帧,让目标 item 被构建
|
||||
await WidgetsBinding.instance.endOfFrame;
|
||||
if (!mounted) return;
|
||||
if (!mounted || generation != _scrollGeneration) return;
|
||||
|
||||
tagContext = _currentTagKey.currentContext;
|
||||
if (tagContext != null) {
|
||||
await _calibrateWithTag(tagContext);
|
||||
|
||||
// 如果还是 null,说明估算偏差太大,用迭代方式推进
|
||||
if (tagContext == null) {
|
||||
// 最多尝试 2 次,每次推进半屏
|
||||
for (int i = 0; i < 2; i++) {
|
||||
final viewportHeight2 = _scrollController.position.viewportDimension;
|
||||
final direction = (currentIndex > queue.length / 2) ? -1 : 1;
|
||||
final step = viewportHeight2 * 0.7 * direction;
|
||||
final newOffset = (_scrollController.offset + step)
|
||||
.clamp(0.0, _scrollController.position.maxScrollExtent);
|
||||
_scrollController.jumpTo(newOffset);
|
||||
await WidgetsBinding.instance.endOfFrame;
|
||||
if (!mounted || generation != _scrollGeneration) return;
|
||||
tagContext = _currentTagKey.currentContext;
|
||||
if (tagContext != null) break;
|
||||
}
|
||||
if (tagContext == null) {
|
||||
_hasScrolledToCurrent = true;
|
||||
return;
|
||||
}
|
||||
|
||||
currentOffset = _scrollController.offset;
|
||||
if ((currentOffset - lastOffset).abs() < 10) break;
|
||||
lastOffset = currentOffset;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mounted || generation != _scrollGeneration) return;
|
||||
|
||||
// ⭐ 获取 Tag 的真实位置
|
||||
final renderBox = tagContext!.findRenderObject() as RenderBox?;
|
||||
if (renderBox == null) {
|
||||
_hasScrolledToCurrent = true;
|
||||
return;
|
||||
}
|
||||
|
||||
/// 精确定位:1.5 秒平滑滚动
|
||||
Future<void> _calibrateWithTag(BuildContext tagContext) async {
|
||||
final renderBox = tagContext.findRenderObject() as RenderBox?;
|
||||
if (renderBox == null) return;
|
||||
|
||||
final tagPosition = renderBox.localToGlobal(Offset.zero);
|
||||
final tagSize = renderBox.size;
|
||||
final tagCenter = tagPosition.dy + tagSize.height / 2;
|
||||
@@ -161,13 +118,30 @@ class _PlaylistPageState extends State<PlaylistPage> {
|
||||
final targetOffset = (currentOffset + delta)
|
||||
.clamp(0.0, _scrollController.position.maxScrollExtent);
|
||||
|
||||
if ((targetOffset - currentOffset).abs() < 2) return;
|
||||
// 如果偏差太小,直接标记完成
|
||||
if ((targetOffset - currentOffset).abs() < 5) {
|
||||
_hasScrolledToCurrent = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// ⭐ 根据距离动态计算时长
|
||||
final distance = (targetOffset - currentOffset).abs();
|
||||
final durationMs = (250 + distance * 0.3).clamp(250, 700).round();
|
||||
final duration = Duration(milliseconds: durationMs);
|
||||
|
||||
debugPrint(
|
||||
'🎯 [定位] 距离: ${distance.toStringAsFixed(0)}px, 时长: ${durationMs}ms');
|
||||
|
||||
// ⭐ 唯一的一次可见动画
|
||||
await _scrollController.animateTo(
|
||||
targetOffset,
|
||||
duration: _scrollDuration,
|
||||
duration: duration,
|
||||
curve: Curves.easeOutCubic,
|
||||
);
|
||||
|
||||
if (mounted && generation == _scrollGeneration) {
|
||||
_hasScrolledToCurrent = true;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
Reference in New Issue
Block a user