优化正在播放的自滑动刹车动画动作中
This commit is contained in:
@@ -14,12 +14,11 @@ class _PlaylistPageState extends State<PlaylistPage> {
|
|||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
final GlobalKey _currentTagKey = GlobalKey();
|
final GlobalKey _currentTagKey = GlobalKey();
|
||||||
|
|
||||||
bool _hasScrolledToCurrent = false;
|
// ⭐ 定位事务 ID(防止旧任务污染)
|
||||||
int _retryCount = 0;
|
int _scrollGeneration = 0;
|
||||||
static const int _maxRetries = 3;
|
|
||||||
|
|
||||||
// ⭐ 统一滚动时长 1.5 秒
|
// ⭐ 是否已完成定位
|
||||||
static const Duration _scrollDuration = Duration(milliseconds: 1500);
|
bool _hasScrolledToCurrent = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -35,18 +34,7 @@ class _PlaylistPageState extends State<PlaylistPage> {
|
|||||||
super.dispose();
|
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 {
|
Future<void> _scrollToCurrentSong() async {
|
||||||
if (_hasScrolledToCurrent) return;
|
if (_hasScrolledToCurrent) return;
|
||||||
|
|
||||||
@@ -58,97 +46,66 @@ class _PlaylistPageState extends State<PlaylistPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 生成新的事务 ID,使旧任务失效
|
||||||
|
final generation = ++_scrollGeneration;
|
||||||
|
|
||||||
|
// 等待页面稳定
|
||||||
await Future.delayed(const Duration(milliseconds: 300));
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
if (!mounted) return;
|
if (!mounted || generation != _scrollGeneration) return;
|
||||||
|
|
||||||
final totalCount = queue.length;
|
|
||||||
final avgHeight = _calculateAverageItemHeight(totalCount);
|
|
||||||
final maxExtent = _scrollController.position.maxScrollExtent;
|
|
||||||
|
|
||||||
|
// 获取目标 item 的当前布局状态
|
||||||
var tagContext = _currentTagKey.currentContext;
|
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 viewportHeight = _scrollController.position.viewportDimension;
|
||||||
final step = viewportHeight * 0.5;
|
final roughOffset =
|
||||||
final direction = (currentIndex > totalCount / 2) ? -1 : 1;
|
(currentIndex * avgHeight) - (viewportHeight / 2) + (avgHeight / 2);
|
||||||
final newOffset =
|
final clamped = roughOffset.clamp(0.0, maxExtent);
|
||||||
(currentOffset + direction * step).clamp(0.0, maxExtent);
|
|
||||||
|
|
||||||
if ((newOffset - currentOffset).abs() < 50) {
|
// ⭐ 瞬时跳转(用户不可见)
|
||||||
final bigStep = viewportHeight * 0.8 * direction;
|
_scrollController.jumpTo(clamped);
|
||||||
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;
|
|
||||||
|
|
||||||
|
// 等待一帧,让目标 item 被构建
|
||||||
await WidgetsBinding.instance.endOfFrame;
|
await WidgetsBinding.instance.endOfFrame;
|
||||||
if (!mounted) return;
|
if (!mounted || generation != _scrollGeneration) return;
|
||||||
|
|
||||||
tagContext = _currentTagKey.currentContext;
|
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;
|
_hasScrolledToCurrent = true;
|
||||||
return;
|
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;
|
_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 tagPosition = renderBox.localToGlobal(Offset.zero);
|
||||||
final tagSize = renderBox.size;
|
final tagSize = renderBox.size;
|
||||||
final tagCenter = tagPosition.dy + tagSize.height / 2;
|
final tagCenter = tagPosition.dy + tagSize.height / 2;
|
||||||
@@ -161,13 +118,30 @@ class _PlaylistPageState extends State<PlaylistPage> {
|
|||||||
final targetOffset = (currentOffset + delta)
|
final targetOffset = (currentOffset + delta)
|
||||||
.clamp(0.0, _scrollController.position.maxScrollExtent);
|
.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(
|
await _scrollController.animateTo(
|
||||||
targetOffset,
|
targetOffset,
|
||||||
duration: _scrollDuration,
|
duration: duration,
|
||||||
curve: Curves.easeOutCubic,
|
curve: Curves.easeOutCubic,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (mounted && generation == _scrollGeneration) {
|
||||||
|
_hasScrolledToCurrent = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
Reference in New Issue
Block a user