原文地址:http://timheuer.com/blog/archive/2015/05/06/making-circular-images-in-xaml-easily.aspx
前阵子似乎一些比较酷的程序开始使用圆形头像来取代之前方形或者圆角边的显示方式了。我(原文作者。下文中如果没特别提到,均指原文作者)在两年前注意到一些 App 开始这样做的时候,做出了一个偏激的发言:
看看吧,程序里会越来越多圆形的头像了,方形的将不会再有了
——Tim Heuer(@timheuer) 2013 年 5 月 23 日
现在,这似乎变成一种流行的趋势了,因为大家都是这么做,我想使用 XAML 来开发的程序猿们都知道怎么实现的吧。然而,我却仍然看到有很多人在问这个问题,一些人尝试去实现,却变得更加复杂。因此,我想我应该发表一篇博客来阐述这个问题。我曾经看到过愚蠢的人们将原来的图片通过算法来裁剪,然后存储到硬盘,再显示给他们的用户看。这是完全没必要的,其实我们很简单就可以做到这种显示效果。
<Ellipse Width="250" Height="250"> <Ellipse.Fill> <ImageBrush ImageSource="ms-appx:///highfive.jpg" /> </Ellipse.Fill> </Ellipse>
你会看见,第三行我们使用一个 ImageBrush 来填充一个 Ellipse。使用一个 Ellipse 可以帮助我们得到一个精确的圆形裁剪,并且不会出现毛边的状况。上面这段代码会显示成如下效果:
现在,尽管这是 ok 的。但是,在 Windows 8.1 中,使用 ImageBrush 将不会得到自动根据需要渲染大小来进行解码的功能。
注意:自动根据需要渲染大小来解码的这个功能是指即使一个图片是比较大的,但只解码需要渲染的大小。所以,如果你有一个 2000 像素乘以 2000 像素大小的图片,但仅仅需要渲染成 100 像素乘以 100 像素大小的时候,我们将图片解码为 100 像素乘以 100 像素的话,就可以节省大量的内存了。
对于绝大部分的程序来说,可能已经存放好符合我们所需要的大小了。这是没问题的。然而,对于社交程序或者其它你不知道图片来源的程序,它们上传到服务器的时候是不会对图片调整大小的。那么,你将会想通过解码到指定的大小来节省你的内存。这是很容易在 XAML 中做到的,仅仅需要复杂一点的语法。将上面那段 XAML 修改成这样子:
<Ellipse Width="250" Height="250"> <Ellipse.Fill> <ImageBrush> <ImageBrush.ImageSource> <BitmapImage DecodePixelHeight="250" DecodePixelWidth="250" UriSource="ms-appx:///highfive.jpg" /> </ImageBrush.ImageSource> </ImageBrush> </Ellipse.Fill> </Ellipse>
不需要很大的改动,在第 5 行使用 DecodePixelHeight 和 DecodePixelWidth 来告诉系统框架解码的大小。渲染出来的效果跟上面是一样的。当你需要显示比原图小的时候,这个技巧是十分有效的,而不是去使用其它奇淫技巧。
所以你们快点去帮助那些为了显示圆形头像而陷入发狂状态的码农们!希望能够帮到他们。
译者(h82258652)注:本文为意译,尽可能将原文作者想告诉大家的东西翻译过来给大家。如果有任何疑难,请查阅原文。谢谢!
下面进入译者我的加工部分:
一般来说,我们还是比较习惯做成一个控件的,总不可能每次用到圆形图像的话,去写上面这么一大堆。下面我们就来动手干!
在 Visual Studio 中新建一个用户控件(UserControl),我们命名为 CircleImage。
然后在后台代码中定义一个依赖属性 Source,表示图片的源。由于 BitmapImage 的 UriSource 是 Uri 类型的,因此我们的 Source 属性也是 Uri 类型。Source 变化时,我们设置到 XAML 中的 BitmapImage 的 UriSource 属性上去。
public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(nameof(Source), typeof(Uri), typeof(CircleImage), new PropertyMetadata(null, SourceChanged)); private static void SourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var obj = (CircleImage)d; var value = (Uri)e.NewValue; obj.bitmapImage.UriSource = value; } public Uri Source { get { return (Uri)GetValue(SourceProperty); } set { SetValue(SourceProperty, value); } }
然后开始编写前台 XAML:
<UserControl x:Class="MyApp.CircleImage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:MyApp" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400"> <Ellipse> <Ellipse.Fill> <ImageBrush> <ImageBrush.ImageSource> <BitmapImage x:Name="bitmapImage" DecodePixelWidth="1" DecodePixelHeight="1" /> </ImageBrush.ImageSource> </ImageBrush> </Ellipse.Fill> </Ellipse> </UserControl>
将 BitmapImage 命名为 bitmapImage,给上面的后台 cs 代码使用。
这里可能你会奇怪,为什么我将解码的大小写成了 1?这是因为,如果解码大小写成小于 1 的整数的话,就等于没有自动根据渲染大小来解码的功能了,那就跟一开始原文作者的效果一样。所以这里我先写成 1,运行的时候再根据控件的大小来动态调整。
那么既然要动态调整,那么我们必须得完善后台代码了,添加一些代码上去,修改成这样:
public sealed partial class CircleImage : UserControl { public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(nameof(Source), typeof(Uri), typeof(CircleImage), new PropertyMetadata(null, SourceChanged)); private static void SourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var obj = (CircleImage)d; var value = (Uri)e.NewValue; obj.bitmapImage.UriSource = value; } public Uri Source { get { return (Uri)GetValue(SourceProperty); } set { SetValue(SourceProperty, value); } } public CircleImage() { this.InitializeComponent(); this.SizeChanged += CircleImage_SizeChanged;// 监听控件大小发生变化。 } private void ReSize() { // 计算新的解码大小,向上取整。 int width = (int)Math.Ceiling(this.ActualWidth); int height = (int)Math.Ceiling(this.ActualHeight); // 确保解码大小必须大于 0,因为上面的结果可能为 0。 bitmapImage.DecodePixelWidth = Math.Max(width, 1); bitmapImage.DecodePixelHeight = Math.Max(height, 1); // 让 BitmapImage 重新渲染。 var temp = bitmapImage.UriSource; bitmapImage.UriSource = null; bitmapImage.UriSource = temp; } private void CircleImage_SizeChanged(object sender, SizeChangedEventArgs e) { ReSize(); } }
这样我们就完成了这个控件了,接下来我们来测试下究竟这个东西威力有多大。
测试:
测试图片使用我博客的背景图片,足够大的了,1920*1080,相信应该会吃掉不少内存^-^。
图片地址:http://images.cnblogs.com/cnblogs_com/h82258652/693238/o_wallpaper_summer2013_1920X1080.jpg
测试程序代码:
前台 XAML 代码:
<Page x:Class="MyApp.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:MyApp" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <StackPanel Grid.Row="0" HorizontalAlignment="Center" Orientation="Horizontal"> <Button Content="加载旧版" Click="BtnOld_Click" /> <Button Content="加载新版" Click="BtnNew_Click" /> </StackPanel> <GridView Grid.Row="1" x:Name="gvwBigMemory"> <GridView.ItemTemplate> <DataTemplate> <Ellipse Width="100" Height="100"> <Ellipse.Fill> <ImageBrush ImageSource="http://images.cnblogs.com/cnblogs_com/h82258652/693238/o_wallpaper_summer2013_1920X1080.jpg" /> </Ellipse.Fill> </Ellipse> </DataTemplate> </GridView.ItemTemplate> </GridView> <GridView Grid.Row="2" x:Name="gvwMinMemory"> <GridView.ItemTemplate> <DataTemplate> <local:CircleImage Width="100" Height="100" Source="http://images.cnblogs.com/cnblogs_com/h82258652/693238/o_wallpaper_summer2013_1920X1080.jpg" /> </DataTemplate> </GridView.ItemTemplate> </GridView> </Grid> </Page>
都放在 GridView 里面,大小都设定为 100*100。
后台代码:
public sealed partial class MainPage : Page { public MainPage() { this.InitializeComponent(); } private void BtnOld_Click(object sender, RoutedEventArgs e) { gvwBigMemory.ItemsSource = Enumerable.Range(1, 100); } private void BtnNew_Click(object sender, RoutedEventArgs e) { gvwMinMemory.ItemsSource = Enumerable.Range(1, 100); } }
很简单,就是让 GridView 加载 100 个。
好,走起!
初始运行占用 40.6 MB 内存,当然这个值可能下次运行就不一样了,多少会有点波动。
接下来我们加载旧版:
令人吃惊,一瞬间就跑到 250.6 MB 内存了。
那我们再来看看新的版本,记得先将上面这个关掉,重新打开,否则影响结果。
新版:
44.6 MB!基本没发生任何的变化。可见威力很强大,说明我们的代码起作用了。
结语:
可以见到这点小小的优化能带来多大的影响。另外由于 GridView 默认使用了虚拟化,所以实际上并没有加载到 100 个,但是仍然可以见到旧版的内存占用十分厉害, 所以根本没法想象真真正正加载 100 个的时候有多壮观。小小的细节可能会引起巨大的变化,考虑到还有众多 512 MB 内存的 Windows Phone 用户,这点小小的细节仍然很有必要去做的。