本文章是由機器翻譯。

使用者介面前沿

Windows Phone 7 中的頁面轉換

查理斯 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>

我不會選擇顯示這些標題和標題以所有大寫字母,但這就是他們中的顯示方式原始檔和程式盲目拉出來完全基於這些行被之前,至少三個空行。

圖 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 顯示一章開頭。("冥頑不靈"的人在每一章開頭題詞,有些人寫的自己的喬治 · 艾略特)。

圖 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;
  }
}

圖 8滑入視圖的頁面

您可以從應用程式欄功能表中選擇其它兩個的頁面過渡效果: FlipTransition 類似于 SlideTransition,只是它使用 PlaneProjection 變換 3D 看看。因為如果右上角拉回來,CurlTransition 將顯示的頁面。(我原來寫的 CurlTransition 代碼從右下角,捲曲,能夠將它轉換只需通過調整所有水準的座標。您可以更改它回右下角捲曲的 curlFromTop 常數設置為 false 並重新編譯。)

MiddlemarchReader 允許使用者跳轉到一個章節的開始,因為在該程式的頂部顯示實際頁碼是不再可行。相反,該程式顯示基於文本長度的百分比。

分頁的恐懼

在實際使用中,MiddlemarchReader 開始看,感覺很像真正的電子書閱讀器。但是,它仍在遭受其分頁邏輯表現不佳。事實上,冥頑不靈的"人"的第一章的第一頁是有點時,延遲程式運行 Windows Phone 7 的設備上,因為本章開頭的幾個頁面進行下去的段落。分頁後後跳轉到一個新的段落有時需要第二個左右,與我還沒有足夠的勇氣因為這些功能需要重新分頁引入該程式的使用者選定的字體或字體大小。

加快分頁的方式?也許。無疑這種改善將優雅、 聰明和比預料的要困難。

Charles Petzold  是一名長期特約編輯 MSDN 雜誌*.*他最近的書,"程式設計 Windows 7 手機"(微軟出版社,2010年),是可免費下載在bit.ly/cpebookpdf