两个 CoverFlow 的故事
两个基本的 coverflow 控件 - 一个用 JavaScript,一个用 Silverlight。
引言
我最近给自己布置了一个任务,要开发两个版本的简单CoverFlow控件 - 一个用 Silverlight,一个用 JavaScript。在本文中,我将对这两个实现进行高层次的概述,并讨论在用这两种技术开发过程中遇到的一些相似之处和不同之处。本文不会深入探讨代码的许多细节。如果您想进一步了解代码,请随时下载完整的源代码。该项目还托管在 github 上。
您可以在这里看到这两个控件的实际运行效果
- JavaScript 版本(仅限 WebKit)
- Silverlight 版本
一个警告:决定仅为 WebKit 浏览器开发 JavaScript 版本,这使得一些捷径得以实现,可能导致任何比较都有些不公平。显然,要完成一个更具跨浏览器兼容性的版本需要更多的努力,而 Silverlight 版本应该可以在任何安装了相应插件的浏览器中运行。在比较代码时,需要牢记这一点。
目录
实现概述
两个实现都采用了相似的方法。它们都从水平排列的图像集合开始。
然后,将“当前”元素缩放到最前面,并对其相邻元素进行缩放和旋转,如下图所示。所有其他元素都将被隐藏。
添加了倒影,以给人一种元素是站在玻璃表面上,而不是漂浮在空中的感觉!
然后水平移动这些元素,以使当前元素保持在中心位置。
最后,将上述所有缩放、旋转和水平调整都进行动画处理,以在“当前”元素改变时提供翻阅元素的感受。
基本布局
每个实现的精髓在于一个用于构建 UI 的元素集合。
JavaScript 版本
对于 JavaScript 应用程序,我采用了jQuery 插件的方法。元素集合通过 `settings.items` 数组传递给插件。
$.fn.coverFlow = function (settings) {
settings = $.extend({}, $.fn.coverFlow.defaultSettings, settings || {});
return this.each(function () {
$.tmpl(settings.template, settings).appendTo(this);
$("#coverFlowItems").width(settings.itemSize * settings.items.length);
...
});
};
它使用jQuery 模板来渲染元素。`$.fn.coverFlow.defaultSettings` 提供了 `template` 和 `itemSize` 的默认值,因此在调用插件时可以省略它们,或者将它们添加到 `settings` 对象以覆盖默认值。
默认模板将元素渲染为无序列表,如下所示。
<ul id='coverFlowItems'>
{{each $data.items}}
<li>
<a href='${$value.url}' tabindex='-1'>
<img src="${$value.image}"
width='${$data.itemSize}'
height='${$data.itemSize}' />
</a>
</li>
{{/each}}
</ul>
然后只需要一些 CSS 来将元素水平排列...
#coverFlowItems > li {
display: inline-block;
...
}</
Silverlight 版本
在 Silverlight 版本中,元素的布局由一个 `ItemsControl` 提供,该控件配置为使用水平 `StackPanel` 作为其布局面板。每个元素的显示委托给一个 `DataTemplate`,其中包含一个 `CoverFlowItemView` 类型的 `UserControl`。
...
<ItemsControl ItemsSource="{Binding CoverFlowItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<view:CoverFlowItemView />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
...
使用模型-视图-ViewModel (MVVM) 模式,控件的 `DataContext` 被设置为一个 ViewModel 对象,该对象在一个名为 `CoverFlowItems` 的 `ObservableCollection` 属性中保存了元素集合。 `ItemsControl` 的 `ItemsSource` 绑定到此属性。
`CoverFlowItemView` 用户控件定义了每个元素的显示方式,类似于 jQuery 模板:一个带有超链接的图像。
<UserControl x:Class="CoverFlow.View.CoverFlowItemView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<UserControl.Resources>
<!-- Styles & resources -->
...
</UserControl.Resources>
...
<HyperlinkButton NavigateUri="{Binding Url}">
<Image Source="{Binding Image}"
Style="{StaticResource coverFlowImageStyle}"/>
</HyperlinkButton>
...
</UserControl>
比较与观察
到目前为止所展示代码的一个有趣方面是不同类型系统对两种实现的影响。
jQuery 插件利用了 JavaScript 的'鸭子类型' - 如果传入的元素“像 CoverFlow 元素一样行走,像 CoverFlow 元素一样游泳,像 CoverFlow 元素一样叫,那么我们就可以称它们为 CoverFlow 元素!”。换句话说,只要插件接收到的对象具有预期的 `image` 和 `url` 属性,它就会满意。在 Silverlight 版本中,此要求在 `CoverFlowItemViewModel` 类型中明确定义,并包含 `Image` 和 `Url` 属性。
public class CoverFlowItemViewModel : INotifyPropertyChanged
{
...
private readonly string url;
private readonly string image;
public CoverFlowItemViewModel(string url, string image)
{
this.url = url;
this.image = image;
}
public string Url { get { return url; } }
public string Image { get { return image; } }
...
}
静态与动态类型语言的相对优点和缺点已经进行了激烈的争论。在实践中,这取决于在权衡取舍之后,哪种最适合正在开发的应用类型。
总的来说,静态类型语言可以提供更好的工具支持,如自动完成、代码导航、重构等,这在开发过程中可以大大提高生产力。此外,编译时的错误消息可以帮助更早地发现错误。
另一方面,动态语言的编辑-编译-测试-调试周期缩短,可以提供更流畅的反馈和调试体验。
对于这类练习,与开发更大的、更复杂的项目相比,在开发 Silverlight 版本时可用的静态类型检查和更好的工具支持似乎没有那么大的影响。然而,JavaScript 开发中更流畅的反馈和调试体验确实带来了很大的不同。
能够更改一些 JavaScript 并简单地刷新浏览器以查看更改的效果是一个很大的优势。此外,使用浏览器内置的调试工具来检查元素的属性并试验其属性的不同值(UI 会“实时”更新),有助于快速理解和解决许多问题。
我最终利用这一点来试验 JavaScript 版本中元素的样式,然后利用我学到的知识在 Silverlight 版本中实现相同的样式。
转换元素
一旦我们将元素集合放入基本布局中,其余大部分代码都与将这些元素转换为 CoverFlow 样式以及在当前元素更改时对其进行动画处理有关。
JavaScript 版本
在 JavaScript 版本中,代码的主要职责是根据当前元素的变化来添加和删除相关元素的类名。在下面的代码片段中,`_items` 是一个包含元素 DOM 元素的 jQuery 包装对象的集合,即 `$("#coverFlowItems li")`。
// Remove the class names from the old current item and its neighbours...
$(this._items[oldIndex - 2]).removeClass("left-2");
$(this._items[oldIndex - 1]).removeClass("left-1");
$(this._items[oldIndex]).removeClass("active");
$(this._items[oldIndex + 1]).removeClass("right-1");
$(this._items[oldIndex + 2]).removeClass("right-2");
// Add the class names to the new one and its neighbours...
$(this._items[this._currentIndex - 2]).addClass("left-2");
$(this._items[this._currentIndex - 1]).addClass("left-1");
$(this._items[this._currentIndex]).addClass("active");
$(this._items[this._currentIndex + 1]).addClass("right-1");
$(this._items[this._currentIndex + 2]).addClass("right-2");
转换本身是在 CSS 中定义的。
#coverFlowItems > li.active { z-index: 2; opacity: 1; -webkit-transform: scale(2); } #coverFlowItems > li.left-1 { z-index: 1; opacity: 1; -webkit-transform: scale(1.75) rotateY(60deg); } #coverFlowItems > li.right-1 { z-index: 1; opacity: 1; -webkit-transform: scale(1.75) rotateY(-60deg); } #coverFlowItems > li.left-2 { opacity: 0.8; -webkit-transform: scale(1.5) rotateY(60deg); } #coverFlowItems > li.right-2 { opacity: 0.8; -webkit-transform: scale(1.5) rotateY(-60deg); }
动画也是在 CSS 中定义的。
#coverFlowItems > li { ... -webkit-transition: all 0.5s; }
Silverlight 版本
为了在 Silverlight 版本中创建相同的效果,我使用了 VisualState 类来定义每个元素在每个特定状态下的外观。
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState x:Name="Left_2">
<Storyboard TargetName="itemPanel">
<DoubleAnimation
To="0.8"
Duration="0:00:00.5"
Storyboard.TargetProperty="(UIElement.Opacity)" />
<DoubleAnimation
To="-60"
Duration="0:00:00.5"
Storyboard.TargetProperty="(UIElement.Projection).(RotationY)" />
<DoubleAnimation
To="-0"
Duration="0:00:00.5"
Storyboard.TargetProperty="(UIElement.Projection)
.(CenterOfRotationX)" />
<DoubleAnimation
To="1.5"
Duration="0:00:00.5"
Storyboard.TargetProperty="(UIElement.RenderTransform)
.Children[0].ScaleX" />
<DoubleAnimation
To="1.5"
Duration="0:00:00.5"
Storyboard.TargetProperty="(UIElement.RenderTransform)
.Children[0].ScaleY" />
</Storyboard>
</VisualState>
<VisualState x:Name="Left_1">
...
</VisualState>
<VisualState x:Name="Current">
...
</VisualState>
<VisualState x:Name="Right_1">
...
</VisualState>
<VisualState x:Name="Right_2">
...
</VisualState>
<VisualState x:Name="Hidden">
...
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
我创建了一个附加属性,允许每个元素的视觉状态绑定到一个 ViewModel 属性。
public static readonly DependencyProperty VisualStateProperty =
DependencyProperty.RegisterAttached("VisualState", typeof(string),
typeof(CoverFlowItemView), new PropertyMetadata(VisualStateChanged));
public static string GetVisualState(DependencyObject target)
{
return (string)target.GetValue(VisualStateProperty);
}
public static void SetVisualState(DependencyObject target, string value)
{
target.SetValue(VisualStateProperty, value);
}
private static void VisualStateChanged(object sender,
DependencyPropertyChangedEventArgs args)
{
var newState = (string)args.NewValue;
if (!string.IsNullOrWhiteSpace(newState))
{
...
VisualStateManager.GoToState(control, newState, true);
}
}
`CoverFlowItemView` 和 `CoverFlowItemViewModel` 之间的绑定如下所示...
<UserControl x:Class="CoverFlow.View.CoverFlowItemView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CoverFlow.View"
local:CoverFlowItemView.VisualState="{Binding VisualState}">
public class CoverFlowItemViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
...
private string visualState = "Hidden";
public string VisualState
{
get { return visualState; }
set
{
if (VisualState == value)
{
return;
}
visualState = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("VisualState"));
}
}
}
}
因此,当当前元素改变时,可以通过相应地更新它们的 VisualState 来转换元素,这与 JavaScript 版本类似,例如...
public void NextItem()
{
SetItemVisualState(CurrentItemIndex - 2, "Hidden");
SetItemVisualState(CurrentItemIndex - 1, "Left_2");
SetItemVisualState(CurrentItemIndex, "Left_1");
SetItemVisualState(CurrentItemIndex + 1, "Current");
SetItemVisualState(CurrentItemIndex + 2, "Right_1");
SetItemVisualState(CurrentItemIndex + 3, "Right_2");
CurrentItemIndex++;
}
private void SetItemVisualState(int itemIndex, string visualState)
{
if (IsValidIndex(itemIndex))
{
CoverFlowItems[itemIndex].VisualState = visualState;
}
}
比较与观察
从概念上讲,这两个实现非常相似。它们都基于一种分离,即将决定每个元素当前状态的逻辑与定义该状态外观的逻辑分离开来。
但不可否认的是,Silverlight 实现更加冗长。此外,那些 `Storyboard.TargetProperty` 设置中的路径(如下面所示)经过了很多试错才得以正确,并且在它们不起作用时很难调试!
Storyboard.TargetProperty="(UIElement.RenderTransform).Children[0].ScaleX"
正如文章开头提到的,JavaScript 版本之所以只针对 WebKit,很大程度上是由于 CSS 中那些转换定义。
-webkit-transform: scale(1.75) rotateY(60deg);
许多厂商特定的属性在每个主要浏览器中都有等效项,但它们不是标准的,也不能保证在不同浏览器中以相同的方式工作。(事实上,它们也不能保证在同一浏览器的未来版本中以相同的方式工作!)
在 Firefox 中进行快速测试,将所有 `-webkit-` 前缀替换为 `-moz-` 后,效果并未如预期,显然比仅仅用不同的厂商前缀重复属性要复杂得多!
一些人欢迎这些厂商特定的属性,认为它们加速了 CSS 开发;另一些人则认为它们损害了 Web 标准。无论如何,Silverlight 版本的一个主要优势是,既然花费了精力实现了这个更冗长的解决方案,我们就可以期望它在任何安装了 Silverlight 插件的浏览器中都能正常工作。
Silverlight 版本可在主流浏览器中运行...
结论
在这里,我提供了一个相当高层次的概述,并讨论了两个实现之间一些有趣的相似之处或不同之处。为了文章的简洁性,忽略了两个版本中的一些细节。如果您想更详细地研究代码,完整的源代码可用。
如果我被迫做出一个结论,我会说,对于这个练习来说,JavaScript 语言的灵活性和动态性似乎导致了一个比 Silverlight 版本更简洁的解决方案,并且浏览器内置的调试工具节省了大量时间。Silverlight 开发中可用的静态类型检查和其他工具(以及 JavaScript 开发环境中缺乏这些工具)的影响似乎不如我预期的那么大。然而,我们不能忽视 Silverlight 版本无需额外努力即可提供更广泛的浏览器覆盖率,而 JavaScript 版本在这方面仍有一些工作要做。
当然,不难想象在某些情况下,这些差异的影响会反转——即,Silverlight 开发中的类型检查和工具变得更加重要。更好的看待这些差异的方式是将其视为基于权衡的选择,并在开发的应用类型的背景下进行考虑,而不是一个整体的“哪个更好”的决定。
最后,我建议您自己尝试进行此类练习(用两种技术开发一个简单的应用程序并比较体验)。它可以成为一个有用的工具,帮助您理解这些权衡,并更好地了解它们可能对您工作的应用程序类型产生的影响。