You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

283 lines
9.9 KiB

using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
using ReactiveUI.Avalonia;
using ReactiveUI;
using System.Reactive.Disposables;
using AuroraDesk.Presentation.ViewModels.Pages;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace AuroraDesk.Presentation.Views.Pages;
/// <summary>
/// 图片浏览页面视图
///
/// 核心功能:
/// - 实现滚动检测,当滚动接近底部时自动加载更多图片
/// - 距底部小于500px时触发加载
/// - 防止重复加载的保护逻辑
/// </summary>
public partial class ImageGalleryPageView : ReactiveUserControl<ImageGalleryPageViewModel>
{
private ScrollViewer? _scrollViewer;
private CancellationTokenSource? _scrollDetectionCancellationToken;
private CancellationTokenSource? _layoutCheckCancellationToken;
private bool _isLoadingMore = false; // 防止重复加载
private bool _isDisposed = false; // 标记视图是否已停用
public ImageGalleryPageView()
{
InitializeComponent();
this.WhenActivated(disposables =>
{
// 当视图激活时,设置滚动检测并重置清理标志
Dispatcher.UIThread.Post(() =>
{
SetupScrollDetection();
// 重置所有 ImageMetadata 的清理标志并尝试从缓存恢复
// 性能优化:只恢复可见区域附近的图片,其他图片按需加载
if (ViewModel != null && _scrollViewer != null)
{
// 尝试计算可见区域(如果滚动位置已恢复)
// 由于 WrapPanel 布局复杂,暂时只恢复前100个(保守策略)
// 其他图片会在 UI 绑定时按需加载,不会影响性能
ViewModel.ResetAllThumbnailReferences();
}
else if (ViewModel != null)
{
// 如果 ScrollViewer 还没准备好,只恢复前100个
ViewModel.ResetAllThumbnailReferences();
}
}, DispatcherPriority.Loaded);
// 当视图停用时,清理资源
disposables.Add(Disposable.Create(() =>
{
// 标记为已停用,防止事件处理器访问已释放的资源
_isDisposed = true;
// 先取消事件订阅,防止事件处理器访问已释放的资源
if (_scrollViewer != null)
{
_scrollViewer.ScrollChanged -= OnScrollChanged;
_scrollViewer.LayoutUpdated -= OnLayoutUpdated;
_scrollViewer = null;
}
// 清理滚动检测相关资源
try
{
_scrollDetectionCancellationToken?.Cancel();
}
catch (ObjectDisposedException)
{
// 已被释放,忽略
}
_scrollDetectionCancellationToken?.Dispose();
_scrollDetectionCancellationToken = null;
try
{
_layoutCheckCancellationToken?.Cancel();
}
catch (ObjectDisposedException)
{
// 已被释放,忽略
}
_layoutCheckCancellationToken?.Dispose();
_layoutCheckCancellationToken = null;
// 清理缩略图引用,防止在 tab 切换时访问已释放的 Bitmap
ViewModel?.ClearThumbnailReferences();
}));
});
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
/// <summary>
/// 设置滚动检测
/// 监听 ScrollViewer 的滚动事件,当接近底部时加载更多
/// </summary>
private void SetupScrollDetection()
{
// 重置停用标志
_isDisposed = false;
// 先清理之前可能存在的订阅(如果视图被重新激活)
if (_scrollViewer != null)
{
_scrollViewer.ScrollChanged -= OnScrollChanged;
_scrollViewer.LayoutUpdated -= OnLayoutUpdated;
}
// 查找 ScrollViewer
_scrollViewer = this.FindControl<ScrollViewer>("MainScrollViewer");
if (_scrollViewer == null)
{
_scrollViewer = this.GetLogicalDescendants().OfType<ScrollViewer>().FirstOrDefault();
}
if (_scrollViewer != null)
{
// 绑定滚动事件
_scrollViewer.ScrollChanged += OnScrollChanged;
// 监听布局完成事件,确保在布局完成后检查是否需要加载更多
_scrollViewer.LayoutUpdated += OnLayoutUpdated;
// 延迟检查一次,确保在初始加载完成后能自动加载更多
_ = Task.Run(async () =>
{
await Task.Delay(800);
await Dispatcher.UIThread.InvokeAsync(async () =>
{
if (!_isLoadingMore && ViewModel != null)
{
await CheckAndLoadMoreAsync();
}
}, DispatcherPriority.Background);
});
}
}
/// <summary>
/// 布局更新时检查是否需要加载更多
/// </summary>
private void OnLayoutUpdated(object? sender, EventArgs e)
{
// 如果视图已停用,直接返回,防止访问已释放的资源
if (_isDisposed)
return;
// 只在布局稳定后检查一次,避免频繁触发
if (!_isLoadingMore && ViewModel != null && _scrollViewer != null)
{
// 防抖:取消之前的布局检查任务
try
{
_layoutCheckCancellationToken?.Cancel();
}
catch (ObjectDisposedException)
{
// 已被释放,忽略
}
_layoutCheckCancellationToken?.Dispose();
_layoutCheckCancellationToken = new CancellationTokenSource();
var token = _layoutCheckCancellationToken.Token;
// 检查是否需要加载更多(延迟检查,避免在布局过程中频繁触发)
_ = Task.Run(async () =>
{
await Task.Delay(200, token);
if (token.IsCancellationRequested)
return;
await Dispatcher.UIThread.InvokeAsync(async () =>
{
if (!token.IsCancellationRequested && !_isLoadingMore && ViewModel != null)
{
await CheckAndLoadMoreAsync();
}
}, DispatcherPriority.Background);
}, token);
}
}
/// <summary>
/// 滚动时检查是否需要加载更多
/// </summary>
private void OnScrollChanged(object? sender, ScrollChangedEventArgs e)
{
// 如果视图已停用,直接返回,防止访问已释放的资源
if (_isDisposed)
return;
// 防抖:取消之前的检测任务
try
{
_scrollDetectionCancellationToken?.Cancel();
}
catch (ObjectDisposedException)
{
// 已被释放,忽略
}
_scrollDetectionCancellationToken?.Dispose();
_scrollDetectionCancellationToken = new CancellationTokenSource();
var token = _scrollDetectionCancellationToken.Token;
// 延迟200ms检测,避免频繁触发
_ = Task.Delay(200, token).ContinueWith(async _ =>
{
if (!token.IsCancellationRequested && ViewModel != null && _scrollViewer != null)
{
await CheckAndLoadMoreAsync();
}
}, token);
}
/// <summary>
/// 检查是否需要加载更多图片
/// 当滚动位置距底部小于500px时,触发加载
/// </summary>
private async Task CheckAndLoadMoreAsync()
{
if (ViewModel == null || _scrollViewer == null || _isLoadingMore)
return;
try
{
// 如果正在加载初始数据,不触发加载更多
if (ViewModel.IsLoading)
return;
// 获取滚动信息
var scrollOffset = _scrollViewer.Offset.Y;
var viewportHeight = _scrollViewer.Viewport.Height;
var extentHeight = _scrollViewer.Extent.Height;
if (extentHeight <= 0 || viewportHeight <= 0)
return;
// 计算距离底部的距离
var maxScrollOffset = extentHeight - viewportHeight;
var distanceToBottom = maxScrollOffset - scrollOffset;
// 如果距离底部小于500px,且还有更多内容未加载,则触发加载
if (distanceToBottom < 500 && ViewModel.Images.Count < ViewModel.TotalImageCount)
{
_isLoadingMore = true;
try
{
// 调用 ViewModel 加载更多
await ViewModel.LoadMoreImagesAsync();
}
finally
{
// 延迟重置标志,避免短时间内重复触发
await Task.Delay(300);
_isLoadingMore = false;
}
}
}
catch (Exception ex)
{
// 忽略错误,避免影响UI
System.Diagnostics.Debug.WriteLine($"检查加载更多时出错: {ex.Message}");
_isLoadingMore = false;
}
}
}