用户界面前沿

Windows Phone 7 中的页面过渡

查尔斯 Petzold

下载代码示例

Charles Petzold
三个规则存在几种不同学科。在喜剧,例如,规则的三个指示一个笑话开始进行观众的兴趣,然后提高预期和最后,可能比预期的有点不太滑稽。

在经济学中,三个规则涉及的三个主要的竞争对手,在特定市场中存在。在编程中,它更像是一个三罢工规则: 只要一块类似代码出现了三次,它应重构公共回路或方法。(最初听说此规则为"三个或更多吗?使用用于。"; 也许我组成该静乐自己)

这将我们带入文本布局的主题。有时程序员需要显示是太长,无法在单个屏幕上的文档。也许最不可取的方法包括缩小字体大小,直到该文档适合。更多传统的解决方案是一个滚动条。但近年来 — — 和特别是在小型设备如片和电话 — — 已分页文件,让用户翻页,好像阅读真正印刷的书籍或杂志的趋势。

显示分页的文件也涉及到三规则: 最流体的页面过渡效果,为用户界面需要支持三种不同页面 — — 当前页、 下一页和上一页。随着用户页面转发,当前页前一页,下一页成为当前页面。倒退,当前页成为下一个页面,在前一页成为当前页。在本文中,我将介绍如何实现这种技术具有足够的灵活性,为三个不同的页面过渡效果的方式 — — 页幻灯片、 3D 样翻转和 2D 页卷曲。

回古腾堡

在此列的上一期 (msdn.microsoft.com/magazine/hh205757),我演示了一些相当简单分页逻辑在名为 EmmaReader,这么说是因为它可以让你读简 · 奥斯汀小说"埃玛"(1815) 的 Windows Phone 7 程序中。程序使用从著名的项目古腾堡 Web 站点上下载一个纯文本文件 (在gutenberg.org),这使得可用 3 万多名公共领域的书籍。

分页是时间的一个非平凡的过程,可以要求明显量。对进行这一原因,EmmaReader 只分页需求随着用户通过这本书。后创建的页面,该程序将存储信息指示书中该页面开始位置和结束。然后,它可以让用户页面中通过这本书向后使用此信息。

本文中的代码,我选择了乔治 · 艾略特的小说,冥顽不灵的"人"(1874),并称为可下载的 Windows Phone 7 项目 MiddlemarchReader。此程序表示为基于古登堡纯文本文件的 Windows Phone 7 的广义的电子书阅读器更接近另一步。您不能使用此电子书阅读器为当前的畅销书,但它肯定会让你探索英语文学的经典。

古登堡纯文本文件将每个段落划分为多个连续的 72 个字符行。一个空行分隔段落。这意味着,任何希望分页和演示文稿的古腾堡项目文件需要将这些单独的行串联到单行段落格式的程序。在 MiddlemarchReader,这发生在 GenerateParagraphs 方法中的 PageProvider 类。这本书加载,每次调用此方法并将各段存储类型字符串的简单泛型列表对象中。

这段串联逻辑分崩离析中至少两个例: 当整个段落缩进时,程序不连接这些行,所以单独包装的每一行。本书包含诗歌,而不是散文时发生相反的问题: 程序错误地将这些行连接起来。这些问题是不可能避免未经审查的实际文本,这是相当困难,无需人为干预的语义。

新的一章

大多数的书也分为章节,并允许用户跳转到特定章节是一个重要的功能的电子书阅读器。在 EmmaReader,我被忽略的章节,但 MiddlemarchReader PageProvider 类包括确定每个章节的开始位置的 GenerateChapters 方法。一般情况下,古腾堡项目文件确定的界限与三个或四个空白的线条,取决于这本书的章节。为"冥顽不灵"的人,三个空行之前指示章节标题行。GenerateChapters 方法使用此功能来确定特定段落每一章的开始位置。

本书分为章节有助于减轻电子书阅读器的分页问题。例如,假定读者到了尽头的一本书,并决定更改字体大小。(EmmaReader 和 MiddlemarchReader 都不具有此功能,但从理论上说。无章的内部分歧,整个预订点需要重新分页。但如果这本书分为章节,只有当前的章需要重新分页。回在机械类型的天,章节书分成帮助排字中完全相同的方式时不得不插入或删除的行。

MiddlemarchReader 程序将书籍信息保存在独立存储中。坚持此信息的主类称为 AppSettings。AppSettings 引用一个 BookInfo 对象,它包含每章都有一个 ChapterInfo 对象的集合。这些 ChapterInfo 对象包含章节的标题,这一章的 PageInfo 对象的集合。PageInfo 对象指示开始基于 ParagraphIndex 的引用的每个段落,字符串列表和 CharacterIndex 内的索引字符串的每个页面的位置。一章的第一个 PageInfo 对象是在前面所述的 GenerateChapters 方法中创建的。后续的 PageInfo 对象创建和积累,章逐步进行分页,如用户读取这一章。此外保存在 BookInfo 是用户的当前页面中,标识为一个章索引和这一章中的页索引。

"冥顽不灵的人"有 86 章,但 PageProvider 在 GenerateChapters 方法中的逻辑发现 110 的章节包括标题为"冥顽不灵"的人划分为八个"书",并在开头和结尾的文件材料。格式化的电话,这本小说是大约 1800 页或每章大约 20 页。

图 1MainPage.xaml 中显示内容面板。有两个控件,占据单个单元格网格中,但在任何时间,其中只有一位可见。这两个控件包含绑定到 MiddlemarchPageProvider 定义为一种资源类型的对象。不久我将讨论 BookViewer 控制。列表框显示可滚动列表的章节,并调用使用该程序的 ApplicationBar 上的按钮。图 2MiddlemarchReader,滚动到关于中间显示的章节列表。

图 1MainPage.xaml 中的内容面板

<phone:PhoneApplicationPage.Resources>
  <local:MiddlemarchPageProvider x:Key="pageProvider" />
</phone:PhoneApplicationPage.Resources>
...
<Grid x:Name="ContentPanel" Grid.Row="1">
  <local:BookViewer x:Name="bookViewer"
                    PageProvider="{StaticResource pageProvider}"
                    PageChanged="OnBookViewerPageChanged">
    <local:BookViewer.PageTransition>
      <local:SlideTransition />
    </local:BookViewer.PageTransition>
  </local:BookViewer>

  <ListBox Name="chaptersListBox"
           Visibility="Collapsed"
           Background="{StaticResource PhoneBackgroundBrush}"
           ItemsSource="{Binding Source={StaticResource pageProvider}, 
                                 Path=Book.Chapters}"
           FontSize="{StaticResource PhoneFontSizeLarge}"
           SelectionChanged="OnChaptersListBoxSelectionChanged">
    <ListBox.ItemTemplate>
      <DataTemplate>
        <Grid Margin="0 2">
          <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
          </Grid.ColumnDefinitions>
                            
          <TextBlock Grid.Column="0"
                     Text="&#x2022;"
                     Margin="0 0 3 0" />
                        
          <TextBlock Grid.Column="1" 
                     Text="{Binding Title}"
                     TextWrapping="Wrap" />
        </Grid>
      </DataTemplate>
    </ListBox.ItemTemplate>
  </ListBox>
</Grid>

我不会选择显示这些标题和标题以所有大写字母,但这就是他们中的显示方式原始文件和程序盲目拉出来完全基于这些行被之前,至少三个空行。

The Scrollable List of Chapters

图 2可滚动的章节列表

背页问题

对进行像 EmmaReader,MiddlemarchReader 分页上的需求,用户读取这本书,和页信息存储,用户可以向后页。 那里没有问题。 MiddlemarchReader 还允许用户跳转到特定的一章。 没问题,或者,因为该程序已经知道每一章的开始位置。

但是,假定用户跳转到的一章开始,然后决定最后一个向后页当前页上一章。 如果前一章已不被分页,这整个一章必须分页显示的最后一页。 这一段可能短,或它可能很长,取决于这本书和一章。 这是一个问题。

虽然 MiddlemarchReader 对进行分页的需求,它实际上超越一点了。 当用户正在阅读的一页、 下一页和上一页已准备分页向前或向后。 通常情况下,获取这些额外的页并不是一个问题。 但该程序怎么办当用户跳转到的一章开头吗? 该程序应得到准备向后传呼可能性前一章的最后一页吗? 这是一种浪费: 在大多数情况下,用户将只能页向前,这样为什么麻烦吗?

很明显,获取有点棘手。 我已经提到 PageProvider 类负责分析原古腾堡项目文件,并将它划分为段落和章节,但它也负责为分页。 MiddlemarchPageProvider 类中引用图 1很小。 它从 PageProvider 派生,并只访问"米德尔马契"文件,所以它可以由 PageProvider 处理。

我想要尽可能的 PageProvider 内部小知识作为 BookViewer 控制。 为此,BookViewer 的 PageProvider 属性的类型 IPageProvider (PageProvider 所实现的任何接口),作为所示图 3

图 3IPageProvider 接口

public interface IPageProvider
{
  void SetPageSize(Size size);

  void SetFont(FontFamily fontFamily, double FontSize);

  FrameworkElement GetPage(int chapterIndex, int pageIndex);

  FrameworkElement QueryLastPage(int chapterIndex, out int pageIndex);

  FrameworkElement GetLastPage(int chapterIndex, out int pageIndex);
}

下面介绍它的工作原理:BookViewer 对象通知 PageProvider 的每个页面的大小和字体和字体大小,用于创建 TextBlock 元素,包括页面。 大多数情况下,BookViewer 获得页面通过调用 GetPage。 如果 GetPage 返回 null,章索引页不在范围内,与该页面不存在。 这可能表明到 BookViewer,它超越一章结束和需要前进到下一章。 图 4MiddlemarchReader 显示一章开头。 ("冥顽不灵"的人在每一章开头题词,有些人写的自己的乔治 · 艾略特)。

The BookViewer Control Displaying a Page

图 4BookViewer 控件显示一页

当 BookViewer 导航到的一章开头时,IPageProvider 定义了两种方法获取前一章的最后一页。 QueryLastPage 是最快速的方法。 如果已分页完上一章,且可用的最后一页,该方法返回页面和设置的页的索引。 如果该页面不是可用的 QueryLastPage 将返回 null。 这是 BookViewer 调用获取前一页,当用户导航到的一章开头的方法。

QueryLastPage 返回 null 和用户然后页回那丢失的页面,如果 BookViewer 已经没有追索权,但若要调用 GetLastPage。 如有必要,GetLastPage 将分页一整章只是为了获得最后一页。

后在用户定位到一章的开头,为什么不分页前一章的第二个线程的执行? 也许用户决定回页的时间将完成第二个线程。 虽然这肯定是一个合理的解决方案,分页逻辑需要有点改组,因为它创建 StackPanel 与 TextBlock 对象,并且这些不能使用的主线程。

BookViewer 转换

我已经提到,在一般情况下,BookViewer 在一次戏法三页。 该控件派生自 UserControl,和图 5显示 XAML 文件。 三的边框元素,与 pageHost0、 pageHost1 和 pageHost2 的名称作为父母索取 PageProvider 页。 (这些页面是实际上 StackPanel 元素与 TextBlock 页上的每一段)。与以 pageContainer 开头的名称的其他三个的边框元素提供白色背景和页面的边框周围的白色小边距。 这些也是操纵的页面过渡效果的元素。 GestureListener (Silverlight 的 Windows 电话工具包中,可下载从在 CodePlexbit.ly/cB8hxu) 提供触摸输入。

图 5BookViewer 的 XAML 文件

<UserControl x:Class="MiddlemarchReader.BookViewer"
             xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:toolkit="clr-namespace:Microsoft.Phone.Controls;assembly=
               Microsoft.Phone.Controls.Toolkit">

  <UserControl.Resources>
    <Style x:Key="pageContainerStyle" TargetType="Border">
      <Setter Property="BorderBrush" Value="Black" />
      <Setter Property="BorderThickness" Value="1" />
      <Setter Property="Background" Value="White" />
    </Style>

    <Style x:Key="pageHostStyle" TargetType="Border">
      <Setter Property="Margin" Value="12, 6" />
      <Setter Property="CacheMode" Value="BitmapCache" />
    </Style>
  </UserControl.Resources>

  <Grid x:Name="LayoutRoot">
    <toolkit:GestureService.GestureListener>
      <toolkit:GestureListener GestureBegin="OnGestureListenerGestureBegin"
                               GestureCompleted=
                                 "OnGestureListenerGestureCompleted"
                               Tap="OnGestureListenerTap"
                               Flick="OnGestureListenerFlick" 
                               DragStarted="OnGestureListenerDragStarted"
                               DragDelta="OnGestureListenerDragDelta"
                               DragCompleted="OnGestureListenerDragCompleted" />
    </toolkit:GestureService.GestureListener>

    <Border Name="pageContainer0" Style="{StaticResource pageContainerStyle}">
      <Border Name="pageHost0" Style="{StaticResource pageHostStyle}" />
    </Border>

    <Border Name="pageContainer1" Style="{StaticResource pageContainerStyle}">
      <Border Name="pageHost1" Style="{StaticResource pageHostStyle}" />
    </Border>

    <Border Name="pageContainer2" Style="{StaticResource pageContainerStyle}">
      <Border Name="pageHost2" Style="{StaticResource pageHostStyle}" />
    </Border>
  </Grid>
</UserControl>

BookViewer 创建时,它将三个 pageHost 对象存储在名为 pageHosts 的数组中。 名为 pageHostBaseIndex 的字段设置为 0。 如用户通过一本的书页,pageHostBaseIndex 去 1,然后 2,0,然后回到。 作为用户页面向后,pageHostBaseIndex 转从 0 到 2,然后 1,然后回 0。 在任何时间,pageHosts [pageHostBaseIndex] 是当前页。 下一步的页面是:

pageHosts[(pageHostBaseIndex + 1) % 3]

前一页是:

pageHosts[(pageHostBaseIndex + 2) % 3]

此计划下,一旦某一特定页已设置的页的主机,它留在那里直到它被更换。 程序并不需要的页面转移到另一页主机。

这项计划还允许相当简单页面过渡效果: 只需使用 Canvas.SetZIndex,pageHosts [pageHostBaseIndex] 是另外两个的基础上。 但这可能不是你的页面之间的过渡的方式。 它是而柔和,甚至有点危险。 不精确的触摸输入的是,用户可能会意外地移动未来而不是,两个页面,甚至没有意识到它。 出于此原因,更戏剧性的页面过渡效果是可取的。

BookViewer 通过定义类型 PageTransition,抽象类中所示的 PageTransition 属性达到极大的灵活性,对页面过渡效果图 6

图 6PageTransition 类

public abstract class PageTransition : DependencyObject
{
  public static readonly DependencyProperty FractionalBaseIndexProperty =
    DependencyProperty.Register("FractionalBaseIndex",
      typeof(double),
      typeof(PageTransition),
      new PropertyMetadata(-1.0, OnTransitionChanged));

  public double FractionalBaseIndex
  {
    set { SetValue(FractionalBaseIndexProperty, value); }
    get { return (double)GetValue(FractionalBaseIndexProperty); }
  }

  public virtual double AnimationDuration 
  {
    get { return 1000; }
  }

  static void OnTransitionChanged(DependencyObject obj, 
                                  DependencyPropertyChangedEventArgs args)
  {
    (obj as PageTransition).OnTransitionChanged(args);
  }

  void OnTransitionChanged(DependencyPropertyChangedEventArgs args)
  {
    double fraction = (3 + this.FractionalBaseIndex) % 1;
    int baseIndex = (int)(3 + this.FractionalBaseIndex - fraction) % 3;
    ShowPageTransition(baseIndex, fraction);
  }

  public abstract void Attach(Panel containerPanel,
                              FrameworkElement pageContainer0,
                              FrameworkElement pageContainer1,
                              FrameworkElement pageContainer2);

  public abstract void Detach();

  protected abstract void ShowPageTransition(int baseIndex, double fraction);
}

PageTransition 定义了名为 FractionalBaseIndex 的 BookViewer 是负责设置基于触摸输入一个依赖项属性。 如果用户点击屏幕,FractionalBaseIndex 被上升 DoubleAnimation 从 pageHostBaseIndex 到 pageHostBaseIndex 加 1。 如果用户甩出屏幕,也会触发动画。 如果基于距离的手指沿屏幕,FractionalBaseIndex"手动"设置的用户拖动用户已拖他的手指在屏幕的总宽度的百分比。

PageTransition 类 FractionalBaseIndex 分成整数 baseIndex 和一小部分为派生类的利益。 ShowPageTransition 是由是特定的过渡特征的方法的派生类实现的。 默认值是 SlideTransition,所示图 7,该幻灯片来回的页面。 图 8显示正在滑到视图的页面。 类附加到页面容器的 TranslateTransform 对象附加呼叫过程中,他们在分离过程中删除。

图 7SlideTransition 类

public class SlideTransition : PageTransition
{
  FrameworkElement[] pageContainers = new FrameworkElement[3];
  TranslateTransform[] translateTransforms = new TranslateTransform[3];

  public override double AnimationDuration
  {
    get { return 500; }
  }

  public override void Attach(Panel containerPanel,
                              FrameworkElement pageContainer0, 
                              FrameworkElement pageContainer1, 
                              FrameworkElement pageContainer2)
  {
    pageContainers[0] = pageContainer0;
    pageContainers[1] = pageContainer1;
    pageContainers[2] = pageContainer2;

    for (int i = 0; i < 3; i++)
    {
      translateTransforms[i] = new TranslateTransform();
      pageContainers[i].RenderTransform = translateTransforms[i];
    }
  }

  public override void Detach()
  {
    foreach (FrameworkElement pageContainer in pageContainers)
      pageContainer.RenderTransform = null;
  }

  protected override void ShowPageTransition(int baseIndex, double fraction)
  {
    int nextIndex = (baseIndex + 1) % 3;
    int prevIndex = (baseIndex + 2) % 3;

    translateTransforms[baseIndex].X = -fraction * 
      pageContainers[prevIndex].ActualWidth;
    translateTransforms[nextIndex].X = translateTransforms[baseIndex].X + 
      pageContainers[baseIndex].ActualWidth;
    translateTransforms[prevIndex].X = translateTransforms[baseIndex].X – 
      pageContainers[prevIndex].ActualWidth;
  }
}

A Page Sliding into View

图 8滑入视图的页面

您可以从应用程序栏菜单中选择其它两个的页面过渡效果: FlipTransition 类似于 SlideTransition,只是它使用 PlaneProjection 变换 3D 看看。 因为如果右上角拉回来,CurlTransition 将显示的页面。 (我原来写的 CurlTransition 代码从右下角,卷曲,能够将它转换只需通过调整所有水平的坐标。 您可以更改它回右下角卷曲的 curlFromTop 常数设置为 false 并重新编译。)

MiddlemarchReader 允许用户跳转到一个章节的开始,因为在该程序的顶部显示实际页码是不再可行。 相反,该程序显示基于文本长度的百分比。

分页的恐惧

在实际使用中,MiddlemarchReader 开始看,感觉很像真正的电子书阅读器。 但是,它仍在遭受其分页逻辑表现不佳。 事实上,冥顽不灵的"人"的第一章的第一页是有点时,延迟程序运行 Windows Phone 7 的设备上,因为本章开头的几个页面进行下去的段落。 分页后后跳转到一个新的段落有时需要第二个左右,与我还没有足够的勇气因为这些功能需要重新分页引入该程序的用户选定的字体或字体大小。

加快分页的方式? 也许。 无疑这种改善将优雅、 聪明和比预料的要困难。

查尔斯 Petzold 是一名长期特约编辑 MSDN 杂志*.*他最近的书,"编程 Windows 7 手机"(微软出版社,2010年),是可免费下载在bit.ly/cpebookpdf