Статьи

DragFlickBehavior — от Windows Phone 7 до Windows 8 в стиле Metro

Предисловие

Немногим более года назад я создал DragFlickBehavior , поведение для Windows Phone, которое делает практически что-либо перетаскиваемым и «мерцающим», то есть вы можете перетаскивать элемент графического интерфейса вместе с пальцем, и у вас появляется небольшая инерция, когда вы позволяете ему идти. В моем предыдущем посте я рассказал об основах поведения в стиле Windows 8 Metro. Тестирование этого было сделано с использованием портированной версии DragFlickBehavior. Я пересмотрел свои шаги к тому, как я заставил это работать, и опишу процесс переноса существующего поведения здесь.

Чтобы DragFlickBehavior работал, сначала нужно было заложить некоторые основы. Для Windows Phone я сначала сделал несколько методов расширения для FrameworkElement и StoryBoard. Что еще хуже, один из этих методов расширения в FrameworkElementExtensions использовал еще один метод расширения — GetVisualParent в VisualTreeHelperExtensions из Phone7.Fx… Nil desperandum… Я начну с самого начала

Портирование VisualTreeHelperExtensions

Я создал библиотеку классов Win8nl.External, скопировал VisualTreeHelperExtensions.cs из его местоположения кодового комплекса и открыл ее редактор. И тогда процесс был довольно прост:

  • Пространство имен System.Windows.Media исчезло. Поэтому я удалил его используя.
  • Я щелкнул по каждой красной линии, нажал Control- (это Control-точка), и в большинстве случаев редактор предлагает пространство имен для добавления

В конце концов я, кажется, добавил

using Windows.UI.Xaml;
using Windows.UI.Xaml.Media;

И затем возникла небольшая проблема двух мест, где автор вызывает VisualStateManager.GetVisualStateGroups и ожидает, что результатом будет IList. Теперь это IEnumerable. Во всяком случае, я решил это, изменив

IList groups = VisualStateManager.GetVisualStateGroups(root);

в

var groups = VisualStateManager.GetVisualStateGroups(root);

в обоих случаях. Один файл готов. Я даже не буду притворяться, что понимаю, что на самом деле делают все эти методы в этом файле. Я просто портировал их.

Porting FrameworkElementExtensions

Затем я создал библиотеку Win8nl, добавил ссылки на Win8nl.External и WinRtBehaviors и начал использовать FrameworkElementExtensions. Это оказалось довольно тривиальным вопросом. Мне нужно было удалить

using System.Windows.Controls;
using System.Windows.Media;
using System.Linq;
using Phone7.Fx;

И добавить после контрольной точки через ошибки, которые я обнаружил, я добавил

using Windows.UI.Xaml.Media;
using Windows.UI.Xaml;
using Windows.Foundation
using Windows.UI.Xaml.Controls

Два файла сделано!

Портирование StoryboardExtensions

Рутина начинает оседать. Удалить

using System.Windows.Media;
using System.Windows.Media.Animation;

Точка управления вокруг, и вы увидите, что вы добавили

using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Animation;

Но затем мы попали в нашу первую загвоздку. Два метода используют параметр типа IEasingFunction, который больше не существует. Но это можно исправить, изменив его на EasingFunctionBase .

Затем я обнаружил, что Storyboard.SetTargetProperty, по-видимому, больше не хочет иметь PropertyPath — который может быть сделан из объекта DependencyProperty — но является строкой . Итак, метод

public static void AddAnimation(this Storyboard storyboard,
	DependencyObject item, Timeline t, <span color="#ff0000" style="">DependencyProperty p</span>)
{
  if (p == null) throw new ArgumentNullException("p");
  Storyboard.SetTarget(t, item);
  Storyboard.SetTargetProperty(t, new PropertyPath(p));
  storyboard.Children.Add(t);
}

Нужно изменить на

 public static void AddAnimation(this Storyboard storyboard,
	DependencyObject item, Timeline t, <span color="#ff0000" style="">string property</span>)
{
  if (string.IsNullOrWhiteSpace(property)) throw new ArgumentNullException("property");
  Storyboard.SetTarget(t, item);
  Storyboard.SetTargetProperty(t, property);
  storyboard.Children.Add(t);
}

Это плохие новости, так как это нарушает публичный интерфейс. И это ломает еще больше, а именно
еще один публичный метод расширения

public static void AddTranslationAnimation(this Storyboard storyboard,
   FrameworkElement fe, Point from, Point to, Duration duration,
   EasingFunctionBase easingFunction)
{
  storyboard.AddAnimation(
      fe.RenderTransform,
      storyboard.CreateDoubleAnimation(duration, from.X, to.X, easingFunction),
                                       <span color="#ff0000" style="">CompositeTransform.TranslateXProperty</span>);
  storyboard.AddAnimation(fe.RenderTransform,
       storyboard.CreateDoubleAnimation(duration, from.Y, to.Y, easingFunction),
                                        <span color="#ff0000" style="">CompositeTransform.TranslateYProperty</span>);
}

Необходимо изменить на

public static void AddTranslationAnimation(this Storyboard storyboard,
  FrameworkElement fe, Point from, Point to, Duration duration,
  EasingFunctionBase easingFunction)
{
  storyboard.AddAnimation(fe.RenderTransform,
  storyboard.CreateDoubleAnimation(duration, from.X, to.X, easingFunction),
                                   <span color="#ff0000" style="">"TranslateX"</span>);
  storyboard.AddAnimation(fe.RenderTransform,
  storyboard.CreateDoubleAnimation(duration, from.Y, to.Y, easingFunction),
                                   <span color="#ff0000" style="">"TranslateY"</span>);
}

Я должен честно сказать, что я нахожу очевидную необходимость точного указания целевых свойств раскадровки, как в строках, довольно своеобразно, но, очевидно, именно так это и нужно сделать. Я здесь только посланник.

Портирование DragFlickBehavior

Это снова мы. Удалить

using System.Windows.Interactivity;
using System.Windows.Media;
using System.Windows.Media.Animation;
using Wp7nl.Utilities;

А Control-dotting узнает, что вам необходимо добавить следующее:

using Win8nl.Utilities;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Animation;
using WinRtBehaviors;

Вскоре после этого, вы узнаете , что второй параметр события ManipulationDelta больше не из ManipulationDeltaEventArgs типа , но из ManipulationDelta маршрутизируемого EventArgs, и что он не делает долго свойство «DeltaManipulation» , но свойство простого «Дельта». Таким образом, метод AssociatedObjectManipulationDelta, фиксирующий событие, был следующим:

void AssociatedObjectManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
  var dx = e.DeltaManipulation.Translation.X;
  var dy = e.DeltaManipulation.Translation.Y;
  var currentPosition = elementToAnimate.GetTranslatePoint();
  elementToAnimate.SetTranslatePoint(currentPosition.X + dx, currentPosition.Y + dy);
}

и теперь должно быть это

void AssociatedObjectManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
{
  var dx = e.Delta.Translation.X;
  var dy = e.Delta.Translation.Y;
  var currentPosition = elementToAnimate.GetTranslatePoint();
  elementToAnimate.SetTranslatePoint(currentPosition.X + dx, currentPosition.Y + dy);
}

Там нет ракетостроения, верно? И практически идентичный набор переделок должен быть выполнен для метода захвата ManipulationCompleted. Его второй параметр имел тип ManipulationCompletedEventArgs и теперь — как вы уже, наверное, догадались — ManipulationCompleted Routed EventArgs. И это не делает давно имеет свойство e.FinalVelocities.LinearVelocity.X и Y , но делает имеет Velocities.Linear.X и Y.

По какой-то причине эти свойства возвращают значения, которые находятся где-то между 0 и 1, или, по крайней мере, так кажется. Поэтому я сделал конвертирование по эмпирическому правилу, умножив их на 1000. Подводя итог: AssociatedObjectManipulationCompleted раньше был

private void AssociatedObjectManipulationCompleted(object sender,
                                                    ManipulationCompletedEventArgs e)
{
  // Create a storyboard that will emulate a 'flick'
  var currentPosition = elementToAnimate.GetTranslatePoint();
  var velocity = e.FinalVelocities.LinearVelocity;
  var storyboard = new Storyboard { FillBehavior = FillBehavior.HoldEnd };

  var to = new Point(currentPosition.X + (velocity.X / BrakeSpeed),
                     currentPosition.Y + (velocity.Y / BrakeSpeed));
  storyboard.AddTranslationAnimation(elementToAnimate, currentPosition, to, 
    new Duration(TimeSpan.FromMilliseconds(500)), 
    new CubicEase {EasingMode = EasingMode.EaseOut});
  storyboard.Begin();
}

и сейчас

private void AssociatedObjectManipulationCompleted(object sender,
                                                   ManipulationCompletedRoutedEventArgs e)
{
  // Create a storyboard that will emulate a 'flick'
  var currentPosition = elementToAnimate.GetTranslatePoint();
  var xVelocity = e.Velocities.Linear.X * 1000;
  var yVelocity = e.Velocities.Linear.Y * 1000;
  var storyboard = new Storyboard { FillBehavior = FillBehavior.HoldEnd };
  var to = new Point(currentPosition.X + (xVelocity / BrakeSpeed),
                     currentPosition.Y + (yVelocity / BrakeSpeed));
  storyboard.AddTranslationAnimation(elementToAnimate, currentPosition, to,
      new Duration(TimeSpan.FromMilliseconds(500)),
	  new CubicEase { EasingMode = EasingMode.EaseOut });
  storyboard.Begin();
}

Последнее, что вам необходимо учитывать при портировании поведения, — это тот факт, что вы использовали событие OnAttached и Loaded. У вас все еще есть, но по самой своей природе я реализовал поведение, все, что происходило в OnAttached и OnLoaded, должно быть в OnAttached. То же самое касается Unloaded и OnDetaching — последний запускается первым. Следуйте шаблону, который я изложил и инициализировал только в OnAttached, и выполняйте очистку только в OnDetached .

Таким образом, поведение используется для настройки, подобной этой:

protected override void OnAttached()
{
  base.OnAttached();
  AssociatedObject.Loaded += AssociatedObjectLoaded;
  AssociatedObject.ManipulationDelta += AssociatedObjectManipulationDelta;
  AssociatedObject.ManipulationCompleted += AssociatedObjectManipulationCompleted;
}

void AssociatedObjectLoaded(object sender, RoutedEventArgs e)
{
  elementToAnimate = AssociatedObject.GetElementToAnimate();
  if (!(elementToAnimate.RenderTransform is CompositeTransform))
  {
    elementToAnimate.RenderTransform = new CompositeTransform();
    elementToAnimate.RenderTransformOrigin = new Point(0.5, 0.5);
  }
}

И это должно быть теперь 

protected override void OnAttached()
{
  elementToAnimate = AssociatedObject.GetElementToAnimate();
  if (!(elementToAnimate.RenderTransform is CompositeTransform))
  {
    elementToAnimate.RenderTransform = new CompositeTransform();
    elementToAnimate.RenderTransformOrigin = new Point(0.5, 0.5);
  }
  AssociatedObject.ManipulationDelta += AssociatedObjectManipulationDelta;
  AssociatedObject.ManipulationCompleted += AssociatedObjectManipulationCompleted;
  AssociatedObject.ManipulationMode = 
    ManipulationModes.TranslateX | ManipulationModes.TranslateY;
  base.OnAttached();
}

Обратите внимание на пару интересных вещей:

  • Захват «Загружен» ушел. Нам это больше не нужно
  • Есть дополнительная последняя строка, устанавливающая «ManipulationMode». Очевидно, вам нужно настроить это так, чтобы ManipulationDelta и ManipulationCompleted происходили вообще. Случайно наткнулся на это

Наконец, последняя часть: OnDetaching. Это было

protected override void OnDetaching()
{
  AssociatedObject.Loaded -= AssociatedObjectLoaded;
  AssociatedObject.ManipulationCompleted -= AssociatedObjectManipulationCompleted;
  AssociatedObject.ManipulationDelta -= AssociatedObjectManipulationDelta;

  base.OnDetaching();
}

И единственное, что нужно изменить, чтобы использовать
это удаление первой строки: AssociatedObject.Loaded — = AssociatedObjectLoaded ;;

И тогда мы закончили. Если вы добавите это поведение к любому объекту на экране, как я показал в предыдущем посте:

<Page
  x:Class="Catchit8.BlankPage"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="using:Catchit8"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:Win8nl_Behaviors="using:Win8nl.Behaviors"
  xmlns:WinRtBehaviors="using:WinRtBehaviors"
  mc:Ignorable="d">

  <Grid Background="{StaticResource ApplicationPageBackgroundBrush}">
    <TextBlock HorizontalAlignment="Left" Margin="503,213,0,0" TextWrapping="Wrap" 
		 VerticalAlignment="Top" FontSize="18" Text="Drag me">
      <WinRtBehaviors:Interaction.Behaviors>
         <Win8nl_Behaviors:DragFlickBehavior BrakeSpeed ="5"/>
      </WinRtBehaviors:Interaction.Behaviors>
    </TextBlock>
    <Button Content="Drag me too!" HorizontalAlignment="Left" Margin="315,269,0,0" 
		 VerticalAlignment="Top" >
      <WinRtBehaviors:Interaction.Behaviors>
          <Win8nl_Behaviors:DragFlickBehavior BrakeSpeed ="5"/>
      </WinRtBehaviors:Interaction.Behaviors>
     </Button>

  </Grid>
</Page>

Вывод

На первый взгляд, разработка Windows 8, похоже, не сильно отличается от разработки Windows Phone. После того, как я сделал свою структуру поведения, портировать довольно сложное поведение, как это было довольно легко, так что я бы сказал, что это верно и на второй взгляд. Конечно, некоторые вещи отличаются — в основном пространства имен и некоторые имена свойств. XAML также немного отличается. Относительно того, почему Microsoft решила изменить пространства имен, переименовать свойства или методы или даже позволить возвращаемым значениям быть немного другими — я не знаю. Что делать знаю, что скулить об этом, вероятно , привести к повышению кровяного давления , но это не поможет вам очень много , как разработчик. Только подумайте: они продали 450 миллионов копий Windows 7. Не думаю, что всебыть Windows 8 в следующем году, но я думаю, что отметка в 100 миллионов будет достигнута довольно скоро. Выбор за вами — либо вы тратите время и силы на то, чтобы разозлиться на то, что Microsoft переместила ваш сыр (или на самом деле, только некоторые из них), либо вы можете найти новый и, возможно, кровавый кусок сыра.

Как обычно, полное демонстрационное решение для тех, кто, как и я, слишком ленив, чтобы самостоятельно печатать, можно найти здесь . Таким образом, вы можете начать еще быстрее.