Este artigo foi traduzido por máquina.

Fronteiras da interface do usuário

Transições de páginas no Windows Phone 7

Charles Petzold

Baixe o código de exemplo

Charles PetzoldA regra de três existe em várias disciplinas diferentes. Na comédia, por exemplo, a regra de três ordens que uma piada começam por envolver o interesse do público, levantar, em seguida, antecipação e, finalmente, ser um pouco menos engraçado do que o previsto.

Em economia, a regra de três refere-se à existência de três principais concorrentes em um mercado específico. Na programação, é mais como uma regra de três-greve: sempre que um pedaço de código semelhante aparece três vezes, deve ser reformulado em um loop comum ou método. (Originalmente ouvi esta regra como "três ou mais? Uso uma para. " Ou talvez eu fiz até esse jingle).

Isso nos leva ao assunto de layout de texto. Às vezes, os programadores precisam exibir um documento que é muito grande para caber em uma única tela. Talvez a abordagem menos desejável envolve diminuindo o tamanho da fonte até que o documento se encaixa. Uma solução mais tradicional é uma barra de rolagem. Mas nos últimos anos — e particularmente em dispositivos pequenos, como comprimidos e telefones — tem havido uma tendência em direção a paginação do documento e permitindo que o usuário folhear as páginas como se lendo um livro impresso real ou revista.

Exibindo um documento paginado também envolve uma regra de três: para as transições de página mais fluidas, a interface do usuário precisa oferecer suporte a três páginas distintas — a página atual, a próxima página e página anterior. Como encaminhar as páginas de usuário, a página atual se torna a página anterior e próxima página torna-se a página atual. Indo para trás, a página atual torna-se a próxima página e página anterior torna-se a página atual. Neste artigo, vou descrever como implementar essa técnica de maneira flexível o suficiente para três transições de página diferentes — um slide de página, um flip 3D-como e um 2D page curl.

Voltar à Gutenberg

Na edição anterior desta coluna (msdn.microsoft.com/magazine/hh205757), eu demonstrei alguma lógica de paginação bastante simples em um programa de Windows Phone 7 chamado EmmaReader, assim chamado porque ele permite que você leia romance de Jane Austen "Emma" (1815). O programa usa um arquivo de texto sem formatação baixado do famoso Project Gutenberg Web site (em gutenberg.org), que disponibiliza mais de 30.000 livros de domínio público.

A paginação é um processo não-trivial que pode exigir uma quantidade considerável de tempo. Por esse motivo, EmmaReader pagina apenas sob demanda conforme o andamento do usuário através do livro. Depois de criar uma página, o programa armazena informações indicando onde o livro nessa página começa e termina. Ele, em seguida, pode usar esta informação para deixar a página de usuário para trás com o livro.

Para o código neste artigo, eu escolhi o romance de George Eliot, "middlemarch" (1874) e chamou o projeto Windows Phone 7 para download MiddlemarchReader. Este programa representa outro passo mais perto de uma generalizada e-book reader para Windows Phone 7 com base em arquivos de texto sem formatação do projeto Gutenberg. Você não será capaz de usar este e-book reader para Best-sellers atuais, mas certamente vai deixar você explorar os clássicos da literatura inglesa.

Os arquivos de texto sem formatação do projeto Gutenberg dividem cada parágrafo em várias linhas de 72 caracteres consecutivas. Parágrafos são separados por uma linha em branco. Isso significa que qualquer programa que deseja formatar que um arquivo do Project Gutenberg para paginação e apresentação precisa para concatenar essas linhas separadas em números de linha única. Em MiddlemarchReader, isso ocorre no método GenerateParagraphs da classe PageProvider. Esse método é chamado sempre que o livro é carregado, e ele armazena os parágrafos em um simple objeto de lista genérico do tipo String.

Esta lógica de concatenação de n º cai por terra pelo menos dois casos: quando um parágrafo inteiro é recuado, o programa não concatenar essas linhas, para que cada linha é empacotada separadamente. O problema oposto ocorre quando contém um livro de poesia, em vez de prosa: O programa por engano concatena as linhas. Estes problemas são impossíveis de evitar sem examinar a semântica do texto real, que é bastante difícil sem intervenção humana.

Um novo capítulo

A maioria dos livros também são divididos em capítulos, e permitindo que um usuário saltar para um determinado capítulo é uma característica importante de um leitor de e-book. Em EmmaReader, eu ignorei capítulos, mas a classe de PageProvider de MiddlemarchReader inclui um método GenerateChapters que determina onde cada capítulo começa. Geralmente, um arquivo de projeto Gutenberg delimita capítulos com três ou quatro linhas em branco, dependendo do livro. Para "middlemarch," três linhas em branco precedem uma linha que indica o título do capítulo. O método GenerateChapters usa esse recurso para determinar o número especial onde cada capítulo começa.

Dividindo um livro em capítulos ajuda a aliviar o problema de paginação em um leitor de e-book. Por exemplo, suponha que um leitor está perto do fim de um livro e decide mudar o tamanho da fonte. (Nem EmmaReader nem MiddlemarchReader ter esse recurso, mas eu estou falando teoricamente). Sem divisões de capítulo, todo o livro a que ponto precisa ser repaginated. Mas se o livro é dividido em capítulos, apenas o capítulo atual deve ser repaginated. Para trás nos dias do tipo mecânico, divisão de um livro em capítulos ajudou os paginadores exatamente da mesma maneira quando linhas tinham que ser inserido ou excluído.

O programa MiddlemarchReader salva informações sobre o livro no armazenamento isolado. A classe principal para persistir esta informação é chamada AppSettings. AppSettings faz referência a um objeto BookInfo que contém uma coleção de objetos ChapterInfo, um para cada capítulo. Esses objetos ChapterInfo contenham o título do capítulo e uma coleção de objetos PageInfo para esse capítulo. Os objetos PageInfo indicam onde cada página começa com base em um ParagraphIndex que faz referência a lista de seqüências de caracteres para cada parágrafo, e um CharacterIndex em que indexado Cadeia de caracteres. O primeiro objeto PageInfo para um capítulo é criado no método GenerateChapters que descrevi anteriormente. Subseqüentes PageInfo objetos são criados e acumulados como o capítulo é paginado progressivamente conforme o usuário lê esse capítulo. Também salva em BookInfo é a página do usuário atual, identificada como um capítulo de índice e um índice de página no interior do capítulo.

"middlemarch" tem 86 capítulos, mas a lógica no método GenerateChapters em PageProvider encontra 110 capítulos, incluindo títulos para a divisão de "middlemarch" em oito "livros" e material no início e no final do arquivo. Formatado para o telefone, o romance é aproximadamente 1.800 páginas, ou cerca de 20 páginas por capítulo.

Figura 1 mostra o painel conteúdo na MainPage.xaml. Há dois controles que ocupam a grade de célula única, mas a qualquer momento, apenas um deles é visível. Ambos os controles contêm ligações para um objeto do tipo que middlemarchpageprovider definido como um recurso. Falarei sobre o controle de BookViewer em breve. O ListBox exibe uma lista rolável de capítulos, e você invocá-lo com um botão no ApplicationBar do programa. Figura 2 mostra a lista de capítulos em MiddlemarchReader, rolado sobre o meio.

Figura 1 O painel conteúdo em MainPage.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>

Eu não teria escolhido para exibir esses títulos e títulos com letras maiúsculas, mas que é como eles aparecem no arquivo original e o programa cegamente puxa-los fora baseados unicamente nestas linhas sendo precedidas pelo menos três linhas em branco.

The Scrollable List of Chapters

Figura 2 A lista rolável de capítulos

O problema de página Back

Como EmmaReader, MiddlemarchReader pagina sob demanda como o usuário lê o livro, e as informações da página são armazenadas para que o usuário pode página para trás. Nenhum problema lá. MiddlemarchReader também permite que o usuário saltar para um capítulo específico. Não há problema, qualquer, porque o programa já sabe onde começa a cada capítulo.

No entanto, suponha que o usuário vai para o início de um capítulo e, em seguida, decida a página para trás para a última página do capítulo anterior. Se o capítulo anterior ainda não foi paginado, esse capítulo inteiro deve ser paginado para mostrar essa última página. O parágrafo pode ser curto, ou poderia ser bastante longo, dependendo do livro e o capítulo. Isso é um problema.

Embora MiddlemarchReader pagina sob demanda, ele realmente vai um pouco além disso. Está pronto para frente ou para trás de paginação quando o usuário está lendo uma página, a próxima página e página anterior. Normalmente, a obtenção dessas páginas adicionais não é um problema. Mas o que o programa fazer se o usuário salta para o início de um capítulo? O programa deve começar a última página do capítulo anterior para estar pronto para a possibilidade de paginação para trás? Que é um desperdício: na maioria dos casos, o usuário será apenas página frente, isso por que se preocupar?

Obviamente, fica um pouco complicado. Já mencionei que a classe PageProvider é responsável por analisar o arquivo original do Project Gutenberg e dividindo-a em parágrafos e capítulos, mas ele também é responsável por paginação. A classe MiddlemarchPageProvider referenciada no Figura 1 é minúsculo. Ele deriva de PageProvider e simplesmente acessa o arquivo "middlemarch" para que ele pode ser processado por PageProvider.

Eu queria o controle BookViewer para ter como pouco conhecimento de internos de PageProvider quanto possível. Por esse motivo, a propriedade de PageProvider de BookViewer é do tipo IPageProvider (uma interface implementada por PageProvider), como mostrado na Figura 3.

Figura 3 A Interface IPageProvider

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

Eis como funciona: O BookViewer objeto informa PageProvider do tamanho de cada página e a fonte e o tamanho da fonte a ser usado para criar os TextBlock elementos que compõem a página. Na maioria das vezes, BookViewer Obtém páginas chamando GetPage. Se GetPage retorna null, o índice de capítulo ou índice de página está fora do intervalo e essa página não existe. Isso poderia indicar a BookViewer que é ido além do fim de um capítulo e precisa avançar para o próximo capítulo. Figura 4 mostra MiddlemarchReader no início do capítulo. (Cada capítulo em "middlemarch" começa com uma epígrafe, alguns deles escrito por George Eliot, ela mesma).

The BookViewer Control Displaying a Page

Figura 4 O controle de BookViewer, exibindo uma página

Quando BookViewer navega para o início de um capítulo, IPageProvider define dois métodos para obter a última página do capítulo anterior. QueryLastPage é o método rápido. Se o capítulo anterior já foi paginado, e essa última página estiver disponível, o método retorna a página e define o índice da página. Se a página não estiver disponível, QueryLastPage retorna null. Este é o método que chama BookViewer para obter a página anterior quando o usuário navega para o início de um capítulo.

Se QueryLastPage retorna null e o usuário então páginas volta para essa página ausente, BookViewer não tem nenhum recurso mas para chamar GetLastPage. Se necessário, GetLastPage á pagina um capítulo inteiro apenas para obter a última página.

Por que não paginate o capítulo anterior em um segundo segmento de execução após o usuário navega para o início de um capítulo? Talvez o segundo segmento será concluído quando que o usuário decide a página novamente. Apesar de que é certamente uma solução plausível, a lógica de paginação precisaria ser reestruturada um pouco porque ele está criando objetos StackPanel e TextBlock, e estas não podem ser usadas pelo segmento primário.

As transições de BookViewer

Como já mencionei, no caso geral, BookViewer é malabarismo três páginas ao mesmo tempo. O controle deriva de UserControl, e Figura 5 mostra o arquivo XAML. Os três elementos de borda com nomes de pageHost0, pageHost1 e pageHost2 são usados como pais das páginas obtidos a partir de PageProvider. (Estas páginas são realmente StackPanel elementos com um TextBlock para cada parágrafo na página). Os outros três elementos fronteira com nomes que começam com pageContainer fornecem um fundo branco e uma pouca margem de branco ao redor as bordas da página. Estes também são os elementos manipulados para transições de página. Um GestureListener (disponível no Silverlight para Windows telefone Toolkit, disponível para download do CodePlex na bit.ly/cB8hxu) fornece entrada por toque.

Figura 5 O arquivo XAML para BookViewer

<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>

Quando BookViewer é criado, ele armazena os três objetos de pageHost em uma matriz chamada pageHosts. Um campo chamado pageHostBaseIndex é definido como 0. Como o usuário páginas através de um livro, pageHostBaseIndex vai para 1 e 2, em seguida, de volta para 0. Como as páginas de usuário para trás, pageHostBaseIndex vai de 0 a 2, depois 1 e, em seguida, voltar para 0. A qualquer momento, pageHosts [pageHostBaseIndex] é a página atual. A próxima página é:

pageHosts[(pageHostBaseIndex + 1) % 3]

A página anterior é:

pageHosts[(pageHostBaseIndex + 2) % 3]

Com este regime, uma vez que uma determinada página foi definida em um host de página, ele permanece lá até que ele seja substituído. O programa não precisa transferir páginas do host de uma página para outra.

Esse regime também permite transições de página bastante simples: basta usar Canvas.SetZIndex para que pageHosts [pageHostBaseIndex] é sobre os outros dois. Mas isso não é provavelmente a maneira que você quer fazer a transição entre páginas. É bastante agradável e até um pouco perigosa. A imprecisão da entrada por toque é tal que o usuário pode acidentalmente mover duas páginas à frente em vez de um e nem mesmo realizá-lo. Por esta razão, as transições de página mais dramáticas são desejáveis.

BookViewer atinge grande flexibilidade com relação a transições de página, definindo uma propriedade PageTransition do tipo PageTransition, uma classe abstrata mostrada na Figura 6.

Figura 6 A classe PageTransition

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 define uma propriedade de dependência única chamada FractionalBaseIndex que BookViewer é responsável pela configuração com base na entrada por toque. Se o usuário toques na tela, FractionalBaseIndex é aumentado por um DoubleAnimation de pageHostBaseIndex para pageHostBaseIndex mais 1. Animações são acionadas também se o usuário de movimentos de tela. Se o arrasta usuário que um dedo ao longo da tela, FractionalBaseIndex é definir "manualmente" com base na distância o usuário tem arrastado o dedo como uma porcentagem da largura total da tela.

A classe PageTransition separa FractionalBaseIndex em um baseIndex inteiro e uma fração em benefício de classes derivadas. O ShowPageTransition é implementada por classes derivadas de uma forma que é característica da transição particular. O padrão é SlideTransition, mostrado na a Figura 7, que desliza as páginas e para trás. Figura 8 mostra uma página sendo deslizou em exibição. A classe anexa TranslateTransform objetos para os recipientes de página durante a chamada de anexar e remove-los durante desanexar.

Figura 7 A classe SlideTransition

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

Figura 8 uma página deslizando no modo de exibição

Você pode selecionar as outras duas transições de página no menu da barra do aplicativo: FlipTransition é semelhante ao SlideTransition, exceto que ele usa a transformação de PlaneProjection para uma aparência 3D. O CurlTransition exibe a página como se o canto superior direito é puxado para trás. (Eu originalmente escreveu o código CurlTransition para ondular do canto inferior direito e foi capaz de converter simplesmente ajustando todas as coordenadas horizontais. Você pode alterá-lo voltar para a onda inferior-direito, definindo a constante curlFromTop como false e recompilar.)

Porque MiddlemarchReader permite que o usuário saltar para o início de um capítulo, exibir números de página na parte superior do programa não é mais viável. Em vez disso, o programa exibe percentagens baseadas em comprimentos de texto.

Medo de paginação

Em uso real, MiddlemarchReader está começando a olhar e sentir muito parecido com um real e-book reader. No entanto, ele ainda sofre com o fraco desempenho da sua lógica de paginação. Na verdade, a primeira página do primeiro capítulo de "middlemarch" está atrasada um pouco quando o programa está sendo executado em um dispositivo Windows Phone 7 porque o capítulo começa com um parágrafo que passa por várias páginas. Paginação voltar depois de saltar para um novo parágrafo, por vezes, exige um segundo ou assim, e eu ainda não tenho sido corajoso o suficiente para apresentar fontes selecionadas pelo usuário ou tamanhos de fonte no programa porque esses recursos exigem repaginação.

Existe uma maneira de acelerar a paginação? Talvez. Sem dúvida, essa melhoria seria elegante, inteligente e mais difícil do que eu antecipo.

Charles Petzold é um editor antigo MSDN Magazine*.*Seu livro recente, "Programação Windows Phone 7" (Microsoft Press, 2010), está disponível como um download gratuito em bit.ly/cpebookpdf.