概述
UWP Community Toolkit 中有一个图片的扩展控件 - ImageEx,本篇我们结合代码详细讲解 ImageEx 的实现。
ImageEx 是一个图片的扩展控件,包括 ImageEx 和 RoundImageEx,它可以在异步加载图片源时显示加载状态,也可以在加载前使用占位图片,在下载完成后可以在应用内缓存,避免了重复加载的过程。我们来看一下官方的介绍和官网示例中的展示:
Doc: https://docs.microsoft.com/zh-cn/windows/uwpcommunitytoolkit/controls/imageex
Namespace: Microsoft.Toolkit.Uwp.UI.Controls; Nuget: Microsoft.Toolkit.Uwp.UI.Controls;
开发过程
代码分析
我们来看一下 ImageEx 控件的结构:
- ImageEx.Members.cs - ImageEx 控件部分类的成员变量类
- ImageEx.cs - ImageEx 控件部分类的定义类
- ImageEx.xaml - ImageEx 控件样式文件
- ImageExBase.Members.cs - ImageEx 控件基类部分类的成员变量类
- ImageExBase.Placeholder.cs - ImageEx 控件基类部分类的占位符类
- ImageExBase.Source.cs - ImageEx 控件基类部分类的图片源类
- ImageExBase.cs - ImageEx 控件基类部分类的定义类
- ImageExFailedEventArgs.cs - ImageEx 控件的失败事件参数类
- ImageExOpenedEventArgs.cs - ImageEx 控件的打开事件参数类
- RoundImageEx.Members.cs - RoundImageEx 控件部分类的成员变量类
- RoundImageEx.cs - RoundImageEx 控件部分类的定义类
- RoundImageEx.xaml - RoundImageEx 控件样式文件
下面把几个重点的类详细分析一下:
1. ImageEx.xaml
ImageEx 控件的样式文件,来看一下 Template 部分,包含了三层的控件:PlaceHolderImage,Image 和 Progress,这样就可以完成加载中或失败时显示 PlaceHolder 和 Progress,加载成功后显示 Image;同时样式在 Failed,Loading,Loaded 和 Unloaded 状态时,也会切换不同层的显示来完成状态切换;
1 <Style TargetType="controls:ImageEx"> 2 <Setter Property="Background" Value="Transparent" /> 3 <Setter Property="Foreground" Value="{ThemeResource ApplicationForegroundThemeBrush}" /> 4 <Setter Property="Template"> 5 <Setter.Value> 6 <ControlTemplate TargetType="controls:ImageEx"> 7 <Grid Background="{TemplateBinding Background}" CornerRadius="{TemplateBinding CornerRadius}" 8 BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> 9 <Image Name="PlaceholderImage" Opacity="1.0" .../> 10 <Image Name="Image" NineGrid="{TemplateBinding NineGrid}" Opacity="0.0" .../> 11 <ProgressRing Name="Progress" Margin="16" HorizontalAlignment="Center" VerticalAlignment="Center" 12 Background="Transparent" Foreground="{TemplateBinding Foreground}" IsActive="False" Visibility="Collapsed" /> 13 <VisualStateManager.VisualStateGroups> 14 <VisualStateGroup x:Name="CommonStates"> 15 <VisualState x:Name="Failed"> 16 <Storyboard> 17 <ObjectAnimationUsingKeyFrames Storyboard.TargetName="Image" 18 Storyboard.TargetProperty="Opacity"> 19 <DiscreteObjectKeyFrame KeyTime="0" 20 Value="0" /> 21 </ObjectAnimationUsingKeyFrames> 22 <ObjectAnimationUsingKeyFrames Storyboard.TargetName="PlaceholderImage" 23 Storyboard.TargetProperty="Opacity"> 24 <DiscreteObjectKeyFrame KeyTime="0" 25 Value="1" /> 26 </ObjectAnimationUsingKeyFrames> 27 </Storyboard> 28 </VisualState> 29 <VisualState x:Name="Loading" .../> 30 <VisualState x:Name="Loaded" .../> 31 <VisualState x:Name="Unloaded" .../> 32 </VisualStateGroup> 33 </VisualStateManager.VisualStateGroups> 34 </Grid> 35 </ControlTemplate> 36 </Setter.Value> 37 </Setter></Style>
2. ImageExBase.Members.cs
ImageEx 控件的定义和功能实现主要在 ImageExBase 类中,而 ImageExBase.Members.cs 主要定义了类的成员,具体如下:
- Stretch - 获取或设置控件的拉伸属性
- CornerRadius - 获取或设置控件的圆角半径,用于 Rounded 或 Circle 图片控件
- DecodePixelHeight - 获取或设置控件的解码像素高度
- DecodePixelType - 获取或设置控件的解码像素类型
- DecodePixelWidth - 获取或设置控件的解码像素宽度
- IsCacheEnabled - 获取或设置缓存是否可用
另外还定义了 ImageFailed、ImageOpened、ImageExInitialized 事件,以及 GetAlphaMask() 方法,用于获取 alpha 通道的蒙板;
3. ImageExBase.Placeholder.cs
主要定义了 ImageExBase 类的占位符成员,具体如下:
- PlaceholderStretch - 获取或设置占位符的拉伸属性
- PlaceholderSource - 获取或设置占位符的图像源,ImageSource 类型,改变时会触发 PlaceholderSourceChanged(d, e) 方法;
4. ImageExBase.Source.cs
主要定义了 ImageExBase 类的图像源,除了定义 Source 外,还实现了以下几个方法:
① SetSource(source)
初始化 token 后,如果 source 为空,则进入 Unloaded 状态;否则进入 Loading 状态;判断 source 是 ImageSource 类型且有效,则赋值,然后进入 Loaded 状态;如果 source 是 Uri 类型但无效,或 ImageSource 类型无效,则进入 Failed 状态;如果 Uri 有效,判断为 httpUri 则进入 LoadImageAsync(uri) 方法,否则直接拼接 ms-appx:/// 资源格式加载给控件;
1 private async void SetSource(object source) 2 { if (!IsInitialized) { return;} this._tokenSource?.Cancel(); this._tokenSource = new CancellationTokenSource(); 3 4 AttachSource(null); if (source == null) 5 { 6 VisualStateManager.GoToState(this, UnloadedState, true); return; 7 } 8 9 VisualStateManager.GoToState(this, LoadingState, true); var imageSource = source as ImageSource; if (imageSource != null) 10 { 11 AttachSource(imageSource); 12 13 ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs()); 14 VisualStateManager.GoToState(this, LoadedState, true); return; 15 } 16 17 _uri = source as Uri; if (_uri == null) 18 { var url = source as string ?? source.ToString(); if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out _uri)) 19 { 20 VisualStateManager.GoToState(this, FailedState, true); return; 21 } 22 } 23 24 _isHttpSource = IsHttpUri(_uri); if (!_isHttpSource && !_uri.IsAbsoluteUri) 25 { 26 _uri = new Uri("ms-appx:///" + _uri.OriginalString.TrimStart('/')); 27 } await LoadImageAsync(_uri); 28 }
② LoadImageAsync(imageUri)
异步加载图片方法,在缓存可用且是 httpUri 时,从缓存里加载图片资源,根据 token 加载;然后加载对应资源后,进入 Loaded 状态;如果遇到一场,则进入 Failed 状态;如果是本地资源,或 http 资源不允许缓存,则直接实例化,不做缓存操作;
1 private async Task LoadImageAsync(Uri imageUri) 2 { if (_uri != null) 3 { if (IsCacheEnabled && _isHttpSource) 4 { try 5 { var propValues = new List<KeyValuePair<string, object>>(); // Add DecodePixelHeight, DecodePixelWidth, DecodePixelType into propValues ... var img = await ImageCache.Instance.GetFromCacheAsync(imageUri, true, _tokenSource.Token, propValues); lock (LockObj) 6 { if (_uri == imageUri) 7 { 8 AttachSource(img); 9 ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs()); 10 VisualStateManager.GoToState(this, LoadedState, true); 11 } 12 } 13 } catch (OperationCanceledException) 14 { // nothing to do as cancellation has been requested. } catch (Exception e) 15 { lock (LockObj) 16 { if (_uri == imageUri) 17 { 18 ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(e)); 19 VisualStateManager.GoToState(this, FailedState, true); 20 } 21 } 22 } 23 } else 24 { 25 AttachSource(new BitmapImage(_uri)); 26 } 27 } 28 }
5. ImageExBase.cs
类中定义了 ImageEx Template 定义字段对应的变量,包括 Image,Progress,CommonStates,Loading 等等;
此外在 AttachImageOpened,RemoveImageOpened 时设置附加对应的 handler;在 AttachImageFailed,RemoveImageFailed 时设置解除对应的 handler;分别触发对应的事件,并把 VisualState 设置为对应的状态;
6. RoundImageEx.xaml
我们看到,PlaceHolder 和 Image 都是用矩形来实现的,定义了 RadiusX 和 RadiusY 来实现圆角,Fill 使用 ImageBrush 来加载图像;实现圆角或圆形的图片控件;
另外需要注意的是,从 16299 开始,CornerRadius 属性也能适用于 ImageEx 控件,实现圆角矩形图片;如果系统低于 16299,不会引发异常,但是设置会不生效;
1 <Setter Property="Template"> 2 <Setter.Value> 3 <ControlTemplate TargetType="controls:RoundImageEx"> 4 <Grid Width="{TemplateBinding Width}" 5 Height="{TemplateBinding Height}"> 6 <Rectangle x:Name="PlaceholderRectangle" RadiusX="{TemplateBinding CornerRadius}" RadiusY="{TemplateBinding CornerRadius}"...> 7 <Rectangle.Fill> 8 <ImageBrush x:Name="PlaceholderImage" 9 ImageSource="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=PlaceholderSource}" 10 Stretch="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=PlaceholderStretch}" /> 11 </Rectangle.Fill> 12 </Rectangle> 13 <Rectangle x:Name="ImageRectangle" RadiusX="{TemplateBinding CornerRadius}" RadiusY="{TemplateBinding CornerRadius}"...> 14 <Rectangle.Fill> 15 <ImageBrush x:Name="Image" 16 Stretch="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Stretch}" /> 17 </Rectangle.Fill> 18 </Rectangle> 19 <ProgressRing Name="Progress" ... /> 20 21 <VisualStateManager.VisualStateGroups> 22 ... </VisualStateManager.VisualStateGroups> 23 </Grid> 24 </ControlTemplate> 25 </Setter.Value></Setter>
调用示例
我们创建了两个控件,ImageEx 和 RoundImageEx,如下图一是加载中的过渡状态,图二是正常显示的状态;如果 Source 设置有误,则会出现图三只显示 PlaceHolder 的情况,实际应用中,在图片加载失败时我们应该有对应的显示方法;
1 <controls:ImageEx Name="ImageExControl" 2 IsCacheEnabled="True" Width="200" Height="200" 3 PlaceholderSource="/assets/LockScreenLogo.scale-200.png" 4 Source="/assets/01.jpg"/><controls:RoundImageEx Name="RoundImageExControl" 5 IsCacheEnabled="True" Width="200" Height="200" Stretch="UniformToFill" 6 PlaceholderSource="/assets/01.jpg" 7 Source="/assets/02.jpg" 8 CornerRadius="999"/>
总结
到这里我们就把 UWP Community Toolkit 中的 ImageEx 控件的源代码实现过程和简单的调用示例讲解完成了,希望能对大家更好的理解和使用这个控件有所帮助。欢迎大家多多交流,谢谢!