65.9K
CodeProject 正在变化。 阅读更多。
Home

开发Windows Phone 7跳转列表控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (28投票s)

2011年3月10日

CPOL

18分钟阅读

viewsIcon

101241

downloadIcon

2205

本文介绍了 Windows Phone 7 Jump List 控件的开发过程,逐步讲解了该控件的开发过程(最终得到一个相当炫酷的控件!)。

JumpListBasic.png

引言

大约一个月前,我为 Windows Phone 7 创建了一个 Jump List 控件,并发布在我的博客上。我收到了关于该控件的大量反馈,包括关于其某些部分工作原理的提问。因此,我决定在 CodeProject 上发布一篇深入的文章,介绍该控件的开发过程。

该控件本身相当动态,因此了解其效果的最佳方式是观看以下视频,一个是在模拟器上录制的,另一个是在真实设备上录制的 - 展示了该控件的良好性能(抱歉视频质量不佳!)。

如果您只想获取代码并在您的应用程序中使用 jump list,那么请访问我的博客,在那里您会找到用户指南和一些示例。如果您想了解这个控件是如何构建的,请继续阅读……

目录

简介

对于 Silverlight 开发者来说,Windows Phone 7 简直是梦想成真,一个支持他们已经熟悉的语言/框架的移动平台,或者正如 Jesse Liberty 所说,“你已经是 Windows Phone 开发者了”。我真正觉得 Silverlight for WP7 很酷的一点是,在 Web 和移动设备上可以使用完全相同的控件。但是,Windows Phone 7 的控件是专门为移动设备设计的,具有更大的“触碰”区域,以及用于滚动的手势等。尽管如此,有时您确实需要一个特定于移动平台的控件。

在移动设备上导航长列表数据是一件很麻烦的事情。在桌面/Web 上,您可以单击滚动条,通过一次手势导航列表的完整长度,而在移动设备上导航相同的列表则需要多次滑动。这时 Jump List 就派上用场了!

Jump List 将长列表中的项目分组到类别中。单击类别标题(或跳转按钮)将打开一个类别视图,然后您可以在其中单击另一个类别,从而立即将列表滚动到此新选定类别开始的位置。

本文介绍了 Jump List 控件的开发过程。

开发 JumpList 控件

创建自定义控件

构建新控件的第一步是确定一个合适的起点,即一个要扩展的现有框架类。jump list 应支持选择,因此框架的 Selector 类(ListBox 是其子类)是一个潜在的选择;但是,它没有公开的构造函数,所以这行不通!这样就只剩下 Control 了,所以我们只能从那里开始。

public class JumpList : Control
{
  public JumpList()
  {
    DefaultStyleKey = typeof(JumpList);
  }
}

通过扩展 Control,我们正在创建一个“自定义控件”(或者 Visual Studio 的“添加新项”对话框令人困惑地称之为“Silverlight 模板化控件”)。控件的“外观”,即构成控件在屏幕上显示的各种视觉元素,被定义为一个 Style

<Style TargetType="local:JumpList">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:JumpList">
                <Border Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

在构造函数中将 JumpListDefaultStyleKey 设置为引用上面的样式,可以确保该样式应用于我们创建的任何 JumpList 实例。该样式设置了控件的一个属性,即 Template,用于渲染一个 BorderBorder 的各种属性绑定到我们从 Control 继承的各种属性。JumpList 控件实际上还没有做什么,尽管我们可以创建一个实例并设置其各种边框属性。

<local:JumpList Background="Pink" 
                            BorderBrush="White" BorderThickness="5"
                            Width="100" Height="100"/>

control.png

渲染项目

JumpList 需要渲染用户提供的项目集合,其中每个项目都根据模板进行渲染,模仿 ListBox(以及渲染对象列表的其他类,例如 ComboBox)的行为。为了支持这一点,我们在控件中添加了一个类型为 IEnumerableItemsSource 依赖属性。如果您以前创建过自己的依赖属性,您会知道有很多样板代码需要处理,这就是为什么我更喜欢使用代码生成而不是手动或通过代码片段添加这些代码。我在这里使用的技术在博客文章“使用 T4 进行声明式依赖属性定义”中有介绍,其中您只需在类中添加一个描述属性的属性,代码生成就会在生成的类中添加所需的代码。

添加依赖属性就像这样简单……

[DependencyPropertyDecl("ItemsSource", typeof(IEnumerable), null,
     "Gets or sets a collection used to generate the content of the JumpList")]
public partial class JumpList : Control
{
  public JumpList()
  {
    this.DefaultStyleKey = typeof(JumpList);
  }
}

这将生成以下代码

public partial class JumpList  
{
    #region ItemsSource
            
    /// <summary>
    /// Gets or sets a collection used to generate the content
    ///    of the JumpList. This is a Dependency Property.
    /// </summary>    
    public IEnumerable ItemsSource
    {
        get { return (IEnumerable)GetValue(ItemsSourceProperty); }
        set { SetValue(ItemsSourceProperty, value); }
    }
    
    /// <summary>
    /// Identifies the ItemsSource Dependency Property.
    /// <summary>
    public static readonly DependencyProperty ItemsSourceProperty =
        DependencyProperty.Register("ItemsSource", typeof(IEnumerable),
        typeof(JumpList), new PropertyMetadata(null, OnItemsSourcePropertyChanged));
    
        
    private static void OnItemsSourcePropertyChanged(DependencyObject d,
        DependencyPropertyChangedEventArgs e)
    {
        JumpList myClass = d as JumpList;
            
        myClass.OnItemsSourcePropertyChanged(e);
    }
    
    partial void OnItemsSourcePropertyChanged(DependencyPropertyChangedEventArgs e);
        
            
    #endregion
}

为了渲染用户通过 ItemSource 属性提供的项目列表,我们还需要公开一个属性,允许用户指定他们希望如何渲染项目。遵循 ItemsControl 的命名约定,我们将为 JumpList 添加一个 ItemTemplate 属性。

[DependencyPropertyDecl("ItemsSource", typeof(IEnumerable), null,
     "Gets or sets a collection used to generate the content of the JumpList")]
[DependencyPropertyDecl("ItemTemplate", typeof(DataTemplate), null,
     "Gets or sets the DataTemplate used to display each item")]
public partial class JumpList : Control
{
  public JumpList()
  {
    this.DefaultStyleKey = typeof(JumpList);
  }
}

同样,依赖属性本身被添加到 T4 模板生成的类中。

为了渲染用户通过 ItemsSource 属性提供的项目(通过绑定或直接设置属性),我们需要在 JumpList 渲染时以某种方式将它们添加到其视觉树中。我们可以在运行时直接将它们添加到视觉树;但是,框架的 ItemsControl 提供了一种在面板中渲染绑定项目集合的机制,提供了一种更简单、更灵活的解决方案。在代码隐藏中创建了一个 ContentControl 的集合,每个绑定项目对应一个(稍后,此集合还将包括组标题以及项目本身)。

/// <summary>
/// Gets the categorised list of items
/// </summary>
public List<object> FlattenedCategories
{
  get
  {
    return _flattenedCategories;
  }
  private set
  {
    _flattenedCategories = value;
    OnPropertyChanged("FlattenedCategories");
  }
}
private void RebuildCategorisedList()
{
  if (ItemsSource == null)
    return;
  var jumpListItems = new List<object>();
  foreach (var item in ItemsSource)
  {
      jumpListItems.Add(new ContentControl()
      {
          Content = item,
          ContentTemplate = ItemTemplate
      });
  }
  FlattenedCategories = jumpListItems;
}

当设置 JumpListItemsSource 属性时,上面的 RebuildCategorisedList 方法会创建一个 ContentControl 列表,JumpList 通过 FlattenedCategories 属性公开此列表。现在,我们将所有需要做的就是将它们添加到 JumpList 的视觉树中,方法是在模板中添加一个 ItemsControl,通过 RelativeSource-TemplatedParent 绑定将其绑定到 FlattenedCategories 属性。

<Style TargetType="local:JumpList">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="local:JumpList">
        <Border Background="{TemplateBinding Background}"
                          BorderBrush="{TemplateBinding BorderBrush}"
                          BorderThickness="{TemplateBinding BorderThickness}">
          <ItemsControl x:Name="JumpListItems"
                    ItemsSource="{Binding RelativeSource={RelativeSource 
                                 TemplatedParent},Path=FlattenedCategories}">
            <!-- use a virtualizing stack panel to host our items -->
            <ItemsControl.ItemsPanel>
              <ItemsPanelTemplate>
                <VirtualizingStackPanel/>
              </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
              
            <!-- template, which adds a scroll viewer -->
            <ItemsControl.Template>
              <ControlTemplate TargetType="ItemsControl">
                <ScrollViewer x:Name="ScrollViewer">
                  <ItemsPresenter/>
                </ScrollViewer>
              </ControlTemplate>
            </ItemsControl.Template>
          </ItemsControl>
            
        </Border>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

现在我们的控件可以渲染项目集合了,例如,如果一个 JumpList 使用以下模板实例化,其中 ItemsSource 是一个 Person 对象集合(具有 SurnameForename 属性)。

<local:JumpList>
  <local:JumpList.ItemTemplate>
    <DataTemplate>
      <StackPanel Orientation="Horizontal"
                  Margin="0,3,0,3"
                  Height="40">
        <TextBlock Text="{Binding Surname}"
                    Margin="3,0,0,0"
                    VerticalAlignment="Center"
                  FontSize="{StaticResource PhoneFontSizeLarge}"/>
        <TextBlock Text=", "
                    VerticalAlignment="Center"
                  FontSize="{StaticResource PhoneFontSizeLarge}"/>
        <TextBlock Text="{Binding Forename}"
                    VerticalAlignment="Center"
                  FontSize="{StaticResource PhoneFontSizeLarge}"/>
      </StackPanel>
    </DataTemplate>
  </local:JumpList.ItemTemplate>
</local:JumpList>

JumpList 将渲染如下

control2.png

处理 CollectionChanged 事件

ItemsSource 属性的类型是 IEnumerable,这是我们渲染它所需要的对所提供数据的唯一要求。这为用户提供了极大的灵活性;他们可以提供一个 ListArray,或者直接将 ItemsSource 分配给 LINQ 查询的结果。但是,他们也可能将此属性设置为(或绑定到)一个 ObservableCollection,并期望当他们添加或删除列表中的项目时,JumpList 会被更新。为了支持这个要求,我们需要“探测”ItemsSource,看看它是否实现了 INotifyCollectionChangedObservableCollection 工作所需的接口),并相应地更新我们的列表。

// invoked when the ItemsSource dependency property changes
partial void OnItemsSourcePropertyChanged(DependencyPropertyChangedEventArgs e)
{
  INotifyCollectionChanged oldIncc = e.OldValue as INotifyCollectionChanged;
  if (oldIncc != null)
  {
    oldIncc.CollectionChanged -= ItemsSource_CollectionChanged;
  }
  INotifyCollectionChanged incc = e.NewValue as INotifyCollectionChanged;
  if (incc != null)
  {
    incc.CollectionChanged += ItemsSource_CollectionChanged;
  }
  RebuildCategorisedList();
}

// handles collection changed events, rebuilding the list
private void ItemsSource_CollectionChanged(object sender, 
                         NotifyCollectionChangedEventArgs e)
{
  RebuildCategorisedList();
}

请注意,上面的代码可以进行优化,以检查 NotifyCollectionChangedEventArgs.Action 参数,在适当的情况下修改我们公开的列表,而不是完全重建它。

添加类别

到目前为止,控件只是渲染项目列表,除了 ItemsControl 之外什么都不做。为了将其变成一个 jump list,我们需要将项目分配给类别。为了提供如何将项目分配给类别的灵活性,我们通过 ICategoryProvider 接口将这项责任交给了控件的用户。

/// <summary>
/// A category provider assigns items to categories and details
/// the full category list for a set of items.
/// </summary>
public interface ICategoryProvider
{
  /// <summary>
  /// Gets the category for the given items
  /// </summary>
  object GetCategoryForItem(object item);
  /// <summary>
  /// Gets the full list of categories for the given items.
  /// </summary>
  List<object> GetCategoryList(IEnumerable items);
}

向我们的控件添加依赖属性

...
[DependencyPropertyDecl("CategoryProvider", typeof(ICategoryProvider), null,
    "Gets or sets a category provider which groups the items " + 
    "in the JumpList and specifies the categories in the jump menu")]
public partial class JumpList : Control, INotifyPropertyChanged
{
  ...
}

类别提供程序负责将列表中的每个对象分配给一个类别,并提供所有类别的列表。类别列表可能取决于正在渲染的列表,例如,事件的日期,或者它可能是一些固定的列表,例如,字母表。以下显示了该接口的一个实现,该实现根据名为 PropertyName 的属性的第一个字母将项目分配给类别。类别列表是按顺序排列的整个字母表。

/// <summary>
/// A category provider that categorizes items
/// based on the first character of the
/// property named via the PropertyName property.
/// </summary>
public class AlphabetCategoryProvider : ICategoryProvider
{
  /// <summary>
  /// Gets or sets the name of the property that is used to assign each item
  /// to a category.
  /// </summary>
  public string PropertyName { get; set;}
  public object GetCategoryForItem(object item)
  {
    var propInfo = item.GetType().GetProperty(PropertyName);
    object propertyValue = propInfo.GetValue(item, null);
    return ((string)propertyValue).Substring(0, 1).ToUpper();
  }
  public List<object> GetCategoryList(IEnumerable items)
  {
    return Enumerable.Range(0, 26)
            .Select(index => Convert.ToChar(
                   (Convert.ToInt32('A') + index)).ToString())
            .Cast<object>()
            .ToList();
  }
}

您可以在这里看到,类别列表始终是完整的字母表,并且不依赖于 JumpList 当前渲染的项目。控件的用户只需将 CategoryProvider 设置为上面提供程序的实例。例如,如果控件用于渲染 Person 对象(具有 SurnameForename 属性),则 JumpList 的 XAML 将如下所示。

<local:JumpList>
  <local:JumpList.CategoryProvider>
    <local:AlphabetCategoryProvider PropertyName="Surname"/>
  </local:JumpList.CategoryProvider>
</local:JumpList>

上面描述的 RebuildCategorisedList 方法会创建一个 ContentControl 列表,每个列表项对应一个项目,现在可以更新此方法以添加类别标题(即跳转按钮)。我们希望 JumpList 的用户能够样式化这些跳转按钮,因此添加了一些额外的依赖属性。

...
[DependencyPropertyDecl("JumpButtonItemTemplate", typeof(DataTemplate), null,
  "Gets or sets the DataTemplate used to display the Jump buttons. " + 
  "The DataContext of each button is a group key")]
DependencyPropertyDecl("JumpButtonTemplate", typeof(ControlTemplate), null,
  "Gets or sets the ControlTemplate for the Jump buttons")]
[DependencyPropertyDecl("JumpButtonStyle", typeof(Style), null,
  "Gets or sets the style applied to the Jump buttons. " + 
  "This should be a style with a TargetType of Button")]
public class JumpList : Control
{
  ...
}

这三个属性让用户可以完全控制按钮的渲染方式;如果他们只想设置宽度、高度或其他基本属性,他们可以设置 JumpButtonStyle;如果他们想更改模板或添加图标,他们可以设置 JumpButtonTemplate;最后,他们可以通过 JumpButtonItemTemplate 指定表示每个项目类别对象的渲染方式,这允许他们格式化日期等。

RebuildCategorisedList 已扩展为通过简单的 LINQ 查询根据类别提供程序对项目进行分组。按钮被添加到 JumpList 模板内的 ItemsControl 所公开的对象集合中。

private void RebuildCategorisedList()
{
  if (ItemsSource == null)
    return;
      
  // adds each item into a category
  var categorisedItemsSource = ItemsSource.Cast<object>()
              .GroupBy(i => CategoryProvider.GetCategoryForItem(i))
              .OrderBy(g => g.Key)
              .ToList();
      
     
  // create the jump list
  var jumpListItems = new List<object>();
  foreach (var category in categorisedItemsSource)
  {
    jumpListItems.Add(new Button()
    {
      Content = category.Key,
      ContentTemplate = JumpButtonItemTemplate,
      Template = JumpButtonTemplate,
      Style = JumpButtonStyle
    });
    jumpListItems.AddRange(category.Select(item =>
      new ContentControl()
      {
        Content = item,
        ContentTemplate = ItemTemplate
      }).Cast<object>());
  }
  // add interaction handlers
  foreach (var button in jumpListItems.OfType<Button>())
  {
    button.Click += JumpButton_Click;
  }
}
private void JumpButton_Click(object sender, RoutedEventArgs e)
{
  IsCategoryViewShown = true;
}

请注意,每个创建的按钮都添加了一个 Button.Click 事件处理程序 - 稍后将详细介绍!

我们可以在 JumpList 的默认样式(在 generic.xaml 文件中)中添加属性设置器来设置三个跳转按钮属性的默认值。

<Style TargetType="l:JumpList">
  <!-- style the buttons to be left aligned with some padding -->
  <Setter Property="JumpButtonStyle">
    <Setter.Value>
      <Style TargetType="Button">
        <Setter Property="HorizontalAlignment" Value="Left"/>
        <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
        <Setter Property="VerticalContentAlignment" Value="Stretch"/>
        <Setter Property="Padding" Value="8"/>
      </Style>
    </Setter.Value>
  </Setter>
  <!-- an item template that simply displays the category 'object' -->
  <Setter Property="JumpButtonItemTemplate">
    <Setter.Value>
      <DataTemplate>
        <TextBlock Text="{Binding}"
                  FontSize="{StaticResource PhoneFontSizeMedium}"
                  Padding="5"
                  VerticalAlignment="Bottom"
                  HorizontalAlignment="Left"/>
      </DataTemplate>
    </Setter.Value>
  </Setter>
  <!-- the template for our button, a simplified version of the standard button -->
  <Setter Property="JumpButtonTemplate">
    <Setter.Value>
      <ControlTemplate>
        <Grid Background="Transparent">
          <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="CommonStates">
              <VisualState x:Name="Normal"/>
              <VisualState x:Name="MouseOver"/>
              <VisualState x:Name="Pressed">
                <Storyboard>
                  <ColorAnimation To="White" Duration="0:0:0"
                      Storyboard.TargetName="Background"
                      Storyboard.TargetProperty=
                        "(Rectangle.Fill).(SolidColorBrush.Color)"/>
                </Storyboard>
              </VisualState>
              <VisualState x:Name="Disabled"/>
            </VisualStateGroup>
          </VisualStateManager.VisualStateGroups>
          <Rectangle  x:Name="Background"
                            Fill="{StaticResource PhoneAccentBrush}"/>
          <ContentControl x:Name="ContentContainer"
              Foreground="{TemplateBinding Foreground}"
              HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
              VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
              Padding="{TemplateBinding Padding}"
              Content="{TemplateBinding Content}"
              ContentTemplate="{TemplateBinding ContentTemplate}"/>
        </Grid>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
  ...
</Style>

从上面的 XAML 可以看出,JumpButtonStyleJumpButtonItemTemplate 属性的值非常简单。JumpButtonTemplate 稍微复杂一些;在这里,我们定义了用于渲染我们的按钮的模板。跳转按钮没有使用默认的黑色带白色边框的按钮模板,而是被模板化为一个实心矩形,填充了手机的强调色(用户指定的用于活动磁贴等的颜色)。VisualStateManager 标记定义了一个单一的 VisualState,当按钮被按下时,按钮会变成白色。

控件现在开始看起来像一个 jump list 了……

control3.png

类别视图

当用户单击跳转按钮时,我们希望显示一个菜单,允许他们跳转到特定的类别。为了实现这一点,我们需要创建数据的另一个“视图”,该视图是隐藏的,在按钮单击时将其显示出来。

我们可以扩展构建分类项目和跳转按钮的方法,以公开类别列表。

private void RebuildCategorisedList()
{
  // adds each item into a category
  var categorisedItemsSource = ItemsSource.Cast<object>()
                                  .GroupBy(i => CategoryProvider.GetCategoryForItem(i))
                                  .OrderBy(g => g.Key)
                                  .ToList();
  // ... jump list creation code as per above ...
  // creates the category view, where the active state is determined by whether
  // there are any items in the category
  CategoryList = CategoryProvider.GetCategoryList(ItemsSource)
                                  .Select(category => new Button()
                                  {
                                    Content = category,
                                    IsEnabled = categorisedItemsSource.Any(
                                      categoryItems => categoryItems.Key.Equals(category)),
                                    ContentTemplate = this.CategoryButtonItemTemplate,
                                    Style = this.CategoryButtonStyle,
                                    Template = this.CategoryButtonTemplate
                                  }).Cast<object>().ToList();
  foreach (var button in CategoryList.OfType<Button>())
  {
    button.Click += CategoryButton_Click;
  }
}

上面的代码创建了一个按钮列表,每个类别一个。每个按钮的启用状态取决于用户提供的列表中该类别是否存在任何项目。同样,我们允许用户通过模板、样式和项模板属性指定按钮的渲染方式。

模板中添加了以下标记,将一个项控件绑定到 CategoryList 属性,使用了与渲染跳转列表的项控件相同的技术。

<ItemsControl x:Name="CategoryItems"
              Visibility="Collapsed"                      
              ItemsSource="{Binding RelativeSource= {RelativeSource 
                           TemplatedParent}, Path=CategoryList}">              
  <ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
      <tk:WrapPanel Background="Transparent"/>
    </ItemsPanelTemplate>
  </ItemsControl.ItemsPanel>
  <ItemsControl.Template>
    <ControlTemplate>
      <ScrollViewer x:Name="ScrollViewer">
        <ItemsPresenter/>
      </ScrollViewer>
    </ControlTemplate>
  </ItemsControl.Template>
</ItemsControl>

类别按钮的样式、项模板和模板与跳转按钮类似;但是,类别按钮为禁用状态添加了额外的样式,将按钮渲染为深灰色。上面的标记使用 Silverlight Toolkit 的 WrapPanel 来排列按钮,这会产生以下结果。

control4.png

切换视图

该控件现在有两个不同的“视图”,一个是由分类项目和跳转按钮组成的列表,另一个是类别视图。我们所要做的就是处理跳转按钮上的 Click 事件来显示类别视图。为了更大的灵活性,此行为通过一个 bool IsCategoryViewShown 属性公开。当单击跳转按钮时,此属性设置为 true,并且该属性的更改处理程序会负责切换视图。这为控件用户提供了更大的灵活性,允许他们以编程方式切换视图。

为了显示/隐藏 JumpList 模板中定义的类别视图和列表视图,我们需要获取对它们的引用。使用 UserControl,用 x:Name 属性命名的元素会自动连接到相应代码隐藏类中的字段。但是,对于自定义控件,您必须自己完成此连接。以下代码定位了 jump list 和类别视图的 ItemsControl

private ItemsControl _jumpListControl;
private ItemsControl _categoryItemsControl;
public override void OnApplyTemplate()
{
  base.OnApplyTemplate();
  _jumpListControl = this.GetTemplateChild("JumpListItems") as ItemsControl;
  _categoryItemsControl = this.GetTemplateChild("CategoryItems") as ItemsControl;
}

注意:传递给 GetTemplateChild 的名称与这些元素的 x:Name 匹配。

为每个依赖属性生成的代码添加了一个调用,该调用在属性更改时被调用。这允许您添加在属性更改后执行的逻辑。以下方法在每次更改 IsCategoryViewShown 属性时被调用,它只是显示/隐藏项控件。

partial void OnIsCategoryViewShownPropertyChanged(DependencyPropertyChangedEventArgs e)
{
  if ((bool)e.NewValue == true)
  {
    _jumpListControl.Visibility = Visibility.Collapsed;
    _categoryItemsControl.Visibility = Visibility.Visible;
  }
  else
  {
    _jumpListControl.Visibility = Visibility.Visible;
    _categoryItemsControl.Visibility = Visibility.Collapsed;
  }
}

实现“跳转”

我们已经看到 RebuildCategorisedList 方法向类别按钮添加了一个 Click 事件处理程序。现在我们需要添加使列表“跳转”到所需位置的代码。渲染分类项目列表的 ItemsControl 使用 VirtualizingStackPanel 作为项目的容器,并将其放置在 ScrollViewer 中。VirtualizingStackPanel 有一个 SetVerticalOffset 方法,可用于将其滚动到特定索引,从而使列表能够跳转。

我们需要做的第一件事是定位 VirtualizingStackPanel。与我们模板中的其他命名元素不同,此元素无法在 OnApplyTemplate 中通过 GetTemplateChild 检索,因为它位于不同的 XAML 命名空间(而且,如果没有项目要渲染,它可能不会在初始时创建)。为了在需要时定位 VirtualizingStackPanel,我们可以使用 LINQ-to-VisualTree 来查询我们 ItemsControl 的后代元素以找到所需类型的元素。

 /// <summary>
/// Gets the stack panel that hosts our jump list items
/// </summary>
private VirtualizingStackPanel ItemsHostStackPanel
{
  get
  {
    if (_stackPanel == null)
    {
      _stackPanel = _jumpListControl.Descendants<VirtualizingStackPanel>()
                                  .Cast<VirtualizingStackPanel>()
                                  .SingleOrDefault();
    }
    return _stackPanel;
  }
}

单击类别按钮时,我们会找到对应的跳转按钮(两者具有相同的 Content,即 ICategoryProvider 返回的类别)。找到对应的按钮后,我们可以找到它的索引,调整 VirtualizingStackPanel 的偏移量,然后切换回 jump-list 视图。

private void CategoryButton_Click(object sender, RoutedEventArgs e)
{
  var categoryButton = sender as Button;
  // find the jump button for this category 
  var button = FlattenedCategories.OfType<Button>()
                                  .Where(b => b.Content.Equals(categoryButton.Content))
                                  .SingleOrDefault();
  // button is null if there are no items in the clicked category
  if (button != null)
  {
    // find the button index
    var index = FlattenedCategories.IndexOf(button);
    ItemsHostStackPanel.SetVerticalOffset(index);
    
    IsCategoryViewShown = false;
  }
}

现在我们有了一个功能齐全的 JumpList 控件!

锦上添花!

我们到目前为止开发的控件工作良好;但是,它缺乏亮点(我们不希望我们的 iPhone 和 Android 朋友认为他们的平台更好,对吧?)。我们可以添加一些花哨的图形,添加阴影、渐变、图像等……但是,这与 Windows Phone 7 Metro 主题不太一致,后者偏爱清晰的排版和稀疏的图形以及流畅的动画。在本节中,我们将研究如何通过动画使此控件更具视觉吸引力,同时保持其简洁明了的风格。

简单的显示/隐藏动画

当 jump list 控件在类别和列表视图之间切换时,以下代码只是显示/隐藏这两个视图的相应项控件。

partial void OnIsCategoryViewShownPropertyChanged(DependencyPropertyChangedEventArgs e)
{
  if ((bool)e.NewValue == true)
  {
    _jumpListControl.Visibility = Visibility.Collapsed;
    _categoryItemsControl.Visibility = Visibility.Visible;
  }
  else
  {
    _jumpListControl.Visibility = Visibility.Visible;
    _categoryItemsControl.Visibility = Visibility.Collapsed;
  }
}

如果我们可以使用淡入淡出或其他过渡效果来切换这两个视图,那将是很棒的。很久以前,我写了一篇博客文章,其中介绍了一些简单的 FrameworkElement 扩展方法 Show()Hide(),它们会检查元素资源以查找可用于显示或隐藏元素的 storyboard。如果没有 storyboard,则会设置 Visibility 属性。应用此方法后,上面的代码变为。

partial void OnIsCategoryViewShownPropertyChanged(DependencyPropertyChangedEventArgs e)
{
  if ((bool)e.NewValue == true)
  {
    _jumpListControl.Hide();
    _categoryItemsControl.Show();
  }
  else
  {
    _jumpListControl.Show();
    _categoryItemsControl.Hide();
  }
}

这是用于渲染 jump list 的 ItemsControl 的更新后的 XAML,以包含用于更改控件不透明度的 storyboards,从而提供淡入/淡出效果。

<l:JumpListItemsControl x:Name="JumpListItems"
              ItemsSource="{Binding RelativeSource={RelativeSource 
                           TemplatedParent}, Path=FlattenedCategories}">
  <l:JumpListItemsControl.Resources>
    <Storyboard x:Key="JumpListItemsShowAnim">
      <DoubleAnimation To="1.0" Duration="0:0:0.5"
            Storyboard.TargetName="JumpListItems"
            Storyboard.TargetProperty="(ScrollViewer.Opacity)"/>
    </Storyboard>
    <Storyboard x:Key="JumpListItemsHideAnim">
      <DoubleAnimation To="0.35" Duration="0:0:0.5"
            Storyboard.TargetName="JumpListItems"
            Storyboard.TargetProperty="(ScrollViewer.Opacity)"/>
    </Storyboard>
  </l:JumpListItemsControl.Resources>
  <l:JumpListItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
      <VirtualizingStackPanel/>
    </ItemsPanelTemplate>
  </l:JumpListItemsControl.ItemsPanel>
  <l:JumpListItemsControl.Template>
    <ControlTemplate TargetType="l:JumpListItemsControl">
      <ScrollViewer x:Name="ScrollViewer">
        <ItemsPresenter/>
      </ScrollViewer>
    </ControlTemplate>
  </l:JumpListItemsControl.Template>
</l:JumpListItemsControl>

有关 Show() / Hide() 扩展方法的详细信息,请参阅我之前的博客文章

添加加载指示器

如果您只使用模拟器开发 Windows Phone 7 应用程序,您可能会对应用程序的响应能力产生一些错误的印象。真实的 Windows Phone 7 硬件通常比您时髦的开发计算机上的模拟硬件性能差得多!我进行了一些测量,发现我的开发计算机模拟器渲染每个页面的速度大约是真实设备的四倍。当然,结果因机器而异。真正需要记住的信息是,“在真实硬件上测试”。本节介绍 jump list 的一些简单更改,这些更改应确保快速的初始渲染时间。

如果类别视图 ItemsControl 是通过将不透明度从 0 动画到 1.0 来显示的,那么类别视图的所有视觉元素将在 jump list 首次显示时被渲染,即使用户看不到类别视图。这可能会使控件的总加载时间增加多达半秒。如果 ItemsControl 的可见性最初设置为 Collapsed(在 generic.xaml 中),那么它包含的众多子元素的开销将被移除。但是,这仍然不会消除类别视图半秒的额外渲染时间,只是将其推迟到稍后。我们不希望 jump list 在第一次单击按钮时“卡住”,因此添加了一个小的加载指示器,让用户知道手机正在执行某些操作……

当单击类别按钮时,我们最初会显示一个简单的加载消息,然后将类别视图可见性设置为 Visible,从而导致此视图的昂贵的初始构建。当类别视图及其子元素创建完成后,将触发 LayoutUpdated 事件;我们可以在隐藏加载指示器时处理此事件。

这个简单的加载指示器被添加到 jump list 模板中。

<Grid IsHitTestVisible="False"
      x:Name="LoadingIndicator"
      Opacity="0">
  <TextBlock Text="Loading ..."
              HorizontalAlignment="Right"/>
</Grid>

处理 IsCategoryViewShown 属性更改的代码已更新,以便在首次显示类别视图时显示此加载指示器。下次显示时,我们不需要加载指示器,因为类别视图 UI 已经构建完毕,只是通过将其不透明度设置为零来隐藏。

partial void OnIsCategoryViewShownPropertyChanged(DependencyPropertyChangedEventArgs e)
{
  if ((bool)e.NewValue == true)
  {
    _jumpListControl.Hide();
    // first time load!
    if (_categoryItemsControl.Visibility == Visibility.Collapsed)
    {
      // show the loading indicator
      _loadingIndicator.Opacity = 1;
          
      Dispatcher.BeginInvoke(() =>
      {
        // handle layout updated
        _categoryItemsControl.LayoutUpdated += 
           new EventHandler(CategoryItemsControl_LayoutUpdated);
        // make the items control visible so that its UI is built
        _categoryItemsControl.Visibility = Visibility.Visible;
      });
    }
    else
    {
      _jumpListControl.IsHitTestVisible = false;
      _categoryItemsControl.IsHitTestVisible = true;
      _categoryItemsControl.Show();
    }
  }
  else
  {
    _jumpListControl.Show();
    _jumpListControl.IsHitTestVisible = true;
    _categoryItemsControl.IsHitTestVisible = false;
    _categoryItemsControl.Hide();
  }
}
/// <summary>
/// Handles LayoutUpdated event in order to hide the loading indicator
/// </summary>
private void CategoryItemsControl_LayoutUpdated(object sender, EventArgs e)
{
  _categoryItemsControl.LayoutUpdated -= CategoryItemsControl_LayoutUpdated;
  _loadingIndicator.Visibility = System.Windows.Visibility.Collapsed;
  Dispatcher.BeginInvoke(() =>
  {
    // play the 'show' animation
    _categoryItemsControl.Show();
  });
}

我已经将上述内容提炼成一种更通用的方法来延迟某些 UI 元素的渲染,创建了一个 DeferredLoadContentControl,它最初显示一个“正在加载……”消息,同时构建更复杂的内容。您可以在我的博客上阅读有关此控件的信息。

动画“跳转”

前面介绍的 jump list 控件直接更改列表的垂直偏移量,以便将每个类别的标题按钮立即显示出来。在本节中,我们将研究如何动画化此过程,以便选定的类别平滑地滚动到视图中。

我们的列表的垂直偏移量通过以下代码进行更改。

ItemsHostStackPanel.SetVerticalOffset(index);

不幸的是,垂直偏移量没有作为依赖属性公开,因此我们无法通过 storyboard 直接对其进行动画处理。一个简单的解决方案是向我们的 jump list 控件添加一个私有依赖属性,我们可以对其进行动画处理。然后,我们可以处理此依赖属性的属性更改回调,以如上所述设置垂直偏移量。

这是带有设置垂直偏移量的回调的私有依赖属性。

/// <summary>
/// VerticalOffset, a private DP used to animate the scrollviewer
/// </summary>
private DependencyProperty VerticalOffsetProperty = 
  DependencyProperty.Register("VerticalOffset", typeof(double), 
  typeof(JumpList), new PropertyMetadata(0.0, OnVerticalOffsetChanged));

private static void OnVerticalOffsetChanged(DependencyObject d, 
                    DependencyPropertyChangedEventArgs e)
{
  JumpList jumpList = d as JumpList;
  jumpList.OnVerticalOffsetChanged(e);
}
private void OnVerticalOffsetChanged(DependencyPropertyChangedEventArgs e)
{
  ItemsHostStackPanel.SetVerticalOffset((double)e.NewValue);
}

然后,我们可以在 jump list 控件的构造函数中创建一个合适的 storyboard。这里,创建了一个简单的 DoubleAnimation,它使用正弦缓和函数,该函数在开始时加速,在结束时减速(提供更平滑的体验)。

public JumpList()
{
  DefaultStyleKey = typeof(JumpList);
  RebuildCategorisedList();
  // create a scroll animation
  _scrollAnimation = new DoubleAnimation();
  _scrollAnimation.EasingFunction = new SineEase();
  // create a storyboard for the animation
  _scrollStoryboard = new Storyboard();
  _scrollStoryboard.Children.Add(_scrollAnimation);
  Storyboard.SetTarget(_scrollAnimation, this);
  Storyboard.SetTargetProperty(_scrollAnimation, new PropertyPath("VerticalOffset"));
  // Make the Storyboard a resource.
  Resources.Add("anim", _scrollStoryboard);
}

通过向 JumpList 控件添加 ScrollDuration 依赖属性,我们可以使此功能更加灵活。剩下要做的就是在单击类别按钮时使用上述动画。

private void CategoryButton_Click(object sender, RoutedEventArgs e)
{
  var categoryButton = sender as Button;
  // find the jump button for this category 
  var button = FlattenedCategories.OfType<Button>()
                                  .Where(b => b.Content.Equals(categoryButton.Content))
                                  .SingleOrDefault();
  // button is null if there are no items in the clicked category
  if (button != null)
  {
    // find the button index
    var index = FlattenedCategories.IndexOf(button);
    if (ScrollDuration > 0.0)
    {
      _scrollAnimation.Duration = TimeSpan.FromMilliseconds(ScrollDuration);
      _scrollStoryboard.Duration = TimeSpan.FromMilliseconds(ScrollDuration);
      _scrollAnimation.To = (double)index;
      _scrollAnimation.From = ItemsHostStackPanel.ScrollOwner.VerticalOffset;
      _scrollStoryboard.Begin();
    }
    else
    {
      ItemsHostStackPanel.SetVerticalOffset(index);
    }
    IsCategoryViewShown = false;
  }
}

动画类别按钮“磁贴”

从 jump list 到类别视图的切换现在更有趣了,应用了淡入效果(可以通过重新设置控件的模板来替换为其他效果)。但是,如果每个类别按钮都像 Windows Phone 7 中心上的磁贴那样动画显示,那将更加令人兴奋。

为了支持这一点,类别按钮模板得到了扩展,添加了用于显示和隐藏类别按钮的 storyboards,这与上面描述的 Show() / Hide() 扩展方法非常相似。在下面的示例中,定义了一个 storyboard,它通过缩放和旋转磁贴来显示类别按钮,反之亦然。

<Setter Property="CategoryButtonTemplate">
  <Setter.Value>
    <ControlTemplate TargetType="Button">
      <Grid Background="Transparent"
            x:Name="Parent"
            RenderTransformOrigin="0.5,0.5">
        <Grid.Resources>
          <Storyboard x:Key="ShowAnim">
            <DoubleAnimation To="0" Duration="0:0:0.2"
                 Storyboard.TargetName="Parent"
                 Storyboard.TargetProperty="(UIElement.RenderTransform).(
                    TransformGroup.Children)[0].(RotateTransform.Angle)"/>
            <DoubleAnimation To="1" Duration="0:0:0.2"
                  Storyboard.TargetName="Parent"
                  Storyboard.TargetProperty="(UIElement.RenderTransform).(
                    TransformGroup.Children)[1].(ScaleTransform.ScaleX)"/>
            <DoubleAnimation To="1" Duration="0:0:0.2"
                  Storyboard.TargetName="Parent"
                  Storyboard.TargetProperty="(UIElement.RenderTransform).(
                    TransformGroup.Children)[1].(ScaleTransform.ScaleY)"/>
          </Storyboard>
          <Storyboard x:Key="HideAnim">
            <DoubleAnimation To="120" Duration="0:0:0.2"
                  Storyboard.TargetName="Parent"
                  Storyboard.TargetProperty="(UIElement.RenderTransform).(
                    TransformGroup.Children)[0].(RotateTransform.Angle)"/>
            <DoubleAnimation To="0" Duration="0:0:0.2"
                  Storyboard.TargetName="Parent"
                  Storyboard.TargetProperty="(UIElement.RenderTransform).(
                    TransformGroup.Children)[1].(ScaleTransform.ScaleX)"/>
            <DoubleAnimation To="0" Duration="0:0:0.2"
                  Storyboard.TargetName="Parent"
                  Storyboard.TargetProperty="(UIElement.RenderTransform).(
                    TransformGroup.Children)[1].(ScaleTransform.ScaleY)"/>
          </Storyboard>
        </Grid.Resources>
        <Grid.RenderTransform>
          <TransformGroup>
            <RotateTransform Angle="120"/>
            <ScaleTransform ScaleX="0" ScaleY="0"/>
          </TransformGroup>
        </Grid.RenderTransform>
        ... category button template here ...
      </Grid>
    </ControlTemplate>
  </Setter.Value>
</Setter>

为了播放上述动画以显示磁贴,我们必须找到将为每个磁贴创建的 storyboards。为了使“显示”效果更有趣,以下代码通过根据相邻磁贴动画触发之间的所需延迟来设置 BeginTime 属性来“准备”每个类别磁贴的 storyboards。

// sets the begin time for each animation
private static void PrepareCategoryViewStoryboards(ItemsControl itemsControl, 
                    TimeSpan delayBetweenElement)
{
  TimeSpan startTime = new TimeSpan(0);
  var elements = itemsControl.ItemsSource.Cast<FrameworkElement>().ToList();
  foreach (FrameworkElement element in elements)
  {
    var showStoryboard = GetStoryboardFromRootElement(element, "ShowAnim");
    if (showStoryboard != null)
    {
      showStoryboard.BeginTime = startTime;
    }
    var hideStoryboard = GetStoryboardFromRootElement(element, "HideAnim");
    if (hideStoryboard != null)
    {
      hideStoryboard.BeginTime = startTime;
      if (element == elements.Last())
      {
        // when the last animation is complete, hide the ItemsControl
        hideStoryboard.Completed += (s, e) =>
        {
          itemsControl.Opacity = 0;
        };
      }
    }
    startTime = startTime.Add(delayBetweenElement);
  }
}
private static Storyboard GetStoryboardFromRootElement(
               FrameworkElement element, string storyboardName)
{
  FrameworkElement rootElement = element.Elements().Cast<FrameworkElement>().First();
  return rootElement.Resources[storyboardName] as Storyboard;
}

为了显示类别视图,我们只需遍历所有磁贴,触发动画。

// plays the animations associated with each child element
public static void ShowChildElements(ItemsControl itemsControl, 
                                     TimeSpan delayBetweenElement)
{
  itemsControl.Opacity = 1;
  PrepareCategoryViewStoryboards(itemsControl, delayBetweenElement);
  foreach (FrameworkElement element in itemsControl.ItemsSource)
  {
    var showStoryboard = GetStoryboardFromRootElement(element, "ShowAnim");
    if (showStoryboard != null)
    {
      showStoryboard.Begin();
    }
    else
    {
      element.Visibility = Visibility.Visible;
    }
  }
}

这使得控件在两个视图之间的过渡更加有趣。

JumpListAnimation.png

请注意,引入此类别视图动画的方式意味着控件的客户端可以通过简单地提供另一个 CategoryButtonTemplate 来更改动画。

摘要

这差不多就是本文的结尾了。希望您喜欢阅读关于该控件开发的介绍。如果您在 Windows Phone 7 应用程序中使用它,请通过在下方发表评论告诉我。另外,如前所述,如果您想阅读用户指南或查看该控件的一些实际示例,请访问我的博客

© . All rights reserved.