Fronteiras da interface do usuário

Gestos de toque no Windows Phone

Charles Petzold

Baixar o código de exemplo

image: Charles Petzold Como alguém que passa grande parte de sua vida profissional observando a evolução das APIs, tenho me entretido bastante com aquela pequena porção do universo de APIs ocupada pelo multitoque. Não tenho certeza se gostaria de contar o número de diferentes APIs multitoque espalhadas pelo Windows Presentation Foundation (WPF), Microsoft Surface, Silverlight, XNA e Windows Phone, mas o que fica mais evidente é que uma “teoria unificada” de multitoque ainda é algo ilusório.

É claro que essa superabundância de APIs de toque não deveria ser surpreendente para uma tecnologia que ainda é comparativamente jovem. Além disso, o multitoque é mais complexo do que o mouse. Isso se deve em parte à interação em potencial de vários dedos, mas também reflete a diferença entre um dispositivo puramente artificial, como o mouse, e nossos dedos. Nós humanos temos toda uma vida usando nossos dedos e esperamos que eles interajam com o mundo de formas bem conhecidas, mesmo que estejamos tocando a superfície lisa de um monitor de vídeo.

Para o programador de aplicativos, o Windows Phone 7 define quatro — sim, quatro — interfaces de toque diferentes.

Os aplicativos Silverlight criados para Windows Phone 7 têm a opção de obter entrada de toque de nível baixo por meio do evento estático Touch.FrameReported ou entrada de nível superior através de diversos eventos Manipulation roteados. Esses eventos Manipulation são principalmente um subconjunto de eventos semelhantes no WPF, mas diferentes o suficiente para causar grandes dores de cabeça.

Os aplicativos XNA para Windows Phone 7 usam a classe estática TouchPanel para obter entrada de toque, mas essa classe sozinha na verdade incorpora duas interfaces de toque: o método GetState obtém a atividade de dedos de nível baixo e o método ReadGesture obtém gestos de nível superior. Os gestos suportados pelo método ReadGesture não são gestos parecidos com os de uma caneta, como marcas de seleção e círculos. São gestos bem mais simples descritos por nomes como Tocar, Arrastar e Apertar. Para manter a arquitetura XNA, a entrada de toque é sondada pelo aplicativo e não entregue por meio de eventos.

Os gestos chegam ao Silverlight

Presumi naturalmente que o Silverlight para Windows Phone 7 já tinha um número suficiente de APIs multitoque, por isso fiquei bastante surpreso quando vi uma terceira adicionada à combinação — se bem que em um kit de ferramentas disponibilizado um pouco tarde demais para que eu pudesse descrever no meu livro “Programming Windows Phone 7” (Microsoft Press, 2010).

Como você já deve saber, várias edições do WPF e do Silverlight nos últimos anos têm sido complementadas pelos kits de ferramentas lançados via CodePlex. Esses kits de ferramentas permitem que a Microsoft obtenha novas classes para desenvolvedores que estão fora do ciclo de comercialização usual e, muitas vezes, nos permitem “dar uma espiada” nos aprimoramentos feitos nas estruturas que podem ser incorporados em versões futuras. O código-fonte completo é um bônus extra.

Agora o Windows Phone 7 também é beneficiado com essa prática. O Kit de Ferramentas do Silverlight para Windows Phone (disponível em silverlight.codeplex.com) contém os controles DatePicker, TimePicker e ToggleSwitch já conhecidos dos usuários do Windows Phone 7; um WrapPanel (prático para trabalhar com alterações de orientação do telefone) e suporte a gestos multitoque.

Esse novo suporte a gestos do Silverlight no kit de ferramentas pretende ser semelhante ao método TouchPanel.ReadGesture do XNA, com a exceção de que é entregue via eventos roteados e não por sondagem.

Qual é a semelhança? Muito maior do que eu esperava! Olhando para o código-fonte, fiquei bastante surpreso ao descobrir que esses novos eventos de gesto do Silverlight foram totalmente derivados de uma chamada ao método TouchPanel.ReadGesture do XNA. Eu não imaginava que um aplicativo Silverlight no Windows Phone pudesse chamar esse método XNA, mas ali está ele.

Embora os gestos do Silverlight e do XNA sejam bastante parecidos, as propriedades associadas a eles não são. Por exemplo, as propriedades do XNA usam vetores e, como o Silverlight não inclui uma estrutura Vector (uma omissão que considero absurda), foi preciso redefinir as propriedades do Silverlight de certas formas simples.

À medida que trabalho com esses eventos de gestos, eles se tornaram minha API multitoque preferida para o Silverlight para Windows Phone. Considero-as completas para muitas coisas que preciso fazer e também são absolutamente fáceis de usar.

Vou demonstrar atribuindo tarefas a esses gestos.

Serviço e ouvinte de gestos

Todo o código-fonte dessa coluna está em uma única solução do Visual Studio para download chamada GestureDemos, que contém três projetos. Você deverá ter as ferramentas de desenvolvimento do Windows Phone 7 instaladas, é claro, e também o Kit de Ferramentas do Silverlight para Windows Phone.

Depois de instalar o kit de ferramentas, você poderá usá-lo nos seus projetos do Windows Phone adicionando uma referência ao assembly Microsoft.Phone.Controls.Toolkit. Na caixa de diálogo Adicionar Referência, ele deve estar listado na guia .NET.

Em um arquivo XAML, você precisará de uma declaração de namespace XML como esta (mas tudo deve estar em uma só linha):

xmlns:toolkit=
"clr-namespace:Microsoft.Phone.Controls;
assembly=Microsoft.Phone.Controls.Toolkit"

Aqui estão os 12 eventos de gesto disponíveis, na ordem em que falarei sobre eles (os eventos que agrupei em uma única linha estão relacionados e ocorrem em sequência):

GestureBegin, GestureCompleted
Tap
DoubleTap
Hold
DragStarted, DragDelta, DragCompleted
Flick
PinchStarted, PinchDelta, PinchCompleted

Vamos supor que você manipule os eventos Tap e Hold que ocorrem em um Grid ou em qualquer filho de Grid. É possível especificar isso no arquivo XAML da seguinte maneira:

<Grid .... >
  <toolkit:GestureService.GestureListener>
    <toolkit:GestureListener 
      Tap="OnGestureListenerTap"
      Hold="OnGestureListenerHold" />
  </toolkit:GestureService.GestureListener>
    ...
</Grid>

Você indica os eventos e manipuladores em uma marca GestureListener que é filha da propriedade anexada GestureListener da classe GestureService.

Como alternativa no código, você precisará de uma diretiva de namespace para o namespace Microsoft.Phone.Controls e do seguinte código:

GestureListener gestureListener = 
  GestureService.GetGestureListener(element);

gestureListener.Tap += OnGestureListenerTap;
gestureListener.Hold += OnGestureListenerHold;

Seja qual for o caso, se você estiver definindo esse ouvinte de gestos em um painel, verifique se a propriedade Background está definida pelo menos como Transparent! Os eventos simplesmente falharão em um painel com um plano de fundo padrão nulo.

Tap e Hold

Todos os eventos de gesto são acompanhados por argumentos de evento do tipo GestureEventArgs ou de um tipo derivado de GestureEventArgs. A propriedade OriginalSource indica o elemento mais alto tocado pelo primeiro dedo a tocar na tela; o método GetPosition fornece as coordenadas atuais desse dedo em relação a qualquer elemento.

Os eventos de gesto são roteados, o que significa que podem percorrer a árvore visual e ser manipulados para qualquer elemento que tenha um GestureListener instalado. Como de costume, um manipulador de eventos pode definir a propriedade Handled de GestureEventArgs como true para impedir que um evento percorra a árvore visual mais acima. No entanto, isso afeta somente outros elementos que usam esses eventos de gesto. Definir a propriedade Handled como true não impede que elementos em posições mais altas na árvore visual obtenham entrada de toque por meio de outras interfaces.

O evento GestureBegin indica que um dedo tocou em uma tela antes não sensível a toque; GestureCompleted sinaliza quando todos os dedos foram afastados da tela. Esses eventos podem ser úteis para inicialização ou limpeza, mas de uma forma geral você estará mais concentrado em eventos de gesto que ocorrem entre esses dois eventos.

Não vou me ater muito aos gestos mais simples. Um Tap ocorre quando um dedo toca na tela e depois se afasta em aproximadamente 1,1 segundos, sem se afastar muito da posição original. Se dois toques ocorrerem bem próximos em sequência, o segundo aparecerá como um DoubleTap. Um evento Hold ocorre quando um dedo é pressionado na tela e permanece no mesmo lugar por cerca de 1,1 segundos. O evento Hold é gerado no fim desse tempo sem esperar que o dedo deixe de tocar a tela.

Drag e Flick

Uma sequência Drag — formada por um evento DragStarted, zero ou mais eventos DragDelta e um evento DragCompleted — ocorre quando um dedo toca a tela, se movimenta e deixa de tocar a tela. Como não se sabe que ocorrerá uma ação de arrastar quando o dedo tocar a tela pela primeira vez, o evento DragStarted é adiado até o dedo começar de fato a se movimentar além do limite de Tap. O evento DragStarted pode ser precedido por um evento Hold se o dedo tocou na tela sem se movimentar durante aproximadamente um segundo.

Como o dedo já começou a se movimentar quando o evento DragStarted é disparado, o objeto DragStartedEventArgs pode incluir uma propriedade Direction do tipo Orientation (Horizontal ou Vertical). O objeto DragDeltaEventArgs que acompanha o evento DragDelta inclui mais informações: as propriedades HorizontalChange e VerticalChange, que são convenientes para adicionar às propriedades X e Y de uma TranslateTransform, ou as propriedades anexadas Canvas.Left e Canvas.Top.

O evento Flick ocorre quando um dedo se afasta da tela quando ainda em movimento, sugerindo que o usuário deseja que ocorra inércia. Os argumentos do evento incluem Angle (medido no sentido horário a partir do eixo X positivo) e valores HorizontalVelocity e VerticalVelocity, ambos em pixels por segundo.

O evento Flick pode ocorrer isolado ou entre os eventos DragStarted e DragCompleted, sem nenhum evento DragDelta; ele pode ainda vir após uma série de eventos DragDelta antes de DragCompleted. Geralmente convém manipular os eventos Drag e Flick em conjunto, quase como se Flick fosse uma continuação de Drag. No entanto, você precisará adicionar sua própria lógica de inércia.

Isso é demonstrado no projeto DragAndFlick. A tela contém uma elipse que o usuário simplesmente arrasta com o dedo. Se o dedo se afasta da tela com um movimento, ocorre um evento Flick e o manipulador Flick salva algumas informações e instala um manipulador para o evento CompositionTarget.Rendering. Esse evento — que ocorre em sincronização com a atualização do monitor de vídeo — faz com que a elipse continue se movendo enquanto aplica desaceleração à velocidade.

A manipulação do movimento nas laterais é feita de um modo um pouco diferente: O programa mantém uma posição como se a elipse simplesmente continuasse a se movimentar na mesma direção até parar; essa posição é acrescentada à área em que ela pode saltar.

Me belisque, devo estar sonhando

A sequência Pinch ocorre quando dois dedos tocam a tela; geralmente é interpretada para expandir ou contrair um objeto na tela, possivelmente girando-o também.

Não há dúvida de que a operação de apertar é uma das áreas mais traiçoeiras do processamento multitoque, e não é incomum ver interfaces de nível superior não fornecer informações adequadas. Mais evidente, o evento ManipulationDelta do Windows Phone 7 é particularmente complicado de usar.

Quando se manipulam gestos, as sequências Drag e Pinch são mutuamente exclusivas. Elas não se sobrepõem, mas podem ocorrer de modo consecutivo. Por exemplo, pressione um dedo na tela e arraste. Isso gera um DragStarted e vários eventos DragDelta. Agora pressione um segundo dedo contra a tela. Você obterá um DragCompleted para completar a sequência Drag, seguido de um PinchStarted e de vários eventos PinchDelta. Agora tire o segundo dedo enquanto o primeiro continua se movimentando. Esse é um PinchCompleted para completar a sequência Pinch, seguido de DragStarted e DragDelta. Dependendo do número de dedos que tocam a tela, basicamente você alterna entre sequências Drag e Pinch.

Uma característica útil desse gesto Pinch é que ele não descarta informações. É possível usar propriedades dos argumentos de evento para reconstruir completamente as posições dos dois dedos, assim você sempre poderá voltar aos princípios se necessário.

Durante uma sequência Pinch, a localização atual de um dos dedos — vamos chamá-lo de dedo principal — está sempre disponível com o método GetPosition. Para esta discussão, vamos chamar esse valor de retorno de pt1. Para o evento PinchStarted, a classe PinchStartedGestureEventArgs tem duas propriedades adicionais chamadas Distance e Angle que indicam a localização do segundo dedo em relação ao primeiro. Você pode calcular essa localização facilmente usando a seguinte instrução:

Point pt2 = new Point(pt1.X + args.Distance * Cos(args.Angle),
                      pt1.Y + args.Distance * Sin(args.Angle));

A propriedade Angle está em graus, portanto você precisará dos métodos Cos e Sin para converter em radianos antes de chamar Math.Cos e Math.Sin. Antes de o manipulador PinchStarted ser concluído, você também deve salvar as propriedades Distance e Angle em campos, talvez chamados pinchStartDistance e pinchStartAngle.

O evento PinchDelta é acompanhado por um objeto PinchGestureEventArgs. Mais uma vez, o método GetPosition informa a localização do dedo principal, que talvez saiu da localização original. No caso do segundo dedo, os argumentos de evento fornecem as propriedades DistanceRatio e TotalAngleDelta.

DistanceRatio é a razão da distância atual entre os dedos em relação à distância original, o que significa que você pode calcular a distância atual da seguinte maneira:

double distance = args.DistanceRatio * pinchStartDistance;

TotalAngleDelta é uma diferença entre o ângulo atual entre os dedos e o ângulo original. É possível calcular o ângulo atual assim:

double angle = args.TotalAngleDelta + pinchStartAngle;

Agora você pode calcular a localização do segundo dedo como antes:

Point pt2 = new Point(pt1.X + distance * Cos(angle),
                      pt1.Y + distance * Sin(angle));

Não é preciso salvar informações adicionais em campos durante a manipulação de PinchDelta para processar mais eventos PinchDelta.

O projeto TwoFingerTracking demonstra essa lógica exibindo elipses azuis e verdes que rastreiam um ou dois dedos na tela.

Scale e Rotate

O evento PinchDelta também fornece informações suficientes para colocar em escala e girar objetos. Tive de fornecer meu próprio método de multiplicação de matriz.

Para demonstrar isso, o projeto ScaleAndRotate implementa o que agora é um tipo “tradicional” de demonstração que permite arrastar, colocar em escala e, opcionalmente, girar uma fotografia. Para executar essas transformações, defini o elemento Image com um RenderTransform duplo, como ilustrado na Figura 1.

Figura 1 O elemento Image em ScaleAndRotate

<Image Name="image"
  Source="PetzoldTattoo.jpg"
  Stretch="None"
  HorizontalAlignment="Left"
  VerticalAlignment="Top">
  <Image.RenderTransform>
    <TransformGroup>
      <MatrixTransform x:Name="previousTransform" />

        <TransformGroup x:Name="currentTransform">
          <ScaleTransform x:Name="scaleTransform" />
          <RotateTransform x:Name="rotateTransform" />
          <TranslateTransform x:Name="translateTransform" />
        </TransformGroup>
    </TransformGroup>
  </Image.RenderTransform>
</Image>

Quando uma operação Drag ou Pinch está em andamento, as três transformações no TransformGroup aninhado são manipuladas para mover a imagem pela tela, colocá-la em escala e girá-la. Quando ocorre um evento DragCompleted ou PinchCompleted, o Matrix em MatrixTransform chamado previousTransform é multiplicado pela transformação composta disponível como a propriedade Value de TransformGroup. As três transformações nesse TransformGroup são então redefinidas com seus valores padrão.

As operações de colocar em escala e girar são sempre relativas a um ponto central, que é o ponto que permanece no mesmo lugar quando ocorre a transformação. Uma fotografia colocada em escala ou girada em relação ao seu canto superior esquerdo termina em um lugar diferente do que uma fotografia colocada em escala ou girada em relação ao canto inferior direito.

O código de ScaleAndRotate é exibido na Figura 2. Uso o dedo principal como centro de escala e rotação; esses pontos centrais são definidos nas transformações durante a manipulação de PinchStarted e não mudam no decorrer da sequência Pinch. Durante eventos PinchDelta, as propriedades DistanceRatio e TotalAngleDelta fornecem informações de escala e rotação relativas a esse centro. Qualquer alteração de movimento do dedo principal (que deve ser detectada com um campo salvo) se torna um fator de conversão global.

Figura 2 O código de ScaleAndRotate

public partial class MainPage : PhoneApplicationPage
{
    bool isDragging;
    bool isPinching;
    Point ptPinchPositionStart;

    public MainPage()
    {
        InitializeComponent();
    }

    void OnGestureListenerDragStarted(object sender, DragStartedGestureEventArgs args)
    {
        isDragging = args.OriginalSource == image;
    }

    void OnGestureListenerDragDelta(object sender, DragDeltaGestureEventArgs args)
    {
        if (isDragging)
        {
            translateTransform.X += args.HorizontalChange;
            translateTransform.Y += args.VerticalChange;
        }
    }

    void OnGestureListenerDragCompleted(object sender, 
      DragCompletedGestureEventArgs args)
    {
        if (isDragging)
        {
            TransferTransforms();
            isDragging = false;
        }
    }

    void OnGestureListenerPinchStarted(object sender, 
      PinchStartedGestureEventArgs args)
    {
        isPinching = args.OriginalSource == image;

        if (isPinching)
        {
            // Set transform centers
            Point ptPinchCenter = args.GetPosition(image);
            ptPinchCenter = previousTransform.Transform(ptPinchCenter);

            scaleTransform.CenterX = ptPinchCenter.X;
            scaleTransform.CenterY = ptPinchCenter.Y;

            rotateTransform.CenterX = ptPinchCenter.X;
            rotateTransform.CenterY = ptPinchCenter.Y;

            ptPinchPositionStart = args.GetPosition(this);
        }
    }
    void OnGestureListenerPinchDelta(object sender, PinchGestureEventArgs args)
    {
        if (isPinching)
        {
            // Set scaling
            scaleTransform.ScaleX = args.DistanceRatio;
            scaleTransform.ScaleY = args.DistanceRatio;

            // Optionally set rotation
            if (allowRotateCheckBox.IsChecked.Value)
                rotateTransform.Angle = args.TotalAngleDelta;

            // Set translation
            Point ptPinchPosition = args.GetPosition(this);
            translateTransform.X = ptPinchPosition.X - ptPinchPositionStart.X;
            translateTransform.Y = ptPinchPosition.Y - ptPinchPositionStart.Y;
        }
    }

    void OnGestureListenerPinchCompleted(object sender, PinchGestureEventArgs args)
    {
        if (isPinching)
        {
            TransferTransforms();
            isPinching = false;
        }
    }

    void TransferTransforms()
    {
        previousTransform.Matrix = Multiply(previousTransform.Matrix, 
          currentTransform.Value);

        // Set current transforms to default values
        scaleTransform.ScaleX = scaleTransform.ScaleY = 1;
        scaleTransform.CenterX = scaleTransform.CenterY = 0;

        rotateTransform.Angle = 0;
        rotateTransform.CenterX = rotateTransform.CenterY = 0;

        translateTransform.X = translateTransform.Y = 0;
    }

    Matrix Multiply(Matrix A, Matrix B)
    {
        return new Matrix(A.M11 * B.M11 + A.M12 * B.M21,
                          A.M11 * B.M12 + A.M12 * B.M22,
                          A.M21 * B.M11 + A.M22 * B.M21,
                          A.M21 * B.M12 + A.M22 * B.M22,
                          A.OffsetX * B.M11 + A.OffsetY * B.M21 + B.OffsetX,
                          A.OffsetX * B.M12 + A.OffsetY * B.M22 + B.OffsetY);
    }
}

Este é com certeza o código de Pinch mais simples que eu já escrevi, e esse fato talvez seja o melhor endosso que eu possa dar para essa nova interface de gesto.

Talvez uma teoria unificada de multitoque não esteja tão longe afinal.

Charles Petzold  é editor colaborador da MSDN Magazine há muito tempo. Seu novo livro, “Programming Windows Phone 7” (Microsoft Press, 2010), está disponível gratuitamente para download em bit.ly/cpebookpdf.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Richard Bailey