Реализация нового Frame в стиле IOS
Или проще говоря — Frame в стиле Modern UI.
Здравствуйте. Меня зовут Андрей и я очень устал пользоваться стандартным VK на Windows 10. Его горизонтальная навигация меня утомила и как то она не вписывается в общий дизайн. Ещё очень давно хотел реализовать такое дело, а именно: плавная навигация как на iPhone. Для чего? Для того, что я хочу сделать свой VK клиент на WPF. Для начала покажу общую картину:
Можно сделать вывод, что такой подход будет очень удобным. DataContext между страницами будет передаваться через конструктор, но дальше будет интереснее.
Начну с namespace UFC.UI. Так как на каждой странице может находиться несколько кнопок, то мне пришлось создать интерфейс:
public delegate void FloppyPageNavigateEventHandler(IFloppyPage page, FloppyPageEventArgs e); public delegate void FloppyPageGoBackEventHandler(FloppyPageEventArgs e); public class FloppyPageEventArgs : EventArgs { public FloppyPageEventArgs() { } } public interface IFloppyPage { event FloppyPageNavigateEventHandler Navigate; event FloppyPageGoBackEventHandler GoBack; IFloppyPages IFloppyPages { get; set; } string Title { get; set; } }
Каждая страница наследует этот интерфейс и получает очень удобное дополнение.
public partial class Page1 : Page, IFloppyPage { public event FloppyPageNavigateEventHandler Navigate; public event FloppyPageGoBackEventHandler GoBack; public IFloppyPages IFloppyPages { get; set; } public Page1() : this(null) { } public Page1(object dataContext) { InitializeComponent(); if (dataContext != null) this.DataContext = dataContext; else this.DataContext = this; Title = "Первая страница"; } private void NavigateTo_MainPage(object sender, RoutedEventArgs e) { if (Navigate != null) Navigate(new MainPage(DataContext), new FloppyPageEventArgs()); } private void NavigateTo_Page2(object sender, RoutedEventArgs e) { if (Navigate != null) Navigate(new Page2(DataContext), new FloppyPageEventArgs()); } private void NavigateTo_Page3(object sender, RoutedEventArgs e) { if (Navigate != null) Navigate(new Page3(DataContext), new FloppyPageEventArgs()); } private void Button_GoBack(object sender, RoutedEventArgs e) { if (GoBack != null) GoBack(new FloppyPageEventArgs()); } }
Теперь плавно можно подойти к интересному. Здесь затронут интерфейс IFloppyPages. Конечно его можно было бы по другому назвать, но я выбрал именно такое название. Его функция ни чем не отличается от DataContext. Такое решение сделано для того, что бы в будущем мы могли использовать DataContext в других целях (mvvm, binding, commands и т.д.)
Собственно, вот его реализация:
public interface IFloppyPages { IFloppyPage FirstPage { get; set; } IFloppyPage CurrentPage { get; set; } int JournalCount { get; set; } void Navigate(IFloppyPage page); bool GoBack(); bool CanGoBack { get; set; } }
Пожалуй теперь можно взглянуть на xaml разметку этого элемента управления:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:UFC.UI.Controls"> <Thickness x:Key="Dynamic.LongPage.MarginAnimation">10, 0, -10, 0</Thickness> <Style TargetType="local:FloppyPages"> <Style.Setters> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="local:FloppyPages"> <Grid Name="mainGrid"> <Grid Name="grid1"> <Frame Name="frame1" NavigationUIVisibility="Hidden"/> </Grid> <Grid Name="grid2"> <Frame Name="frame2" NavigationUIVisibility="Hidden"/> </Grid> <Grid.Resources> <BeginStoryboard x:Key="grid1Animation"> <Storyboard> <ThicknessAnimation Duration="0:0:0.8" Storyboard.TargetName="grid1" Storyboard.TargetProperty="Margin" From="{DynamicResource Dynamic.LongPage.MarginAnimation}" To="0"> <ThicknessAnimation.EasingFunction> <ElasticEase EasingMode="EaseOut" Oscillations="1"/> </ThicknessAnimation.EasingFunction> </ThicknessAnimation> <ThicknessAnimation Duration="0:0:0.8" Storyboard.TargetName="grid2" Storyboard.TargetProperty="Margin" From="0" To="-100, 20, 100, 20"> <ThicknessAnimation.EasingFunction> <ElasticEase EasingMode="EaseOut" Oscillations="1"/> </ThicknessAnimation.EasingFunction> </ThicknessAnimation> </Storyboard> </BeginStoryboard> <BeginStoryboard x:Key="grid2Animation"> <Storyboard> <ThicknessAnimation Duration="0:0:0.8" Storyboard.TargetName="grid2" Storyboard.TargetProperty="Margin" From="{DynamicResource Dynamic.LongPage.MarginAnimation}" To="0"> <ThicknessAnimation.EasingFunction> <ElasticEase EasingMode="EaseOut" Oscillations="1"/> </ThicknessAnimation.EasingFunction> </ThicknessAnimation> <ThicknessAnimation Duration="0:0:0.8" Storyboard.TargetName="grid1" Storyboard.TargetProperty="Margin" From="0" To="-100, 20, 100, 20"> <ThicknessAnimation.EasingFunction> <ElasticEase EasingMode="EaseOut" Oscillations="1"/> </ThicknessAnimation.EasingFunction> </ThicknessAnimation> </Storyboard> </BeginStoryboard> <BeginStoryboard x:Key="grid3Animation"> <Storyboard> <ThicknessAnimation Duration="0:0:0.8" Storyboard.TargetName="grid1" Storyboard.TargetProperty="Margin" From="0" To="{DynamicResource Dynamic.LongPage.MarginAnimation}"> <ThicknessAnimation.EasingFunction> <ElasticEase EasingMode="EaseOut" Oscillations="1"/> </ThicknessAnimation.EasingFunction> </ThicknessAnimation> <ThicknessAnimation Duration="0:0:0.8" Storyboard.TargetName="grid2" Storyboard.TargetProperty="Margin" From="-100, 20, 100, 20" To="0"> <ThicknessAnimation.EasingFunction> <ElasticEase EasingMode="EaseOut" Oscillations="1"/> </ThicknessAnimation.EasingFunction> </ThicknessAnimation> </Storyboard> </BeginStoryboard> <BeginStoryboard x:Key="grid4Animation"> <Storyboard> <ThicknessAnimation Duration="0:0:0.8" Storyboard.TargetName="grid2" Storyboard.TargetProperty="Margin" From="0" To="{DynamicResource Dynamic.LongPage.MarginAnimation}"> <ThicknessAnimation.EasingFunction> <ElasticEase EasingMode="EaseOut" Oscillations="1"/> </ThicknessAnimation.EasingFunction> </ThicknessAnimation> <ThicknessAnimation Duration="0:0:0.8" Storyboard.TargetName="grid1" Storyboard.TargetProperty="Margin" From="-100, 20, 100, 20" To="0"> <ThicknessAnimation.EasingFunction> <ElasticEase EasingMode="EaseOut" Oscillations="1"/> </ThicknessAnimation.EasingFunction> </ThicknessAnimation> </Storyboard> </BeginStoryboard> </Grid.Resources> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style.Setters> </Style> </ResourceDictionary>
Очень надеюсь, что вам удастся понять мой алгоритм. Всё самое непонятное постараюсь объяснить после кода внизу страницы.
Теперь приведу весь код этого элемента управления:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Media.Animation; namespace UFC.UI { /// <summary> /// Страница по умолчанию. /// </summary> internal class DefaultPage : IFloppyPage { public event FloppyPageNavigateEventHandler Navigate; public event FloppyPageGoBackEventHandler GoBack; public IFloppyPages IFloppyPages { get; set; } public string Title { get; set; } public DefaultPage() { Title = "Страница по умолчанию"; } } public delegate void FloppyPageNavigateEventHandler(IFloppyPage page, FloppyPageEventArgs e); public delegate void FloppyPageGoBackEventHandler(FloppyPageEventArgs e); public class FloppyPageEventArgs : EventArgs { public FloppyPageEventArgs() { } } public interface IFloppyPage { event FloppyPageNavigateEventHandler Navigate; event FloppyPageGoBackEventHandler GoBack; IFloppyPages IFloppyPages { get; set; } string Title { get; set; } } public interface IFloppyPages { IFloppyPage FirstPage { get; set; } IFloppyPage CurrentPage { get; set; } int JournalCount { get; set; } void Navigate(IFloppyPage page); bool GoBack(); bool CanGoBack { get; set; } } } namespace UFC.UI.Controls { public class FloppyPages : Control, IFloppyPages, INotifyPropertyChanged { #region Private Members private bool GridNumber = false; private bool IsDoneAnimation = true; private List<IFloppyPage> journal = new List<IFloppyPage>(); private Frame frame1 = new Frame(); private Frame frame2 = new Frame(); private Grid mainGrid = new Grid(); private Grid grid1 = new Grid(); private Grid grid2 = new Grid(); private BeginStoryboard animation1 = new BeginStoryboard() { Storyboard = new Storyboard() }; private BeginStoryboard animation2 = new BeginStoryboard() { Storyboard = new Storyboard() }; private BeginStoryboard animation3 = new BeginStoryboard() { Storyboard = new Storyboard() }; private BeginStoryboard animation4 = new BeginStoryboard() { Storyboard = new Storyboard() }; #endregion #region Constructors static FloppyPages() { DefaultStyleKeyProperty.OverrideMetadata(typeof(FloppyPages), new FrameworkPropertyMetadata(typeof(FloppyPages))); FloppyPages.NavigatedRoutedEvent = EventManager.RegisterRoutedEvent("Navigated", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FloppyPages)); FloppyPages.WentBackRoutedEvent = EventManager.RegisterRoutedEvent("WentBack", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(FloppyPages)); } public FloppyPages() { FirstPage = new DefaultPage(); } #endregion #region Public Dependency Properties public static readonly DependencyProperty FirstPageProperty = DependencyProperty.RegisterAttached("FirstPage", typeof(IFloppyPage), typeof(FloppyPages)); #endregion #region Public Properties public IFloppyPage FirstPage { get { return (IFloppyPage)GetValue(FirstPageProperty); } set { SetValue(FirstPageProperty, value); OnFirstPage(FirstPage); OnPropertyChanged("FirstPage"); } } #endregion #region Public RoutedEvents public static readonly RoutedEvent NavigatedRoutedEvent; public static readonly RoutedEvent WentBackRoutedEvent; #endregion #region Public Events public event RoutedEventHandler Navigated { add { base.AddHandler(FloppyPages.NavigatedRoutedEvent, value); } remove { base.RemoveHandler(FloppyPages.NavigatedRoutedEvent, value); } } public event RoutedEventHandler WentBack { add { base.AddHandler(FloppyPages.WentBackRoutedEvent, value); } remove { base.RemoveHandler(FloppyPages.WentBackRoutedEvent, value); } } #endregion #region Public Members public IFloppyPage CurrentPage { get { if (journal.Count > 0) return journal[journal.Count - 1]; else return null; } set { } } public int JournalCount { get { return journal.Count; } set { } } public void Navigate(IFloppyPage page) { Start_Navigate(page); } public bool GoBack() { return Start_GoBack(); } public bool CanGoBack { get { if (journal.Count > 1) return true; else return false; } set { } } #endregion #region Private OnFirstPage private void OnFirstPage(IFloppyPage page) { if (page != null && frame1 != null && frame2 != null) { if (GridNumber) frame1.Navigate(page); else frame2.Navigate(page); page.Navigate += Page_Navigate; page.GoBack += Page_GoBack; journal.Clear(); journal.Add(page); OnPropertyChanged("JournalCount"); OnPropertyChanged("CanGoBack"); OnPropertyChanged("CurrentPage"); } } #endregion #region Public OnApplyTemplate public override void OnApplyTemplate() { base.OnApplyTemplate(); mainGrid = GetTemplateChild("mainGrid") as Grid; grid1 = GetTemplateChild("grid1") as Grid; if (grid1 != null) grid1.Margin = new Thickness(0); grid2 = GetTemplateChild("grid2") as Grid; if (grid2 != null) grid2.Margin = new Thickness(this.ActualWidth, 0, (-1 * this.ActualWidth), 0); frame1 = GetTemplateChild("frame1") as Frame; frame2 = GetTemplateChild("frame2") as Frame; animation1 = mainGrid.Resources["grid1Animation"] as BeginStoryboard; animation2 = mainGrid.Resources["grid2Animation"] as BeginStoryboard; animation3 = mainGrid.Resources["grid3Animation"] as BeginStoryboard; animation4 = mainGrid.Resources["grid4Animation"] as BeginStoryboard; if (animation1 != null) if (animation1.Storyboard != null) animation1.Storyboard.Completed += NewGridMargin_Completed; if (animation2 != null) if (animation2.Storyboard != null) animation2.Storyboard.Completed += NewGridMargin_Completed; if (animation3 != null) if (animation3.Storyboard != null) animation3.Storyboard.Completed += OldGridMargin_Completed; if (animation4 != null) if (animation4.Storyboard != null) animation4.Storyboard.Completed += OldGridMargin_Completed; if (mainGrid != null) { mainGrid.SizeChanged += (sender, e) => { Application.Current.Resources["Dynamic.LongPage.MarginAnimation"] = new Thickness(this.ActualWidth, 0, -1 * this.ActualWidth, 0); }; } OnFirstPage(FirstPage); } #endregion #region Private Events private void Page_Navigate(IFloppyPage page, FloppyPageEventArgs e) { Start_Navigate(page); } private void Page_GoBack(FloppyPageEventArgs e) { Start_GoBack(); } private void NewGridMargin_Completed(object sender, EventArgs e) { Set_NewMargin(); } private void OldGridMargin_Completed(object sender, EventArgs e) { Set_OldMargin(); } #endregion #region Private Navigate private void Start_Navigate(IFloppyPage page) { if (page != null && IsDoneAnimation) { IsDoneAnimation = false; GridNumber = !GridNumber; page.Navigate += Page_Navigate; page.GoBack += Page_GoBack; if (!GridNumber) { animation1.Storyboard.Stop(); frame2.Navigate(page); Panel.SetZIndex(grid1, 0); Panel.SetZIndex(grid2, 1); grid2.Visibility = Visibility.Visible; animation2.Storyboard.Begin(); } else { animation2.Storyboard.Stop(); frame1.Navigate(page); Panel.SetZIndex(grid2, 0); Panel.SetZIndex(grid1, 1); grid1.Visibility = Visibility.Visible; animation1.Storyboard.Begin(); } journal.Add(page); OnPropertyChanged("JournalCount"); OnPropertyChanged("CurrentPage"); OnPropertyChanged("CanGoBack"); base.RaiseEvent(new RoutedEventArgs(FloppyPages.NavigatedRoutedEvent, this)); } } private void Set_NewMargin() { if (!GridNumber) { grid2.Margin = new Thickness(0); grid1.Margin = new Thickness(this.ActualWidth, 0, (-1 * this.ActualWidth), 0); grid1.Visibility = Visibility.Hidden; } else { grid1.Margin = new Thickness(0); grid2.Margin = new Thickness(this.ActualWidth, 0, (-1 * this.ActualWidth), 0); grid2.Visibility = Visibility.Hidden; } IsDoneAnimation = true; } #endregion #region Private GoBack private bool Start_GoBack() { if (journal.Count > 1 && IsDoneAnimation) { IsDoneAnimation = false; GridNumber = !GridNumber; grid1.Visibility = Visibility.Visible; grid2.Visibility = Visibility.Visible; if (!GridNumber) { animation4.Storyboard.Stop(); grid2.Margin = new Thickness(0); frame2.Navigate(journal[journal.Count - 2]); animation3.Storyboard.Begin(); } else { animation3.Storyboard.Stop(); grid1.Margin = new Thickness(0); frame1.Navigate(journal[journal.Count - 2]); animation4.Storyboard.Begin(); } journal.Remove(journal[journal.Count - 1]); OnPropertyChanged("JournalCount"); OnPropertyChanged("CurrentPage"); OnPropertyChanged("CanGoBack"); base.RaiseEvent(new RoutedEventArgs(FloppyPages.WentBackRoutedEvent, this)); return true; } else return false; } private void Set_OldMargin() { if (!GridNumber) { Panel.SetZIndex(grid1, 0); Panel.SetZIndex(grid2, 1); grid1.Margin = new Thickness(this.ActualWidth, 0, (-1 * this.ActualWidth), 0); grid1.Visibility = Visibility.Hidden; } else { Panel.SetZIndex(grid1, 1); Panel.SetZIndex(grid2, 0); grid2.Margin = new Thickness(this.ActualWidth, 0, (-1 * this.ActualWidth), 0); grid2.Visibility = Visibility.Hidden; } IsDoneAnimation = true; } #endregion #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged(string propertyName) { PropertyChangedEventHandler handler = PropertyChanged; if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); } #endregion } }
Начну с того, что ещё ранее вы могли заметить в xaml разметке этого элемента ресурс:
«Dynamic.LongPage.MarginAnimation».
И очень странно, почему там стояли такие размеры: 10,0,-10,0;
На самом деле это не так важно, потому что при сборке мы автоматически подписываемся на событие SizeChanged элемента mainGrid в методе OnApplyTemplate().
if (mainGrid != null) { mainGrid.SizeChanged += (sender, e) => { Application.Current.Resources["Dynamic.LongPage.MarginAnimation"] = new Thickness(this.ActualWidth, 0, -1 * this.ActualWidth, 0); }; }
Благодаря такой реализации мы получаем элемент управления, где анимация двигается на то расстояние, на которое мы укажем, то есть просто изменив размер окна.
Напомню кстати говоря, что в методе OnApplyTemplate() мы получаем ссылки на все мелкие элементы из разметки методом GetTemplateChild(«mainGrid»);
Алгоритм получился таким: вы как бы видите одну страницу, потом при переходе на следующую страницу с правого края вылезает вторая страница. Первая страница уходит на задний план, затем после окончания анимации первая страница уходит в правый край где была вторая страница.
Таким образом мы получаем две чередующие панели, на которых лежат frame1 и frame2. Благодаря переменной GridNumber мы проверяем, на какой grid мы попали и на каком frame поменять страницу.
Так же здесь реализован журнал, но в нём ничего интересного нет. Обычный список, который удаляет IFloppyPage только после перехода «Назад» (GoBack).
Да и ещё. Как только приложение начинает свою жизнь, ему присваивается первая страница, это может быть либо DefaultPage по умолчанию, либо та страница, которую укажете вы. Затем FloppyPages автоматически привяжет ваш IFloppyPage к событию Navigate и GoBack. Так он будет следить, когда на одной из ваших IFloppyPage вы решитесь перейти на другую страницу.
Теперь покажу окно, где и создаётся FloppyPages, и присваивается первая страница.
using System.Windows; using UFC.Pages; namespace UFC { public partial class Browser : Window { public Browser() { InitializeComponent(); floppyPages.FirstPage = new MainPage() { IFloppyPages = floppyPages }; } private void Button_GoBack(object sender, RoutedEventArgs e) { floppyPages.GoBack(); } } }
<Window x:Class="UFC.Browser" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ufc="clr-namespace:UFC.UI.Controls;assembly=UFC.UI" Title="UFC" Height="640" Width="380" > <Grid Background="LightGray"> <Grid.RowDefinitions> <RowDefinition Height="40"/> <RowDefinition/> </Grid.RowDefinitions> <ufc:FloppyPages Grid.Row="1" Name="floppyPages" /> <Grid Grid.Row="0" Background="LightGray"> <Grid.ColumnDefinitions> <ColumnDefinition Width="40"/> <ColumnDefinition Width="1*"/> <ColumnDefinition Width="40"/> </Grid.ColumnDefinitions> <TextBox Grid.Column="1" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" IsReadOnly="True" Background="Transparent" FontSize="20" Text="{Binding ElementName=floppyPages, Path=CurrentPage.Title, UpdateSourceTrigger=PropertyChanged}"/> <Button Name="MenuButton" Grid.Column="0" Visibility="Visible"> <Path Margin="5" Stretch="UniformToFill" Fill="Black" Data="F1 M 19,23L 27,23L 27,31L 19,31L 19,23 Z M 19,34L 27,34L 27,42L 19,42L 19,34 Z M 31,23L 57,23L 57,31L 31,31L 31,23 Z M 19,45L 27,45L 27,53L 19,53L 19,45 Z M 31,34L 57,34L 57,42L 31,42L 31,34 Z M 31,45L 57,45L 57,53L 31,53L 31,45 Z "/> </Button> <Button Name="BackButton" Grid.Column="0" Visibility="Hidden" Click="Button_GoBack"> <Path Margin="5,9" Stretch="UniformToFill" Fill="Black" Data="F1 M 18.0147,41.5355C 16.0621,39.5829 16.0621,36.4171 18.0147,34.4645L 26.9646,25.5149C 28.0683,24.4113 29,24 31,24L 52,24C 54.7614,24 57,26.2386 57,29L 57,47C 57,49.7614 54.7614,52 52,52L 31,52C 29,52 28.0683,51.589 26.9646,50.4854L 18.0147,41.5355 Z M 47.5281,42.9497L 42.5784,37.9999L 47.5281,33.0502L 44.9497,30.4717L 40,35.4215L 35.0502,30.4717L 32.4718,33.0502L 37.4215,37.9999L 32.4718,42.9497L 35.0502,45.5281L 40,40.5783L 44.9497,45.5281L 47.5281,42.9497 Z "/> </Button> <Button Grid.Column="2"> <Path Margin="5,9" Stretch="UniformToFill" Fill="Black" Data="F1 M 57.9853,41.5355L 49.0354,50.4854C 47.9317,51.589 47,52 45,52L 24,52C 21.2386,52 19,49.7614 19,47L 19,29C 19,26.2386 21.2386,24 24,24L 45,24C 47,24 47.9317,24.4113 49.0354,25.5149L 57.9853,34.4645C 59.9379,36.4171 59.9379,39.5829 57.9853,41.5355 Z M 28.4719,42.9497L 31.0503,45.5281L 36,40.5784L 40.9498,45.5281L 43.5282,42.9497L 38.5785,37.9999L 43.5282,33.0502L 40.9498,30.4718L 36,35.4215L 31.0503,30.4718L 28.4719,33.0502L 33.4216,37.9999L 28.4719,42.9497 Z "/> </Button> </Grid> </Grid> </Window>
Такое проектное решение без всякого труда позволит вам добавить и ViewModel, и Model и возможность использовать один и тот же DataContext на разных страницах.
Спасибо за внимание.
ссылка на оригинал статьи https://habrahabr.ru/post/317896/
Добавить комментарий