refactor cached image

This commit is contained in:
Lightczx
2024-05-15 13:39:19 +08:00
parent 29454b188e
commit f4593cd325
9 changed files with 26 additions and 193 deletions

View File

@@ -23,13 +23,10 @@ internal sealed class CachedImage : Implementation.ImageEx
{ {
DefaultStyleKey = typeof(CachedImage); DefaultStyleKey = typeof(CachedImage);
DefaultStyleResourceUri = "ms-appx:///Control/Image/CachedImage.xaml".ToUri(); DefaultStyleResourceUri = "ms-appx:///Control/Image/CachedImage.xaml".ToUri();
IsCacheEnabled = true;
EnableLazyLoading = false;
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override async Task<ImageSource?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token) protected override async Task<Uri?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
{ {
IImageCache imageCache = this.ServiceProvider().GetRequiredService<IImageCache>(); IImageCache imageCache = this.ServiceProvider().GetRequiredService<IImageCache>();
@@ -38,10 +35,7 @@ internal sealed class CachedImage : Implementation.ImageEx
HutaoException.ThrowIf(string.IsNullOrEmpty(imageUri.Host), SH.ControlImageCachedImageInvalidResourceUri); HutaoException.ThrowIf(string.IsNullOrEmpty(imageUri.Host), SH.ControlImageCachedImageInvalidResourceUri);
string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true); // BitmapImage need to be created by main thread. string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true); // BitmapImage need to be created by main thread.
token.ThrowIfCancellationRequested(); // check token state to determine whether the operation should be canceled. token.ThrowIfCancellationRequested(); // check token state to determine whether the operation should be canceled.
return file.ToUri();
// https://learn.microsoft.com/en-us/windows/uwp/debug-test-perf/optimize-animations-and-media#optimize-image-resources
// BitmapImage initialize with a uri will increase image quality and loading speed.
return new BitmapImage(file.ToUri());
} }
catch (COMException) catch (COMException)
{ {

View File

@@ -6,7 +6,6 @@
<Setter Property="Background" Value="Transparent"/> <Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{ThemeResource ApplicationForegroundThemeBrush}"/> <Setter Property="Foreground" Value="{ThemeResource ApplicationForegroundThemeBrush}"/>
<Setter Property="IsTabStop" Value="False"/> <Setter Property="IsTabStop" Value="False"/>
<Setter Property="LazyLoadingThreshold" Value="256"/>
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="shci:CachedImage"> <ControlTemplate TargetType="shci:CachedImage">

View File

@@ -21,12 +21,6 @@ namespace Snap.Hutao.Control.Image.Implementation;
[TemplatePart(Name = PartImage, Type = typeof(object))] [TemplatePart(Name = PartImage, Type = typeof(object))]
[TemplatePart(Name = PartPlaceholderImage, Type = typeof(object))] [TemplatePart(Name = PartPlaceholderImage, Type = typeof(object))]
[DependencyProperty("Stretch", typeof(Stretch), Stretch.Uniform)] [DependencyProperty("Stretch", typeof(Stretch), Stretch.Uniform)]
[DependencyProperty("DecodePixelHeight", typeof(int), 0)]
[DependencyProperty("DecodePixelWidth", typeof(int), 0)]
[DependencyProperty("DecodePixelType", typeof(DecodePixelType), DecodePixelType.Physical)]
[DependencyProperty("IsCacheEnabled", typeof(bool), false)]
[DependencyProperty("EnableLazyLoading", typeof(bool), false, nameof(EnableLazyLoadingChanged))]
[DependencyProperty("LazyLoadingThreshold", typeof(double), default(double), nameof(LazyLoadingThresholdChanged))]
[DependencyProperty("PlaceholderSource", typeof(object), default(object))] [DependencyProperty("PlaceholderSource", typeof(object), default(object))]
[DependencyProperty("PlaceholderStretch", typeof(Stretch), Stretch.Uniform)] [DependencyProperty("PlaceholderStretch", typeof(Stretch), Stretch.Uniform)]
[DependencyProperty("PlaceholderMargin", typeof(Thickness))] [DependencyProperty("PlaceholderMargin", typeof(Thickness))]
@@ -42,8 +36,6 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
protected const string FailedState = "Failed"; protected const string FailedState = "Failed";
private CancellationTokenSource? tokenSource; private CancellationTokenSource? tokenSource;
private object? lazyLoadingSource;
private bool isInViewport;
public bool IsInitialized { get; private set; } public bool IsInitialized { get; private set; }
@@ -58,10 +50,10 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
public abstract CompositionBrush GetAlphaMask(); public abstract CompositionBrush GetAlphaMask();
protected virtual Task<ImageSource?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token) protected virtual Task<Uri?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
{ {
// By default we just use the built-in UWP image cache provided within the Image control. // By default we just use the built-in UWP image cache provided within the Image control.
return Task.FromResult<ImageSource?>(new BitmapImage(imageUri)); return Task.FromResult<Uri?>(imageUri);
} }
protected virtual void OnImageOpened(object sender, RoutedEventArgs e) protected virtual void OnImageOpened(object sender, RoutedEventArgs e)
@@ -80,19 +72,10 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
RemoveImageFailed(OnImageFailed); RemoveImageFailed(OnImageFailed);
Image = GetTemplateChild(PartImage); Image = GetTemplateChild(PartImage);
PlaceholderImage = GetTemplateChild(PartPlaceholderImage);
IsInitialized = true; IsInitialized = true;
if (Source is null || !EnableLazyLoading || isInViewport) SetSource(Source);
{
lazyLoadingSource = null;
SetSource(Source);
}
else
{
lazyLoadingSource = Source;
}
AttachImageOpened(OnImageOpened); AttachImageOpened(OnImageOpened);
AttachImageFailed(OnImageFailed); AttachImageFailed(OnImageFailed);
@@ -148,33 +131,6 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
} }
} }
private static void EnableLazyLoadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not ImageExBase control)
{
return;
}
bool value = (bool)e.NewValue;
if (value)
{
control.LayoutUpdated += control.OnImageExBaseLayoutUpdated;
control.InvalidateLazyLoading();
}
else
{
control.LayoutUpdated -= control.OnImageExBaseLayoutUpdated;
}
}
private static void LazyLoadingThresholdChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ImageExBase { EnableLazyLoading: true } control)
{
control.InvalidateLazyLoading();
}
}
private static void SourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) private static void SourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{ {
if (d is not ImageExBase control) if (d is not ImageExBase control)
@@ -187,15 +143,7 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
return; return;
} }
if (e.NewValue is null || !control.EnableLazyLoading || control.isInViewport) control.SetSource(e.NewValue);
{
control.lazyLoadingSource = null;
control.SetSource(e.NewValue);
}
else
{
control.lazyLoadingSource = e.NewValue;
}
} }
private static bool IsHttpUri(Uri uri) private static bool IsHttpUri(Uri uri)
@@ -203,11 +151,8 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
return uri.IsAbsoluteUri && (uri.Scheme == "http" || uri.Scheme == "https"); return uri.IsAbsoluteUri && (uri.Scheme == "http" || uri.Scheme == "https");
} }
private void AttachSource(ImageSource? source) private void AttachSource(BitmapImage? source, Uri? uri)
{ {
// Setting the source at this point should call ImageExOpened/VisualStateManager.GoToState
// as we register to both the ImageOpened/ImageFailed events of the underlying control.
// We only need to call those methods if we fail in other cases before we get here.
if (Image is Microsoft.UI.Xaml.Controls.Image image) if (Image is Microsoft.UI.Xaml.Controls.Image image)
{ {
image.Source = source; image.Source = source;
@@ -221,13 +166,15 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
{ {
VisualStateManager.GoToState(this, UnloadedState, true); VisualStateManager.GoToState(this, UnloadedState, true);
} }
else if (source is BitmapSource { PixelHeight: > 0, PixelWidth: > 0 }) else
{ {
// https://learn.microsoft.com/en-us/windows/uwp/debug-test-perf/optimize-animations-and-media#optimize-image-resources
source.UriSource = uri;
VisualStateManager.GoToState(this, LoadedState, true); VisualStateManager.GoToState(this, LoadedState, true);
} }
} }
private void AttachPlaceholderSource(ImageSource? source) private void AttachPlaceholderSource(BitmapImage? source, Uri? uri)
{ {
if (PlaceholderImage is Microsoft.UI.Xaml.Controls.Image image) if (PlaceholderImage is Microsoft.UI.Xaml.Controls.Image image)
{ {
@@ -242,8 +189,10 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
{ {
VisualStateManager.GoToState(this, UnloadedState, true); VisualStateManager.GoToState(this, UnloadedState, true);
} }
else if (source is BitmapSource { PixelHeight: > 0, PixelWidth: > 0 }) else
{ {
// https://learn.microsoft.com/en-us/windows/uwp/debug-test-perf/optimize-animations-and-media#optimize-image-resources
source.UriSource = uri;
VisualStateManager.GoToState(this, LoadedState, true); VisualStateManager.GoToState(this, LoadedState, true);
} }
} }
@@ -256,10 +205,9 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
} }
tokenSource?.Cancel(); tokenSource?.Cancel();
tokenSource = new CancellationTokenSource(); tokenSource = new CancellationTokenSource();
AttachSource(null); AttachSource(default, default);
if (source is null) if (source is null)
{ {
@@ -268,13 +216,6 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
VisualStateManager.GoToState(this, LoadingState, true); VisualStateManager.GoToState(this, LoadingState, true);
if (source as ImageSource is { } imageSource)
{
AttachSource(imageSource);
return;
}
if (source as Uri is not { } uri) if (source as Uri is not { } uri)
{ {
string? url = source as string ?? source.ToString(); string? url = source as string ?? source.ToString();
@@ -319,20 +260,13 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
tokenSource?.Cancel(); tokenSource?.Cancel();
tokenSource = new(); tokenSource = new();
AttachPlaceholderSource(null); AttachPlaceholderSource(default, default);
if (source is null) if (source is null)
{ {
return; return;
} }
if (source as ImageSource is { } imageSource)
{
AttachPlaceholderSource(imageSource);
return;
}
if (source as Uri is not { } uri) if (source as Uri is not { } uri)
{ {
string? url = source as string ?? source.ToString(); string? url = source as string ?? source.ToString();
@@ -354,13 +288,13 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
return; return;
} }
ImageSource? img = await ProvideCachedResourceAsync(uri, tokenSource.Token).ConfigureAwait(true); Uri? actualUri = await ProvideCachedResourceAsync(uri, tokenSource.Token).ConfigureAwait(true);
ArgumentNullException.ThrowIfNull(tokenSource); ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested) if (!tokenSource.IsCancellationRequested)
{ {
// Only attach our image if we still have a valid request. // Only attach our image if we still have a valid request.
AttachPlaceholderSource(img); AttachPlaceholderSource(new BitmapImage(), actualUri);
} }
} }
catch (OperationCanceledException) catch (OperationCanceledException)
@@ -379,99 +313,13 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
return; return;
} }
if (IsCacheEnabled) Uri? actualUri = await ProvideCachedResourceAsync(imageUri, token).ConfigureAwait(true);
ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested)
{ {
ImageSource? img = await ProvideCachedResourceAsync(imageUri, token).ConfigureAwait(true); // Only attach our image if we still have a valid request.
AttachSource(new BitmapImage(), actualUri);
ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested)
{
// Only attach our image if we still have a valid request.
AttachSource(img);
}
}
else if (string.Equals(imageUri.Scheme, "data", StringComparison.OrdinalIgnoreCase))
{
string source = imageUri.OriginalString;
const string base64Head = "base64,";
int index = source.IndexOf(base64Head, StringComparison.Ordinal);
if (index >= 0)
{
byte[] bytes = Convert.FromBase64String(source[(index + base64Head.Length)..]);
BitmapImage bitmap = new();
await bitmap.SetSourceAsync(new MemoryStream(bytes).AsRandomAccessStream());
ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested)
{
AttachSource(bitmap);
}
}
}
else
{
AttachSource(new BitmapImage(imageUri)
{
CreateOptions = BitmapCreateOptions.IgnoreImageCache,
});
}
}
private void OnImageExBaseLayoutUpdated(object? sender, object e)
{
InvalidateLazyLoading();
}
private void InvalidateLazyLoading()
{
if (!IsLoaded)
{
isInViewport = false;
return;
}
// Find the first ascendant ScrollViewer, if not found, use the root element.
FrameworkElement? hostElement = default;
IEnumerable<FrameworkElement> ascendants = this.FindAscendants().OfType<FrameworkElement>();
foreach (FrameworkElement ascendant in ascendants)
{
hostElement = ascendant;
if (hostElement is Microsoft.UI.Xaml.Controls.ScrollViewer)
{
break;
}
}
if (hostElement is null)
{
isInViewport = false;
return;
}
Rect controlRect = TransformToVisual(hostElement).TransformBounds(StructMarshal.Rect(ActualSize));
double lazyLoadingThreshold = LazyLoadingThreshold;
// Left/Top 1 Threshold, Right/Bottom 2 Threshold
Rect hostRect = new(
0 - lazyLoadingThreshold,
0 - lazyLoadingThreshold,
hostElement.ActualWidth + (2 * lazyLoadingThreshold),
hostElement.ActualHeight + (2 * lazyLoadingThreshold));
if (controlRect.IntersectsWith(hostRect))
{
isInViewport = true;
if (lazyLoadingSource is not null)
{
object source = lazyLoadingSource;
lazyLoadingSource = null;
SetSource(source);
}
}
else
{
isInViewport = false;
} }
} }
} }

View File

@@ -15,15 +15,14 @@
<Grid> <Grid>
<Grid CornerRadius="{StaticResource ControlCornerRadius}"> <Grid CornerRadius="{StaticResource ControlCornerRadius}">
<!-- Disable some CachedImage's LazyLoading function here can increase response speed --> <!-- Disable some CachedImage's LazyLoading function here can increase response speed -->
<shci:CachedImage EnableLazyLoading="False" Source="{x:Bind Quality, Converter={StaticResource QualityConverter}, Mode=OneWay}"/> <shci:CachedImage Source="{x:Bind Quality, Converter={StaticResource QualityConverter}, Mode=OneWay}"/>
<shci:CachedImage EnableLazyLoading="False" Source="{StaticResource UI_ImgSign_ItemIcon}"/> <shci:CachedImage Source="{StaticResource UI_ImgSign_ItemIcon}"/>
<shci:CachedImage Source="{x:Bind Icon, Mode=OneWay}"/> <shci:CachedImage Source="{x:Bind Icon, Mode=OneWay}"/>
<shci:CachedImage <shci:CachedImage
Margin="2" Margin="2"
HorizontalAlignment="Left" HorizontalAlignment="Left"
VerticalAlignment="Top" VerticalAlignment="Top"
shch:FrameworkElementHelper.SquareLength="16" shch:FrameworkElementHelper.SquareLength="16"
EnableLazyLoading="False"
Source="{x:Bind Badge, Mode=OneWay}"/> Source="{x:Bind Badge, Mode=OneWay}"/>
</Grid> </Grid>
</Grid> </Grid>

View File

@@ -17,7 +17,6 @@
<shci:CachedImage <shci:CachedImage
Width="120" Width="120"
Height="120" Height="120"
EnableLazyLoading="False"
Source="{StaticResource UI_EmotionIcon272}"/> Source="{StaticResource UI_EmotionIcon272}"/>
<TextBlock <TextBlock
Margin="0,16,0,0" Margin="0,16,0,0"

View File

@@ -251,7 +251,6 @@
<shci:CachedImage <shci:CachedImage
Height="120" Height="120"
MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" MinWidth="{ThemeResource SettingsCardContentControlMinWidth}"
EnableLazyLoading="False"
Source="{StaticResource UI_EmotionIcon52}"/> Source="{StaticResource UI_EmotionIcon52}"/>
<TextBlock <TextBlock
Margin="0,5,0,21" Margin="0,5,0,21"

View File

@@ -415,7 +415,6 @@
<shci:CachedImage <shci:CachedImage
Height="120" Height="120"
MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" MinWidth="{ThemeResource SettingsCardContentControlMinWidth}"
EnableLazyLoading="False"
Source="{StaticResource UI_EmotionIcon445}"/> Source="{StaticResource UI_EmotionIcon445}"/>
<TextBlock <TextBlock
Margin="0,5,0,21" Margin="0,5,0,21"

View File

@@ -369,7 +369,6 @@
<shci:CachedImage <shci:CachedImage
Height="120" Height="120"
MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" MinWidth="{ThemeResource SettingsCardContentControlMinWidth}"
EnableLazyLoading="False"
Source="{StaticResource UI_EmotionIcon89}"/> Source="{StaticResource UI_EmotionIcon89}"/>
<TextBlock <TextBlock
Margin="0,5,0,21" Margin="0,5,0,21"
@@ -754,7 +753,6 @@
<shci:CachedImage <shci:CachedImage
Height="120" Height="120"
MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" MinWidth="{ThemeResource SettingsCardContentControlMinWidth}"
EnableLazyLoading="False"
Source="{StaticResource UI_EmotionIcon89}"/> Source="{StaticResource UI_EmotionIcon89}"/>
<TextBlock <TextBlock
Margin="0,5,0,21" Margin="0,5,0,21"

View File

@@ -238,7 +238,6 @@
<shci:CachedImage <shci:CachedImage
Height="120" Height="120"
MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" MinWidth="{ThemeResource SettingsCardContentControlMinWidth}"
EnableLazyLoading="False"
Source="{StaticResource UI_EmotionIcon89}"/> Source="{StaticResource UI_EmotionIcon89}"/>
<TextBlock <TextBlock
Margin="0,5,0,21" Margin="0,5,0,21"
@@ -365,7 +364,6 @@
<shci:CachedImage <shci:CachedImage
Height="120" Height="120"
MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" MinWidth="{ThemeResource SettingsCardContentControlMinWidth}"
EnableLazyLoading="False"
Source="{StaticResource UI_EmotionIcon89}"/> Source="{StaticResource UI_EmotionIcon89}"/>
<TextBlock <TextBlock
Margin="0,5,0,21" Margin="0,5,0,21"