优化:卡片预览的稳定性

This commit is contained in:
ivan 2026-03-30 00:15:28 +08:00
parent 2ae2c19024
commit fb4d1b78d6

View File

@ -41,6 +41,8 @@ class _HomeScreenState extends State<HomeScreen> {
/// [index] -> 最近一次 visibility 回调的 [VisibilityInfo.visibleFraction]
final Map<int, double> _cardVisibleFraction = {};
bool _visibilityReconcileScheduled = false;
/// 是否曾有过「可见比例 ≥ 阈值」的探测器回调(用于区分冷启动无回调 vs 已全部滑出视口)
bool _visibilityHadMeaningfulReport = false;
/// IndexedStack 切回首页或 [homePlaybackResumeNonce] 递增后,让 [VisibilityDetector] 重新计算。
/// 单次 [notifyNow] 在「Modal 收起 / 子路由 pop」后首帧常过早,与 My Gallery 一致采用多帧 + 短延迟。
@ -62,6 +64,17 @@ class _HomeScreenState extends State<HomeScreen> {
});
Future<void>.delayed(const Duration(milliseconds: 80), notify);
Future<void>.delayed(const Duration(milliseconds: 220), notify);
// 冷启动首屏 Grid 布局/Texture 较晚就绪时,前两档仍可能拿不到可见性
Future<void>.delayed(const Duration(milliseconds: 450), notify);
Future<void>.delayed(const Duration(milliseconds: 720), notify);
Future<void>.delayed(const Duration(milliseconds: 950), () {
if (!mounted || !widget.isActive) return;
VisibilityDetectorController.instance.notifyNow();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || !widget.isActive) return;
_seedFirstScreenVideoIndicesIfStillEmpty();
});
});
}
@override
@ -76,6 +89,21 @@ class _HomeScreenState extends State<HomeScreen> {
if (widget.isActive) refreshAccount();
}
void _seedFirstScreenVideoIndicesIfStillEmpty() {
if (!mounted || !widget.isActive) return;
if (_visibleVideoIndices.isNotEmpty) return;
if (_visibilityHadMeaningfulReport) return;
final tasks = _displayTasks;
if (tasks.isEmpty) return;
final seed = <int>{};
for (var i = 0; i < tasks.length && seed.length < _maxConcurrentHomeVideos; i++) {
final u = tasks[i].previewVideoUrl;
if (u != null && u.isNotEmpty) seed.add(i);
}
if (seed.isEmpty) return;
setState(() => _visibleVideoIndices = seed);
}
@override
void dispose() {
homePlaybackResumeNonce.removeListener(_onHomePlaybackResumeSignal);
@ -94,6 +122,10 @@ class _HomeScreenState extends State<HomeScreen> {
void _onExtConfigChanged() {
if (mounted) setState(() {});
// 不用 Timer 防抖:连续 listener 会互相 cancel,首装可能永远不触发 notify
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && widget.isActive) _scheduleVisibilityRefresh();
});
}
void _onHomeReloadNonce() {
@ -110,6 +142,7 @@ class _HomeScreenState extends State<HomeScreen> {
if (!mounted) return;
if (info.visibleFraction >= _videoVisibilityThreshold) {
_cardVisibleFraction[index] = info.visibleFraction;
_visibilityHadMeaningfulReport = true;
} else {
_cardVisibleFraction.remove(index);
}
@ -133,6 +166,16 @@ class _HomeScreenState extends State<HomeScreen> {
scored.add(MapEntry(i, e.value));
}
}
if (scored.isEmpty) {
// 冷启动:探测器尚未给出 ≥ 阈值的回调时,不要用空集覆盖(否则会清掉兜底或一直不播)
if (!_visibilityHadMeaningfulReport) return;
_userPausedVideoIndices.clear();
if (!_setsEqual(_visibleVideoIndices, {})) {
setState(() => _visibleVideoIndices = {});
}
return;
}
scored.sort((a, b) => b.value.compareTo(a.value));
final next = scored
.take(_maxConcurrentHomeVideos)
@ -255,6 +298,10 @@ class _HomeScreenState extends State<HomeScreen> {
print('HomeScreen: Failed to load categories');
}
setState(() => _categoriesLoading = false);
// 默认选中 pets 等不会走 [_loadTasks] 的路径,也要触发一次可见性(否则预览不自动播)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && widget.isActive) _scheduleVisibilityRefresh();
});
}
}
@ -271,6 +318,7 @@ class _HomeScreenState extends State<HomeScreen> {
_userPausedVideoIndices.clear();
_visibleVideoIndices = {};
_cardVisibleFraction.clear();
_visibilityHadMeaningfulReport = false;
});
} else {
setState(() {
@ -278,6 +326,7 @@ class _HomeScreenState extends State<HomeScreen> {
_userPausedVideoIndices.clear();
_visibleVideoIndices = {};
_cardVisibleFraction.clear();
_visibilityHadMeaningfulReport = false;
});
}
setState(() => _tasksLoading = false);
@ -300,6 +349,7 @@ class _HomeScreenState extends State<HomeScreen> {
_userPausedVideoIndices.clear();
_visibleVideoIndices = {};
_cardVisibleFraction.clear();
_visibilityHadMeaningfulReport = false;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && widget.isActive) _scheduleVisibilityRefresh();