AsyncImage 是一個封裝完善,使用簡便,功能齊全的WPF圖片控件,比直接使用Image相對來講更加方便,但它的內部仍然使用Image承載圖像,只不過在其基礎上進行了一次完善成熟的封裝git
AsyncImage解決了如下問題
1) 異步加載及等待提示
2) 緩存
3) 支持讀取多種形式的圖片路徑 (Local,Http,Resource)
4) 根據文件頭識別準確的圖片格式
5) 靜態圖支持設置解碼大小
6) 支持GIFgithub
AsyncImage的工做流程緩存
開始建立網絡
首先聲明一個自定義控件異步
public class AsyncImage : Control { static AsyncImage() { DefaultStyleKeyProperty.OverrideMetadata(typeof(AsyncImage), new FrameworkPropertyMetadata(typeof(AsyncImage))); ImageCacheList = new ConcurrentDictionary<string, ImageSource>(); //初始化靜態圖緩存字典 GifImageCacheList = new ConcurrentDictionary<string, ObjectAnimationUsingKeyFrames>(); //初始化gif緩存字典 } }
聲明成員async
#region DependencyProperty public static readonly DependencyProperty DecodePixelWidthProperty = DependencyProperty.Register("DecodePixelWidth", typeof(double), typeof(AsyncImage), new PropertyMetadata(0.0)); public static readonly DependencyProperty LoadingTextProperty = DependencyProperty.Register("LoadingText", typeof(string), typeof(AsyncImage), new PropertyMetadata("Loading")); public static readonly DependencyProperty IsLoadingProperty = DependencyProperty.Register("IsLoading", typeof(bool), typeof(AsyncImage), new PropertyMetadata(false)); public static readonly DependencyProperty ImageSourceProperty = DependencyProperty.Register("ImageSource", typeof(ImageSource), typeof(AsyncImage)); public static readonly DependencyProperty UrlSourceProperty = DependencyProperty.Register("UrlSource", typeof(string), typeof(AsyncImage), new PropertyMetadata(string.Empty, new PropertyChangedCallback((s, e) => { var asyncImg = s as AsyncImage; if (asyncImg.LoadEventFlag) { Console.WriteLine("Load By UrlSourceProperty Changed"); asyncImg.Load(); } }))); public static readonly DependencyProperty IsCacheProperty = DependencyProperty.Register("IsCache", typeof(bool), typeof(AsyncImage), new PropertyMetadata(true)); public static readonly DependencyProperty StretchProperty = DependencyProperty.Register("Stretch", typeof(Stretch), typeof(AsyncImage), new PropertyMetadata(Stretch.Uniform)); public static readonly DependencyProperty CacheGroupProperty = DependencyProperty.Register("CacheGroup", typeof(string), typeof(AsyncImage), new PropertyMetadata("AsyncImage_Default")); #endregion #region Property /// <summary> /// 本地路徑正則 /// </summary> private const string LocalRegex = @"^([C-J]):\\([^:&]+\\)*([^:&]+).(jpg|jpeg|png|gif)$"; /// <summary> /// 網絡路徑正則 /// </summary> private const string HttpRegex = @"^(https|http):\/\/[^*+@!]+$"; private Image _image; /// <summary> /// 是否容許加載圖像 /// </summary> private bool LoadEventFlag; /// <summary> /// 靜態圖緩存 /// </summary> private static IDictionary<string, ImageSource> ImageCacheList; /// <summary> /// 動態圖緩存 /// </summary> private static IDictionary<string, ObjectAnimationUsingKeyFrames> GifImageCacheList; /// <summary> /// 動畫播放控制類 /// </summary> private ImageAnimationController gifController; /// <summary> /// 解碼寬度 /// </summary> public double DecodePixelWidth { get { return (double)GetValue(DecodePixelWidthProperty); } set { SetValue(DecodePixelWidthProperty, value); } } /// <summary> /// 異步加載時的文字提醒 /// </summary> public string LoadingText { get { return GetValue(LoadingTextProperty) as string; } set { SetValue(LoadingTextProperty, value); } } /// <summary> /// 加載狀態 /// </summary> public bool IsLoading { get { return (bool)GetValue(IsLoadingProperty); } set { SetValue(IsLoadingProperty, value); } } /// <summary> /// 圖片路徑 /// </summary> public string UrlSource { get { return GetValue(UrlSourceProperty) as string; } set { SetValue(UrlSourceProperty, value); } } /// <summary> /// 圖像源 /// </summary> public ImageSource ImageSource { get { return GetValue(ImageSourceProperty) as ImageSource; } set { SetValue(ImageSourceProperty, value); } } /// <summary> /// 是否啓用緩存 /// </summary> public bool IsCache { get { return (bool)GetValue(IsCacheProperty); } set { SetValue(IsCacheProperty, value); } } /// <summary> /// 圖像填充類型 /// </summary> public Stretch Stretch { get { return (Stretch)GetValue(StretchProperty); } set { SetValue(StretchProperty, value); } } /// <summary> /// 緩存分組標識 /// </summary> public string CacheGroup { get { return GetValue(CacheGroupProperty) as string; } set { SetValue(CacheGroupProperty, value); } } #endregion
須要注意的是,當UrlSource發生改變時,也許AsyncImage自己並未加載完成,這個時候獲取模板中的Image對象是獲取不到的,因此要在其PropertyChanged事件中判斷一下load狀態,已經load過才能觸發加載,不然就等待控件的load事件執行以後再加載ide
public static readonly DependencyProperty UrlSourceProperty = DependencyProperty.Register("UrlSource", typeof(string), typeof(AsyncImage), new PropertyMetadata(string.Empty, new PropertyChangedCallback((s, e) => { var asyncImg = s as AsyncImage; if (asyncImg.LoadEventFlag) //判斷控件自身加載狀態 { Console.WriteLine("Load By UrlSourceProperty Changed"); asyncImg.Load(); } }))); private void AsyncImage_Loaded(object sender, RoutedEventArgs e) { _image = this.GetTemplateChild("image") as Image; //獲取模板中的Image Console.WriteLine("Load By LoadedEvent"); this.Load(); this.LoadEventFlag = true; //設置控件加載狀態 }
private void Load() { if (_image == null) return; Reset(); var url = this.UrlSource; if (!string.IsNullOrEmpty(url)) { var pixelWidth = (int)this.DecodePixelWidth; var isCache = this.IsCache; var cacheKey = string.Format("{0}_{1}", CacheGroup, url); this.IsLoading = !ImageCacheList.ContainsKey(cacheKey) && !GifImageCacheList.ContainsKey(cacheKey); Task.Factory.StartNew(() => { #region 讀取緩存 if (ImageCacheList.ContainsKey(cacheKey)) { this.SetSource(ImageCacheList[cacheKey]); return; } else if (GifImageCacheList.ContainsKey(cacheKey)) { this.Dispatcher.BeginInvoke((Action)delegate { var animation = GifImageCacheList[cacheKey]; PlayGif(animation); }); return; } #endregion #region 解析路徑類型 var pathType = ValidatePathType(url); Console.WriteLine(pathType); if (pathType == PathType.Invalid) { Console.WriteLine("invalid path"); return; } #endregion #region 讀取圖片字節 byte[] imgBytes = null; Stopwatch sw = new Stopwatch(); sw.Start(); if (pathType == PathType.Local) imgBytes = LoadFromLocal(url); else if (pathType == PathType.Http) imgBytes = LoadFromHttp(url); else if (pathType == PathType.Resources) imgBytes = LoadFromApplicationResource(url); sw.Stop(); Console.WriteLine("read time : {0}", sw.ElapsedMilliseconds); if (imgBytes == null) { Console.WriteLine("imgBytes is null,can't load the image"); return; } #endregion #region 讀取文件類型 var imgType = GetImageType(imgBytes); if (imgType == ImageType.Invalid) { imgBytes = null; Console.WriteLine("無效的圖片文件"); return; } Console.WriteLine(imgType); #endregion #region 加載圖像 if (imgType != ImageType.Gif) { //加載靜態圖像 var imgSource = LoadStaticImage(cacheKey, imgBytes, pixelWidth, isCache); this.SetSource(imgSource); } else { //加載gif圖像 this.Dispatcher.BeginInvoke((Action)delegate { var animation = LoadGifImageAnimation(cacheKey, imgBytes, isCache); PlayGif(animation); }); } #endregion }).ContinueWith(r => { this.Dispatcher.BeginInvoke((Action)delegate { this.IsLoading = false; }); }); } }
判斷路徑,判斷文件格式,讀取圖片字節動畫
public enum PathType { Invalid = 0, Local = 1, Http = 2, Resources = 3 } public enum ImageType { Invalid = 0, Gif = 7173, Jpg = 255216, Png = 13780, Bmp = 6677 } /// <summary> /// 驗證路徑類型 /// </summary> /// <param name="path"></param> /// <returns></returns> private PathType ValidatePathType(string path) { if (path.StartsWith("pack://")) return PathType.Resources; else if (Regex.IsMatch(path, AsyncImage.LocalRegex, RegexOptions.IgnoreCase)) return PathType.Local; else if (Regex.IsMatch(path, AsyncImage.HttpRegex, RegexOptions.IgnoreCase)) return PathType.Http; else return PathType.Invalid; } /// <summary> /// 根據文件頭判斷格式圖片 /// </summary> /// <param name="bytes"></param> /// <returns></returns> private ImageType GetImageType(byte[] bytes) { var type = ImageType.Invalid; try { var fileHead = Convert.ToInt32($"{bytes[0]}{bytes[1]}"); if (!Enum.IsDefined(typeof(ImageType), fileHead)) { type = ImageType.Invalid; Console.WriteLine($"獲取圖片類型失敗 fileHead:{fileHead}"); } else { type = (ImageType)fileHead; } } catch (Exception ex) { type = ImageType.Invalid; Console.WriteLine($"獲取圖片類型失敗 {ex.Message}"); } return type; } private byte[] LoadFromHttp(string url) { try { using (WebClient wc = new WebClient() { Proxy = null }) { return wc.DownloadData(url); } } catch (Exception ex) { Console.WriteLine("network error:{0} url:{1}", ex.Message, url); } return null; } private byte[] LoadFromLocal(string path) { if (!System.IO.File.Exists(path)) { return null; } try { return System.IO.File.ReadAllBytes(path); } catch (Exception ex) { Console.WriteLine("Read Local Failed : {0}", ex.Message); return null; } } private byte[] LoadFromApplicationResource(string path) { try { StreamResourceInfo streamInfo = Application.GetResourceStream(new Uri(path, UriKind.RelativeOrAbsolute)); if (streamInfo.Stream.CanRead) { using (streamInfo.Stream) { var bytes = new byte[streamInfo.Stream.Length]; streamInfo.Stream.Read(bytes, 0, bytes.Length); return bytes; } } } catch (Exception ex) { Console.WriteLine("Read Resource Failed : {0}", ex.Message); return null; } return null; }
加載靜態圖this
/// <summary> /// 加載靜態圖像 /// </summary> /// <param name="cacheKey"></param> /// <param name="imgBytes"></param> /// <param name="pixelWidth"></param> /// <param name="isCache"></param> /// <returns></returns> private ImageSource LoadStaticImage(string cacheKey, byte[] imgBytes, int pixelWidth, bool isCache) { if (ImageCacheList.ContainsKey(cacheKey)) return ImageCacheList[cacheKey]; var bit = new BitmapImage() { CacheOption = BitmapCacheOption.OnLoad }; bit.BeginInit(); if (pixelWidth != 0) { bit.DecodePixelWidth = pixelWidth; //設置解碼大小 } bit.StreamSource = new System.IO.MemoryStream(imgBytes); bit.EndInit(); bit.Freeze(); try { if (isCache && !ImageCacheList.ContainsKey(cacheKey)) ImageCacheList.Add(cacheKey, bit); } catch (Exception ex) { Console.WriteLine(ex.Message); return bit; } return bit; }
關於GIF解析url
博客園上的周銀輝老師也作過Image支持GIF的功能,但我我的認爲他的解析GIF部分代碼不太友好,因爲直接操做文件字節,致使若是閱讀者沒有研究過gif的文件格式,將晦澀難懂。幾經周折我找到github上一個大神寫的成熟的WPF播放GIF項目,源碼參考 https://github.com/XamlAnimatedGif/WpfAnimatedGif
解析GIF的核心代碼,從圖片幀的元數據中使用路徑表達式獲取當前幀的詳細信息 (大小/邊距/顯示時長/顯示方式)
/// <summary> /// 解析幀詳細信息 /// </summary> /// <param name="frame">當前幀</param> /// <returns></returns> private static FrameMetadata GetFrameMetadata(BitmapFrame frame) { var metadata = (BitmapMetadata)frame.Metadata; var delay = TimeSpan.FromMilliseconds(100); var metadataDelay = metadata.GetQueryOrDefault("/grctlext/Delay", 10); //顯示時長 if (metadataDelay != 0) delay = TimeSpan.FromMilliseconds(metadataDelay * 10); var disposalMethod = (FrameDisposalMethod)metadata.GetQueryOrDefault("/grctlext/Disposal", 0); //顯示方式 var frameMetadata = new FrameMetadata { Left = metadata.GetQueryOrDefault("/imgdesc/Left", 0), Top = metadata.GetQueryOrDefault("/imgdesc/Top", 0), Width = metadata.GetQueryOrDefault("/imgdesc/Width", frame.PixelWidth), Height = metadata.GetQueryOrDefault("/imgdesc/Height", frame.PixelHeight), Delay = delay, DisposalMethod = disposalMethod }; return frameMetadata; }
建立WPF動畫播放對象
/// <summary> /// 加載Gif圖像動畫 /// </summary> /// <param name="cacheKey"></param> /// <param name="imgBytes"></param> /// <param name="pixelWidth"></param> /// <param name="isCache"></param> /// <returns></returns> private ObjectAnimationUsingKeyFrames LoadGifImageAnimation(string cacheKey, byte[] imgBytes, bool isCache) { var gifInfo = GifParser.Parse(imgBytes); var animation = new ObjectAnimationUsingKeyFrames(); foreach (var frame in gifInfo.FrameList) { var keyFrame = new DiscreteObjectKeyFrame(frame.Source, frame.Delay); animation.KeyFrames.Add(keyFrame); } animation.Duration = gifInfo.TotalDelay; animation.RepeatBehavior = RepeatBehavior.Forever; //animation.RepeatBehavior = new RepeatBehavior(3); if (isCache && !GifImageCacheList.ContainsKey(cacheKey)) { GifImageCacheList.Add(cacheKey, animation); } return animation; }
GIF動畫的播放
建立動畫控制器ImageAnimationController,使用動畫時鐘控制器AnimationClock ,爲控制器指定須要做用的控件屬性
private readonly Image _image; private readonly ObjectAnimationUsingKeyFrames _animation; private readonly AnimationClock _clock; private readonly ClockController _clockController; public ImageAnimationController(Image image, ObjectAnimationUsingKeyFrames animation, bool autoStart) { _image = image; try { _animation = animation; //_animation.Completed += AnimationCompleted; _clock = _animation.CreateClock(); _clockController = _clock.Controller; _sourceDescriptor.AddValueChanged(image, ImageSourceChanged); // ReSharper disable once PossibleNullReferenceException _clockController.Pause(); //暫停動畫 _image.ApplyAnimationClock(Image.SourceProperty, _clock); //將動畫做用於該控件的指定屬性 if (autoStart) _clockController.Resume(); //播放動畫 } catch (Exception) { } }
定義外觀
<Style TargetType="{x:Type local:AsyncImage}"> <Setter Property="HorizontalAlignment" Value="Center"/> <Setter Property="VerticalAlignment" Value="Center"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:AsyncImage}"> <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" HorizontalAlignment="{TemplateBinding HorizontalAlignment}" VerticalAlignment="{TemplateBinding VerticalAlignment}"> <Grid> <Image x:Name="image" Stretch="{TemplateBinding Stretch}" RenderOptions.BitmapScalingMode="HighQuality"/> <TextBlock Text="{TemplateBinding LoadingText}" FontSize="{TemplateBinding FontSize}" FontFamily="{TemplateBinding FontFamily}" FontWeight="{TemplateBinding FontWeight}" Foreground="{TemplateBinding Foreground}" HorizontalAlignment="Center" VerticalAlignment="Center" x:Name="txtLoading"/> </Grid> </Border> <ControlTemplate.Triggers> <Trigger Property="IsLoading" Value="False"> <Setter Property="Visibility" Value="Collapsed" TargetName="txtLoading"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style>
調用示例
<local:AsyncImage UrlSource="{Binding Url}"/> <local:AsyncImage UrlSource="{Binding Url}" IsCache="False"/> <local:AsyncImage UrlSource="{Binding Url}" DecodePixelWidth="50" /> <local:AsyncImage UrlSource="{Binding Url}" LoadingText="正在加載圖像請稍後"/>