Одним из самых красивых элементов управления
Windows Phone является Panorama. Он идеально подходит для отображения большого количества контента на небольшом экране и позволяет пользователю легко перемещаться по нему. Визуальная подсказка для «есть больше» обеспечивается путем показа небольшой части следующей панели справа от текущих данных. Типичный пример показан справа.
Это также один из наиболее злоупотребляемых элементов управления (виновен в том, что он взимается за вашу честь), но все же я хотел перенести
Catch’em Birds на
Windows 8 — и обнаружил, что готового к использованию элемента управления не существует. После борьбы со ScrollViewers и GridViewers и так далее, я пришел к этому очень простому поведению, которое в основном принимает FlipView и превращает его в своего рода панораму.
Теперь FlipView спроектирован как полноэкранный элемент управления, поэтому поведение в основном обходит все элементы в FlipView, сжимает их по горизонтали на настраиваемый процент экрана и смещает «следующую» панель немного влево (делая он появляется в правой части экрана на текущей панели). Чтобы это выглядело немного более быстрым и плавным, я сделал анимированное смещение, чтобы «следующий» экран не столько щелкал, сколько
скользил в поле зрения. Общий эффект выглядит довольно мило для меня. Я надеюсь, что Microsoft тоже так подумает, так как скоро мое приложение готовится к работе в App Excellence Lab ?
В моем приложении это выглядит так. У меня до сих пор нет приличного устройства записи экрана для Windows 8, поэтому я вынул видеокамеру.
Поэтому это поведение, которое первоначально называлось FlipViewPanoramaBehavior, конечно, основано на моем более раннем проекте WinRtBehaviors CodePlex. Это начинается следующим образом, со следующими свойствами зависимости:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Win8nl.External; using Win8nl.Utilities; using WinRtBehaviors; using Windows.Foundation; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Media.Animation; namespace Win8nl.Behaviors { /// <summary> /// A behavior to turn a FlipView into a kind of panorama /// </summary> public class FlipViewPanoramaBehavior : Behavior<FlipView> { #region AnimationTime /// <summary> /// AnimationTime Property name /// </summary> public const string AnimationTimePropertyName = "AnimationTime"; public int AnimationTime { get { return (int)GetValue(AnimationTimeProperty); } set { SetValue(AnimationTimeProperty, value); } } /// <summary> /// AnimationTime Property definition /// </summary> public static readonly DependencyProperty AnimationTimeProperty = DependencyProperty.Register( AnimationTimePropertyName, typeof(int), typeof(FlipViewPanoramaBehavior), new PropertyMetadata(250)); #endregion #region NextPanelScreenPercentage /// <summary> /// NextPanelScreenPercentage Property name /// </summary> public const string NextPanelScreenPercentagePropertyName = "NextPanelScreenPercentage"; public double NextPanelScreenPercentage { get { return (double)GetValue(NextPanelScreenPercentageProperty); } set { SetValue(NextPanelScreenPercentageProperty, value); } } /// <summary> /// NextPanelScreenPercentage Property definition /// </summary> public static readonly DependencyProperty NextPanelScreenPercentageProperty = DependencyProperty.Register( NextPanelScreenPercentagePropertyName, typeof(double), typeof(FlipViewPanoramaBehavior), new PropertyMetadata(10.0)); #endregion } }
Таким образом, «AnimationTime» — это количество миллисекунд, необходимое поведению для перехода к следующей панели, а NextPanelScreenPercentage указывает, сколько экранной недвижимости займет следующая панель. Здесь пока ничего особенного.
Если я хочу обойти содержимое FlipView, я сначала должен найти это содержимое. С некоторыми контрольными точками и часами, которые я обнаружил, я мог использовать следующий код, чтобы найти FlipViewItems:
/// <summary> /// Find all Flip view items /// </summary> /// <returns></returns> private List<FlipViewItem> GetFlipViewItems() { var grid = AssociatedObject.GetVisualChildren().FirstOrDefault(); if (grid != null) { return grid.GetVisualDescendents().OfType<FlipViewItem>().ToList(); } return null; }
Внимательные читатели могут заметить, что ни GetVisualChildren, ни GetVisualDescendents не являются частью API WinRT , что совершенно правильно — они взяты из VisualTreeHelperExtensions, которые я портировал с Windows Phone некоторое время назад . Не начинайте скачивать этот материал и соберите его вместе — подождите до конца, и я покажу ленивый способ сделать это.
В любом случае — я хотел свободно перемещать содержимое FlipView . Это означает, что я буду использовать некоторые раскадровки для работы над переводами. Таким образом, мы идентифицируем содержимое каждого FlipViewItem и устанавливаем для его первого визуального дочернего элемента Rendertransform значение CompositeTransform, если его еще нет:
/// <summary> /// At compositions transforms to every item within every flip view item /// </summary> private void AddTranslates() { var items = GetFlipViewItems(); if (items != null && items.Count > 1) { foreach (var item in items) { var firstChild = item.GetVisualChild(0); if (!(firstChild.RenderTransform is CompositeTransform)) { firstChild.RenderTransform = new CompositeTransform(); firstChild.RenderTransformOrigin = new Point(0.5, 0.5); } } } }
Это предполагает, что каждый FlipViewItem содержит только один дочерний элемент. Лучше удостоверьтесь, что это работает, поэтому поместите вокруг него Сетка, если вам нужно больше, чем одна вещь, чтобы сидеть там.
Теперь ядром всего поведения является этот кусок кода:
/// <summary> /// Does the actual repositioning and sizing of the items displayed in the Flipview /// </summary> private void SizePosFlipViewItems() { AddTranslates(); // <-- moved from AssociatedObjectLoaded for RTM var size = AssociatedObject.ActualWidth*(NextPanelScreenPercentage/100); var shift = size - 15; var items = GetFlipViewItems(); if (items != null && items.Count > 1) { // Make all items a bit smaller and make sure they are aligned left foreach (var item in items) { item.GetVisualChild(0).HorizontalAlignment = HorizontalAlignment.Left; item.GetVisualChild(0).Width = items[0].ActualWidth - size; } var selectedIndex = AssociatedObject.SelectedIndex; if (selectedIndex > 0) { StartTranslateStoryBoard(0, 0, items[selectedIndex - 1].GetVisualChild(0), 0); } StartTranslateStoryBoard(0, 0, items[selectedIndex].GetVisualChild(0), AnimationTime); if (selectedIndex + 1 < items.Count) { StartTranslateStoryBoard(-shift, 0, items[selectedIndex + 1].GetVisualChild(0), AnimationTime); } } }
Сначала он вычисляет новый размер FlipViewItems, а затем вычисляет, насколько он может сместить «следующую панель» — в основном, сколько места есть между этой панелью и следующей. В настоящее время это жестко закодированное число, но не стесняйтесь также указывать это свойство ;-).
Затем для каждого FlipViewItem уменьшается размер первого визуального дочернего элемента и выравнивается по левому краю (чтобы освободить место и с правой стороны). Потом:
- Она перемещает панель, которая только что исчезла влево (если есть), обратно в ее нормальное положение, в кратчайшие сроки (то есть не анимированная — она невидима для любого пути слева, так зачем беспокоиться).
- Он перемещает текущую панель в нормальное положение, но оживляет ее. Это потому, что если он перемещается слева, он движется слишком далеко, как вы могли заметить в фильме — поэтому он скользит назад
- Она перемещает следующую панель (если она есть) немного влево — анимированную, поэтому она отображается в правой части экрана.
Теперь, конечно, есть небольшая проблема метода, который делает раскадровки, чтобы это произошло:
private static void StartTranslateStoryBoard(double desiredX, double desiredY, FrameworkElement fe, int time) { var translatePoint = fe.GetTranslatePoint(); var destinationPoint = new Point(desiredX, desiredY); if (destinationPoint.DistanceFrom(translatePoint) > 1) { var storyboard = new Storyboard { FillBehavior = FillBehavior.HoldEnd }; storyboard.AddTranslationAnimation( fe, translatePoint, destinationPoint, new Duration(TimeSpan.FromMilliseconds(time)), new CubicEase { EasingMode = EasingMode.EaseOut }); storyboard.Begin(); } }
Еще раз, я использую некоторые методы расширения из кода, перенесенного из Windows Phone в статье, которую я упоминал ранее , я подчеркнул их, чтобы они отличались от стандартного API. В основном: этот метод принимает FrameworkElement и перемещает его в нужное место в нужное время, используя раскадровку, которая оживляет перевод. То есть, если он уже не находится в этом желаемом положении. Я думаю, что однажды я сделаю это отдельным методом расширения в библиотеке утилит, но на данный момент все идет хорошо.
Все, что осталось сейчас, это немного подключить, я собрал все это в один блок кода:
protected override void OnAttached() { AssociatedObject.Loaded += AssociatedObjectLoaded; base.OnAttached(); } protected override void OnDetaching() { AssociatedObject.Loaded -= AssociatedObjectLoaded; AssociatedObject.SelectionChanged -= AssociatedObjectSelectionChanged; AssociatedObject.SizeChanged -= AssociatedObjectSizeChanged; } private void AssociatedObjectLoaded(object sender, RoutedEventArgs e) { //AddTranslates(); deleted for RTM SizePosFlipViewItems(); AssociatedObject.SelectionChanged += AssociatedObjectSelectionChanged; AssociatedObject.SizeChanged += AssociatedObjectSizeChanged; } private void AssociatedObjectSelectionChanged(object sender, SelectionChangedEventArgs e) { SizePosFlipViewItems(); } private async void AssociatedObjectSizeChanged(object sender, SizeChangedEventArgs e) { await Task.Delay(250); SizePosFlipViewItems(); }
OnAttached и OnDetaching выполняют свою обычную базовую проводку и разводку событий.
Когда AssociatedObject (т. Е. FlipView) сначала загружается, первый дочерний элемент FlipViewItems получает свои CompositeTransforms, затем создается начальная компоновка экрана путем вызова SizePosFlipViewItems. Затем два события связаны:
- SelectionChanged
- SizeChanged
Теперь первая логична — когда пользователь выбирает следующую панель (т. Е. Он прокручивает ее слева или справа), необходимо снова расположить панели так, чтобы вновь выбранная панель оставалась на виду (слишком много прокручивается вправо запомните) и «новая» следующая панель появится в левой части экрана.
Перехват SizeChanged необходим, когда пользователь поворачивает экран или щелкает приложение. Тогда размер экрана изменится, и часть экрана, которую может использовать следующая панель, будет значительно меньше — в пикселях. В моем приложении об этом позаботился диспетчер визуальных состояний, который прослушивает события страницы — в основном что-то украденное из LayoutAwarePage, которое есть в каждом шаблонном проекте —
но это требует времени, Теперь я знаю, что я буду обижен на это (и у меня есть довольно хорошая идея для кого), но чтобы решить это, обработчик SizeChanged немного ждет фактического вызова SizePosFlipViewItems. И чтобы предотвратить блокировку пользовательского интерфейса, я интересно использовал Task.Delay для этого. Это грубо, но это работает. Как вы, наверное, видели в фильме, когда я снимал приложение.
Так что у вас есть это. Код работает, вы видели его в действии. Его использование смехотворно просто: создайте FlipView, добавьте элементы и добавьте это поведение в FlipView. Готово. Вы можете скачать это поведение
здесь, но вам понадобится довольно много базовых библиотек, чтобы заставить его работать — так как он использует большую часть моей
библиотеки win8nl на CodePlex. Если вы хотите идти простым и быстрым путем: просто
используйте пакет Win8nl NuGet, Это даст вам поведение и все необходимые условия, включая MVVMLight.
Имейте в виду, что win8nl теперь использует реактивные расширения. Они включены в пакет NuGet и будут поставляться с ним в качестве зависимости.
ОБНОВЛЕНИЕ: обратите внимание, что с момента первоначальной публикации произошло небольшое изменение кода: из-за оптимизации Microsoft FlipView не все элементы изначально загружаются, поэтому проверьте, не каждый ли дочерний элемент FlipViewItem имеет CompositeTransform должен выполняться при
каждой манипуляции.