使用传送带动画面板进行 Google 图片搜索
在一个网页中执行四种不同的谷歌图片搜索,每个搜索结果都以“传送带”形式显示和管理。

注意
此代码示例需要以下安装才能运行和编译
因为它基于这两个来源的特性!
引言
有一段时间,我一直想编写一个传送带显示。
我“玩过”许多种“多元素”显示,发现其中许多都有太多与 UI 相关的故障。
传送带显示具有一些“内置”优点
- 很好地适应矩形定义的区域
- 元素具有不同的大小 - 接近、更相关的元素更大,因此是信息相关性导向的
- 用户对项目数量有绝对的概念
- 用户对元素有持续的定位
- 物品沿“传送带”的移动相对优雅(在我对不同多物品 UI 显示的实验中,我注意到如果一个或多个物品移动速度快于其余物品,它实际上会“刺激眼睛”)
最终促使我坐下来编写这个显示器有两件事
- 一篇非常好的文章 C# 5.0 和 WPF 中的图像搜索客户端
- 我自己在用谷歌图片搜索平铺显示时的沮丧
所以我坐下来编写了一个“传送带”显示动画面板...
背景
为什么是面板(甚至更重要的是——为什么是动画)?
实际上,所有 WPF/Silverlight 开箱即用的面板都是“静态内容排列器”有一些很好的理由。主要是,这使得它们保持基本,从而通用。
促使我推翻这个惯例的原因如下
- 为什么不呢?没有人说面板不应该具有动画效果。
- 在这种特殊情况下 - 如果传送带没有实际转动,将面板的子级沿传送带路径散布就没有意义。
- 在这种特殊情况下 - 传送带的转动意味着与面板子级顺序(
zorder
)的密切交互,该顺序在面板内部进行管理。 - 封装 - 一个独立的面板,具有内置功能,无需外部代码操作。
- 我个人希望编写一个可动画的自定义传送带面板。
可控可绑定面板
我遇到的第一个障碍是“绑定面板”控制问题,在 **Dr. WPF** 的精彩文章 概念子级:WPF 中一个强大的新概念 中解释和解决。
简而言之:当您希望面板显示绑定源中的项目时,您通常将面板设置为某种 ItemsControl
(ListBox
* 或 ItemsControl
)的 ItemsPanel
属性,然后将 ItemsControl
的 ItemsSource
属性绑定到您的逻辑项目列表(在使用 **MVVM** 模式时通常在 **ViewModel** 中)。
然后 ItemsControl
(理所当然地)对面板的子级拥有**独占**控制权,因此禁止通过面板(甚至在面板内部)操作子级!
虽然 **Dr. WPF** 使用了一些巧妙的“技巧”来克服这个障碍并“控制不可控”,但我采取了完全不同的方法...
从不同的角度看问题,我们基本上想要的是将面板的子级绑定到逻辑项目源。
我们被迫使用 ItemsControl
作为逻辑项目源和面板子级之间的**中介**(因此失去了对面板子级的控制)。
如果我们能有一个不那么“控制狂”的**中介**,或者甚至是一个面板拥有一个“ItemsSource
”属性,那该有多好?
嗯,我们可以,使用 AttachedProperty
!
<ConvbeltPanel:AnimConvbeltPanel x:Name="AnimConvbeltPanel"
ConvbeltPanel:PanelChildrenBinder.ItemsSource ="{Binding Path=Images}"
ConvbeltPanel:PanelChildrenBinder.ItemTemplate="{StaticResource ImageDataTemplate}"
...
如您所见,我还添加了“ItemTemplate
”支持...
我对“
ListBox
”这个名称有异议,因为“Box”一词暗示了视觉品质,这与 WPF 关于控件“无外观”的核心概念之一相矛盾。这个控件更好的名称是“SelectableItemsControl
”吗?
跨域问题
正如本文名称所暗示的,应用程序的核心功能是利用谷歌图片搜索 API,以传送带形式显示来自**不同来源(域)**的图片。
Silverlight 中禁止跨域数据,**除非**
- 数据是从已批准的 WCF 服务获取的(服务具有有效的用户访问策略文件)
- Silverlight 应用程序作为 Out-Of-Browser (OOB) 应用程序运行。因此,它不受 Silverlight 的跨域限制。
在此示例中,我尝试结合两种选项
启动时,应用程序(Web 浏览器中的普通 Silverlight)检查本地 WCF 是否存在。这个自托管的基于控制台的服务充当 Silverlight 和实际数据源之间的代理。就像任何其他 EXE 一样,它拥有跨域数据访问权限,并且由于它发布运行时生成的 User-Access-policy 文件,因此它是 Silverlight 的有效数据源。
我必须承认我喜欢这种 WCF(自托管基于控制台的),我将其视为老式手写、自包含、基于套接字的 TCP 服务器与传统依赖 IIS 的 WCF 之间的“缺失环节”。
很高兴发现已经有人编写了这样的服务 - 为自托管 TCP 服务上的 SL 应用程序启用跨域调用,所以我需要做的就是添加 GetImageBytes/s
方法。
注意一点 - 这个服务实际上会“动态”创建一个“_clientaccesspolicy.xml_”文件,并将其返回给端口 80 上的调用(WCF 客户端在此端口检查此文件),这意味着 IIS 不应该运行!!!因为它使用相同的端口(默认 HTTP 端口)。
回到这个示例,正如我之前所说,在启动时,应用程序检查本地 WCF 是否可用(通过对服务进行“Timeouted
”调用)。
如果可用 - 应用程序将此服务用作跨域代理 - 应用程序使用 URL/s 参数调用服务上的 GetImageBytes/s
,该服务将下载字节数组并将其返回给应用程序。
如果不可用(对服务的回音调用超时)- 应用程序会提示用户两个选项
- 将此 Silverlight 应用程序安装到本地机器并作为具有跨域权限的 OOB 运行。
- 下载并运行本地 WCF,然后刷新当前 Silverlight 的网页。
Using the Code
面板
然后,当你开始迭代 2(这是构建迭代的开始)时,你可能想要复制测试用例并将它们重新分类到迭代 2。这还允许对测试用例进行粒度跟踪,并允许你说某个测试用例在一个迭代中是准备好的,但在另一个迭代中不是。同样,如何做到这一点取决于你以及你希望如何报告。 “场景”部分提供了更多细节。
我使用高中几何定义了适合面板区域的传送带路径。
我通过定义两个连接外弧的圆,然后将整个图形倾斜到所需角度来完成,像这样
接下来,以非线性方式定义沿此路径的位置(我希望物品在“更近”(更大)时更分散,这样物品信息对用户更相关时,就不会被其他物品遮挡太多)。
* 由于这部分(“构建”)在我的优先级列表中不是很高,所以这段代码远非完美...
动画
出于性能原因,所有定位/大小调整都使用 RenderTransform
的 Translate
和 Scale
完成,从而省略了许多耗时的面板及其子项的测量/排列过程。
所有子项最初都放置在 (0,0) 位置,然后通过更改其 RenderTransform
的值进行定位/移动(动画)。
我测试了各种传送带转动的方法,发现“位置到位置”动画方法提供了最佳性能。
需要注意的一点是 Zorder 方面
尽管每个面板都有一个 Arrange
覆盖方法,但实际决定面板子级 zorder
的是它们在面板内部 Children
集合* 中设置的实际顺序。
我发现“
Arrange
”这个术语令人困惑,因为它与子项的 Z 排序无关。更好的名称是“Positioning2D
”吗?
问题在于:项目沿传送带放置的顺序与它们在面板子级集合中存储的顺序不同。
当传送带转动时,应同时考虑“两种”顺序。
以下插图可能会澄清这一点
蓝色='自然顺序'
红色=ZOrder
因此,当将子级从一个位置动画到下一个(或上一个...)时
- 下一个位置并不总是子集合顺序上的相邻位置。
- 应该发生一些额外的位置切换。
总而言之,转动传送带(单步)需要两个步骤
- 重新排列子集合,使
zorder
与新指定的职位排列匹配。 - 通过渲染变换值将元素动画到指定位置和大小。
动画管理
使用“位置到位置”动画方法意味着传送带的每一次转动(单步)都需要发生多个动画(故事板 - 每个子项一个)。
有几个因素需要处理
- 等待当前转弯步骤中的所有动画结束,然后再开始下一个单步动画。
- 加速 - 用户“请求”在**同一方向**上转动传送带,同时仍在转动。
- 这还有一个额外的含义 - 不仅下一个“单步动画”应该更快,而且当前运行的动画也应该“更新”它们的速度,这样用户将立即收到其加速请求的反馈! - 停止 - 用户在仍在转动时“请求”以**相反方向**转动传送带。
- 减速 - 没有用户“请求”转动传送带。
- 转动停止 - 随着转动减速,当低于预定义的某个速度时 - 转动应该停止。
我不会深入探讨每个因素的实际代码机制,而是将指出它们都涉及的子程序
public void DoSpinAnimation(int dir)
...
private void UpdateNewDuration(Storyboard sb, TimeSpan ts)
...
private async void DoChildrenSingleStepAnim(int dir)
...
private void AnimateChildrenToPos(int dir)
...
void SingleElementAnimation_Completed(object sender, EventArgs e)
“小伎俩”
- 谁在最上面的问题...
如右上角的图像(蓝色数字)所示 - 元素 #8 在元素 #9 的上方,但当传送带顺时针转动时,元素 #9 应该在元素 #8 的上方。由于zorder
是首先更新的(在动画之前),将发生不必要的翻转(逆时针方向也是如此)。
为了解决这个问题,这两个“有问题”的元素被分开,这样它们就不会重叠。
* 同样的问题也出现在元素 #1 和 #16 上 - 在那里,我没有“玩”这个把戏,因为它几乎不明显(而且我很懒...)
- 当元素沿弯曲路径从一个位置动画到另一个位置时,它们实际上是直线移动的,虽然位置相对接近,但几乎不明显,但是...
作为前一个“小伎俩”的结果,#8 和 #9 元素应该沿着长长的弯曲距离移动到下一个位置。
一种解决方案是将这种特殊情况动画分解为沿曲线的多个小分位置(关键帧)。
另一种解决方案是使用单个**缓动关键帧**为这种特殊动画提供弯曲路径,并通过操纵缓动函数对抗缓动模式属性来实现弯曲路径。... SineEase seX = new SineEase(); seX.EasingMode = dir < 0 ? EasingMode.EaseOut : EasingMode.EaseIn; edkfX.EasingFunction = seX; ... ... SineEase seY = new SineEase(); seY.EasingMode = dir < 0 ? EasingMode.EaseIn : EasingMode.EaseOut; edkfY.EasingFunction = seY; ...
GoogleSearchConvBeltUserControl
顾名思义,此用户控件用于搜索(使用 Google 图片搜索 API)和显示(使用传送带面板)图片。
实际工作在其 ViewModel
中完成
在 SearchCommand
上,从 Google 搜索 API 获取 ImageResults
列表。
GimageSearchClient client = new GimageSearchClient();
Task<IList<IImageResult>> t = Task<IList<IImageResult>>.Factory.FromAsync
(client.BeginSearch, client.EndSearch, _SearchText, MaxNumOfResultsSelected, null);
await t;
然后根据应用程序的配置(参见“**跨域问题**”部分),生成图像对象。
if (IsImageBytesByUrlServiceAlive)
{
...
// set Image-Source to the Bytes received from service
...
}
else
{
// when running as OOB : Image-Source can be URL string, so,
// no additional work is needed.
Images = new ObservableCollection<MyImageRes>
(t.Result.Select(ir => new MyImageRes(this, (IImageResult)ir)));
}
面板的 Items-Source 绑定到这些图像(参见“**可控可绑定面板**”部分),因此它们以传送带的形式显示和动画。
此外,ViewModel
会更新其 View
的 Fetch
/Search
状态,以便视图可以设置适当的 **Visual-State**。
此 UserControl
还将来自传送带面板的“左键单击图像”信息**向上**传递到应用程序主窗口,以获得窗口范围的响应。
MainPage
包含四个平铺(有重叠)的传送带图片搜索 UserControl
。
如前所述,如果在浏览器中运行 - 检查代理服务可用性,并相应地重新排列。
从任何 UserControl
接收到 ShowLargeImage
事件时 - 动画图像以显示其大尺寸、高分辨率版本。
测试
构建解决方案(_Release_)
运行 - _...\GoogleSearchConvBeltPanel\DemoProject\Bin\Release\DemoProjectTestPage.html_。
系统将提示您显示与“跨域”相关的消息...
运行 ImageBytesByUrl
服务(位于“_GoogleImageSearchService_”文件夹中)
或者,右键单击并将应用程序安装为 OOB,然后从开始菜单或桌面快捷方式运行它。