Привет, Хабр! Мы продолжаем нашу экспериментальную серию статей для программистов-самоучек, в которой Алексей Плотников, один из участников нашего сообщества Microsoft Developer, рассказывает о создании игры на UWP. Сегодня поговорим о расширенном экране-заставке. Не забывайте оставлять комментарии, вы можете повлиять на ход разработки.



Первая часть

Передаю слово автору.

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

Начнем мы с малого – что такое экран-заставка и зачем его нужно расширять? Вы безусловно сталкивались с заставками приложений задолго до появлений UWP. Далеко ходить не станем. При запуске Visual Studio вы ведите прямоугольное изображение с соответствующей надписью.



Каждой программе нужно немного времени на инициализацию и загрузку интерфейса и, чтобы в это время пользователь понимал, что что-то происходит показывается изображение-заставка. В программах где процесс запуска по сложнее и времени нужно больше, используется расширенная заставка. На таких мы часто видим текст или строку состояния, показывающие статус загрузки приложения.

Теперь давайте разберемся как это работает в UWP. Главное отличие заключается в особенности платформы. Так как она универсальная, то понятие «окно» тут не является приоритетным, а на передний план выходит понятие «экран». Контейнер, в котором отображается экран зависит от устройства и окном он является лишь на ПК.

Первое, что видит пользователь при запуске вашего приложения это экран-заставку, состоящий из изображения в формате PNG и фоновой заливки. Если вы только создали ваше приложение и ничего не меняли, то экран-заставка будет окрашен в основной цвет системы, а в центре будет базовая картинка «заглушка», которая автоматически создается вместе с приложением. Стандартно данная картинка расположена в папке «Assets» и называется «SplashScreen.png». Также в названии файла может присутствовать что-то вроде «scale-200». Это часть системы масштабирования, о которой я буду говорить отдельно в одной из статей цикла, а сейчас затрону лишь в общих чертах.



Так как ваше приложение может запускаться на самых разных устройствах, у вас есть возможность создать несколько вариантов изображений с разными размерами, чтобы на разных разрешениях экрана оно не теряло в качестве от растягиваний или сжиманий. Стандартное имя любого изображения в проекте состоит из двух частей – имя и расширение. Такое изображение будет восприниматься как изображение масштабом 100%. Если же между именем и расширением вы добавите «.scale-xxx», где xxx это коэффициент масштабирования, то оно будет считаться по умолчанию для экранов с таким масштабом, а на остальных растягиваться или сжиматься (если для них нет версий с их масштабом). Например, для масштаба в 200%, имя изображения экрана-заставки будет «SplashScreen.scale-200.png». Далее по тексту я буду рассматривать только изображения в масштабе 100%, чтобы не дублировать тему, запланированную для будущих статей.

Теперь непосредственно о размере изображения. Все базовые визуальные ресурсы приложения имеют фиксированные размеры для каждого из масштабов. Изображение на экране-заставке в стандартном масштабе должно иметь размер 620х300 пикселей. Если вы попытаетесь использовать другой размер, то получите предупреждение об ошибке, однако глядя на скриншот выше, можно увидеть квадратное изображение явно другого размера. На самом деле оно также имеет размер 620х300 пикселей, только большая его часть является прозрачной, что делать не только не воспрещается, но во многих сценариях даже рекомендуется.

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



Установка цвета для фона, как и выбор собственного изображения, осуществляется в окне редактора манифеста на вкладке «Визуальные ресурсы» в подразделе «Экран заставка». Как уже упоминалось в предыдущей статье, открыть редактор манифеста можно двойным щелчком по файлу Package.appxmanifest в обозревателе объектов. Данный редактор развивается вместе с платформой UWP и в последних версиях VS появился инструмент, который позволяет быстро сформировать из одного изображения, все необходимые его вариации в разных масштабах, но об этом инструменте я буду говорить отдельно в другой статье, а здесь рассмотрю лишь ручную установку параметров.

В первом поле «Фон экрана-заставки» нужно указать цвет фона в HTML (HEX) формате. Получить значение цвета в подходящем формате можно практически в любом редакторе изображений (в том числе и в Paint 3D) или онлайн, но не забудьте о правилах записи такого цвета, согласно которым в начале цифробуквенного значения должен стоять знак «#». Например, мой экран заставка в качестве фона использует значение «#fae57a».



Второе поле «Экран-заставка» предлагает указать папку хранения и базовое имя для файла изображения. Папка «Assets» не является обязательным местом хранения изображений, как и не обязательным является имя «SplashScreen» однако необходимость менять их может возникнуть разве что при переносе старых проектов. В остальных же случаях куда проще использовать базовые значения поэтому в своем проекте я оставляю имя и путь без изменений. Также отмечу, что при указании базового имени не нужно добавлять указание на масштаб. Такая приставка в названии появляется автоматически при выборе в окошках ниже.

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



Итак, стандартный экран-заставка готов и пришло время перейти к созданию расширенной версии. Как я уже упоминал выше, в более сложных программах вместо стандартного изображения заставки показывается заставка со статусом загрузки. Расширенный экран-заставка UWP выполняет туже функцию, однако использовать его имеет смысл только в случае наличия длительных задач на старте. В моем приложении к таким задачам можно отнести: загрузку сохранений, начальный просчет изменений, обращение к базе данных в сети и проверку покупок. На разных устройствах все эти задачи займут разное время и неплохо бы, если на более слабых устройствах в этот момент пользователь понимал, что приложение не зависло.

Не смотря на закрепившееся в документации название «Расширенный экран-заставка», по факту мы будем иметь дело с самостоятельно созданной страницей, которая будет имитировать экран-заставку с добавлениями индикаторов загрузки и в любом случае ей будет предшествовать показ стандартного экрана, который мы разобрали выше. Как следствие, расширенная заставка должна по большей части повторять стандартную и максимально плавно появляться после стандартной.

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

Немного подумав, я решил реализовать индикацию загрузки с помощью вращения лопастей на мельнице и добавления текстовой строки, отображающий текущий статус загрузки. Таким образом, по задумке, стандартный экран незаметно превратится в расширенный, лопасти начнут вращение, а ниже появится текст. Однако делая первые шаги в данном вопросе, я пришел к выводу, что имеющиеся материалы охватывают вопрос не полностью, а часть используют не самые удачные «костыли».

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

Шаг 1. Создание пустой страницы


Как я уже упомянул выше расширенный экран-заставка это обычная страница приложения, которую мы сделаем максимально похожей на базовую заставку. Для добавления новой страницы в проект перейдите в меню «Проект > Добавить новый элемент» и в появившемся диалоговом окне выберите элемент «Пустая страница». В официальной документации в качестве имени данной страницы рекомендуется использовать «ExtendedSplash» и, хотя мы вольны выбрать любое другое имя, объективных причин для этого у нас нет. Указываем предлагаемое имя и жмем «Добавить».

Вновь созданная страница состоит из двух файлов: один для разметки ExtendedSplash.xaml и второй для кода ExtendedSplash.xaml.cs/vb. К ним мы вернемся немного позже, а пока следующий шаг.

Шаг 2. Назначение ExtendedSplash в качестве стартовой


В официальном руководстве данный шаг выполняется в конце, но я предпочитаю выполнять его заранее. Дело в том, что нам недостаточно просто добавить страну и, чтобы она появлялась сразу за системным экраном-заставкой, нужно выполнить некоторые манипуляции в файле App.xaml.cs/vb.

Как вы помните из предыдущей статьи это основной файл приложения, отвечающий за его жизненный цикл. В частности, в нем имеется процедура OnLaunched, которая выполняется сразу при запуске приложения и в которой задается стартовая страница.

Вот так выглядит код данной процедуры сразу после создания нового проекта (VB.NET):

Protected Overrides Sub OnLaunched(e As Windows.ApplicationModel.Activation.LaunchActivatedEventArgs)
        Dim rootFrame As Frame = TryCast(Window.Current.Content, Frame)
        If rootFrame Is Nothing Then
            rootFrame = New Frame()
            AddHandler rootFrame.NavigationFailed, AddressOf OnNavigationFailed
            If e.PreviousExecutionState = ApplicationExecutionState.Terminated Then
            End If
            Window.Current.Content = rootFrame
        End If

        If e.PrelaunchActivated = False Then
            If rootFrame.Content Is Nothing Then
                rootFrame.Navigate(GetType(MainPage), e.Arguments)
            End If
            Window.Current.Activate()
        End If
End Sub

Для сокращения кода из него убрано комментирование.

Для полного понимания, что происходит в данной процедуре потребуется более обширное углубление в тему жизненного цикла и навигации приложения, но об этом мы поговорим в одной из следующих статей. Пока все что нужно понять из данного кода это то, что сначала создается объект Frame, который задается в качестве контента (визуального корня) приложения, а затем данный Frame осуществляет навигацию к странице MainPage приложения.

И вот тут логично было бы предположить, что вместо MainPage нужно задать ExtendedSplash и мы получим нужную страницу в качестве стартовой, но данный вариант нам не подходит, так как в таком случае страница ExtendedSplash будет сохранена в истории навигации и к ней можно будет вернутся, а нам этого не нужно. Чтобы решить эту проблему, вместо навигации установим в качестве контента приложения не Frame, а непосредственно ExtendedSplash, которая хоть и является объектом Page, также может являться визуальным корнем приложения.

Исправим код следующим образом:

Protected Overrides Sub OnLaunched(e As Windows.ApplicationModel.Activation.LaunchActivatedEventArgs)
        Dim rootFrame As Frame = TryCast(Window.Current.Content, Frame)
        If rootFrame Is Nothing Then
            rootFrame = New Frame()
            AddHandler rootFrame.NavigationFailed, AddressOf OnNavigationFailed
            If (e.PreviousExecutionState <> ApplicationExecutionState.Running) Then
                Window.Current.Content = New ExtendedSplash(e.SplashScreen)
            End If
        End If
        If e.PrelaunchActivated = False Then
            If rootFrame.Content Is Nothing Then
                rootFrame.Navigate(GetType(MainPage), e.Arguments)
            End If
            Window.Current.Activate()
        End If
End Sub

Как вы заметили, изменения претерпел блок кода в первом условии. Внизу данного блока происходит проверка на то не запущено ли приложение и если оно не запущено, то в качестве контента приложения назначается новый объект страницы ExtendedSplash.

Так же в качестве параметра при объявлении нового класса ExtendedSplash предается класс SplashScreen. Этот класс является важной частью создания расширенного экрана-заставки, так как он содержит данные о размере и позиции изображения на базовом экране-заставке, что поможет нам незаметно подменить базовый экран собственным. Ну а, чтобы данный класс содержал нужные данные, мы берем его из аргументов, полученных процедурой OnLaunched от системы.

Остался последний штрих. В фале кода ExtendedSplash.xaml.cs/vb нужно добавить конструктор New, который будет принимать объект типа SplashScreen:

    Public Sub New(splashscreen As SplashScreen)
        InitializeComponent()
End Sub

Теперь при запуске приложения, мы будем видеть страницу ExtendedSplash, а не MainPage, что позволит нам сразу же просматривать внесенные в разметку страницы изменения и более удобно отлаживать код.

Шаг 3. Разметка страницы


Первое и самое простое изменение в разметке страницы ExtendedSplash, это установка фонового цвета страницы на тот же, что и у базового экрана-заставки. Вновь созданная страница уже содержит корневой элемент Grid со свойством Background. По умолчанию в данном свойстве задан системный ресурс, но мы заменим его на цвет, что ранее указывали в манифесте приложения. В принципе значение цвета можно указать на прямую Background="#fae57a", но я храню основные цвета приложения в специальном фале ApplicationStyle.xaml, о котором я говорил в конце предыдущей статьи. Ресурс для фона я именую как «BackgroundBrush», а затем ссылаюсь на него в нужных местах.

В файле ресурсов стиля:

<SolidColorBrush x:Key="BackgroundBrush" Color="#fae57a"/>
В файле ExtendedSplash.xaml:
<Grid Background="{StaticResource BackgroundBrush}"…

И вот, наконец, мы подобрались к месту, где начинаются основные отличия моего метода от описанного в официальном руководстве и прочих статьях.

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

Авторы руководства исходят из того факта, что изображение заставки может быть масштабировано по-разному в разных ситуациях. Причиной может стать как коэффициент масштабирования на разных устройствах, так и размер окна на ПК. Поведение базового изображение во всех этих ситуациях регулируется системой, а, чтобы мы могли имитировать это поведение, нам дан вышеупомянутый класс SplashScreen, единственным свойством которого является ImageLocation типа Rect. Это свойство содержит информацию о размерах (ширина, высота) и положении изображения (X,Y), поэтому вполне логично, что для манипулирования изображением на основе имеющихся данных нам подойдет элемент Canvas, который работает с объектами именно на основе таких параметров.

И на самом деле, если реализовать расширенный экран заставку так как рекомендуется, то все будет работать, но, только на ПК. Если же выполнить приложение на телефоне, то начинают происходить совершенно не предсказуемые метаморфозы. Ваше изображение либо не отобразится, либо отобразится неправильно. При повороте экрана, в горизонтальном положении, изображение убегает в край экрана, частично срезаясь и только при возврате в вертикальное положение, оно займет нужное место.

Судя по массе обсуждений в сети, а также по статьям с рекомендованными исправлениями, можно сделать вывод, что я не единственный, кто столкнулся с подобной проблемой. Наиболее популярное решение, что я нашел, рекомендует использовать разную разметку для разных устройств. Canvas для ПК и Grid для мобильных. Полностью повторив такое решение, я получил работоспособный экран-заставку в купе со стойким чувством, что использую излишние костыли.

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

Моя разметка окна на первом этапе выглядит так:

<Grid Background="{StaticResource BackgroundBrush}">
        <Image x:Name="SplashScreenImage3" Source="Assets/SplashScreens/SplashScreen3.png" Canvas.ZIndex="3" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        <Image x:Name="SplashScreenImage2" Source="Assets/SplashScreens/SplashScreen2.png" Canvas.ZIndex="2" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        <Image x:Name="SplashScreenImage1" Source="Assets/SplashScreens/SplashScreen1.png" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        <TextBlock x:Name="LoadingSatatusTextBox" HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,20" TextAlignment="Center" Foreground="{StaticResource ForegroundBrush}" FontWeight="Bold"/>
</Grid>

Прежде чем начать ее разбор, я хочу обратится к начинающим разработчикам. Если вы впервые сталкиваетесь с языком разметки XAML, я настоятельно рекомендую вам как можно меньше работать в дизайнере окна (так же называемом «Конструктор»). Несмотря на его совершенствование с каждой новой версией Visual Studio, он по-прежнему остается не точным инструментом. Например, при перетаскивании элемента управления Image на страницу, вам может показаться, что вы выровняли его по центру, но на самом деле позиционирование может установится на основе отступов, а не на основе свойств положения по вертикали и горизонтали. В нашей разметке крайне важно исключить любые отступы и прочие факторы, влияющие на позицию изображений, кроме двух свойств: HorizontalAlignment и VerticalAlignment.

Но вернемся к разметке. Для того, чтобы она работала без ошибок, в проект нужно добавить файлы изображений, которые используются в качестве источников в элементах Image. Расположил я их в подпапке «SplashScreens» папки «Assets». Для добавления изображений в проект, нажмите правой кнопкой мыши на нужную папку и выберите пункт меню: «Добавить > Существующий элемент…». Изображения три так как мне необходимо отделить статичные элементы от подвижных.



Важное замечание, что первое и третье изображение имеют размер как у базового 620х300 пикселей (для стандартного масштаба), при этом третье изображение содержит значок в том же месте, где он располагается на целом изображении, а остальная его часть прозрачная. Это сделано для того, чтобы минимизировать трудозатраты по позиционированию их относительно друг друга. Изображение же лопастей обрезано под край и не имеет лишних участков.

Все три изображения размещаются строго по центру страницы. В нашем случае по центру элемента Grid, но так как он является корневым элементом страницы и не имеет отступов, то его центр равен центру страницы. Далее нам нужно задать правильный порядок наложения изображений друг на друга, чтобы не получилась ситуация, в которой лопасти окажутся сзади мельницы. Для этого используется свойство Canvas.ZIndex, при этом совершенно не важно в каком порядке располагаются элементы Image в разметке, а важно только значение данного свойства. Чем значение выше, тем и элемент будет выше по наложению (оси Z).

Последний элемент в разметке — это текстовый блок, который будет отображать статус загрузки. Он расположен по центру относительно горизонтальной плоскости и внизу относительно вертикальной. Также указан небольшой отступ снизу, чтобы текст не «прилипал» к краю экрана. Еще два свойства указывают на цвет текста и стиль начертания.

Ресурс для цвета текста задан следующий:

<SolidColorBrush x:Key="ForegroundBrush" Color="#c89103"/>

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

Шаг 4. Код страницы


Самая важная часть работы будет проведена именно в коде страницы, что находится в файле ExtendedSplash.xaml.cs/vb.

Первым делом нужно импортировать пространство имен Windows.UI.Core и добавить переменные, которые нам понадобятся далее в коде:

Imports Windows.UI.Core
Public NotInheritable Class ExtendedSplash
    Inherits Page
    Friend splashImageRect As Rect
    Private splash As SplashScreen
    Friend ScaleFactor As Double
    Public Sub New(splashscreen As SplashScreen)
        InitializeComponent()
    End Sub
End Class

Теперь разместим необходимый код в конструкторе класса:
Public Sub New(splashscreen As SplashScreen)
    InitializeComponent()
    AddHandler Window.Current.SizeChanged, AddressOf ExtendedSplash_OnResize
    splash = splashscreen
    If splash IsNot Nothing Then
        AddHandler splash.Dismissed, AddressOf DismissedEventHandler
        ScaleFactor = DisplayInformation.GetForCurrentView().RawPixelsPerViewPixel
        splashImageRect = splash.ImageLocation
        SetSizeImage()
    End If
End Sub

Начинается код с подписки на событие изменения размеров корневого контейнера. Это понадобится, чтобы вносить изменения в размер изображения, когда изменится размер окна или ориентация устройства. Далее в локальную переменную переносится переданный в аргументе класс SplashScreen, к которому мы будем обращаться для получения информации о требуемом размере изображения.

После проверки локальной переменной splash, которая не должна быть пустой, переходим к дальнейшим манипуляциям. Теперь можно подписаться на событие Dismissed, которое возникает, когда системный экран-заставка закрывается. Этот момент важен для понимания общего принципа работы экрана-заставки. По сути пока выполняется код внутри процедуры New все еще идет инициализация страницы и она не готова к отображению, поэтому базовый экран-заставка «подстраховывает» приложение показывая, что оно запущено и сейчас появится нужная страница. Во многом именно поэтому и нужен расширенный экран-заставка, чтобы длительные процессы инициализации сопровождали живой визуализацией, а не статичной заплаткой.

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

Само собой, после добавления этого кода мы получим три сообщения об ошибке, так как у нас нет процедур, на которые привязаны вышеуказанные события, а также отсутствует процедура SetSizeImage.

Добавим их:

Private Sub SetSizeImage()
End Sub

Private Sub DismissedEventHandler(sender As SplashScreen, args As Object)
End Sub

Private Sub ExtendedSplash_OnResize(sender As Object, e As WindowSizeChangedEventArgs)
End Sub

И приступим к манипуляциям по установке размера изображения в процедуре SetSizeImage:
Private Sub SetSizeImage()
        SplashScreenImage1.Height = splashImageRect.Height / ScaleFactor
        SplashScreenImage1.Width = splashImageRect.Width / ScaleFactor

        SplashScreenImage2.Width = splashImageRect.Width / ScaleFactor * 0.3073
        SplashScreenImage2.Margin = New Thickness(-splashImageRect.Width / ScaleFactor * 0.547, -splashImageRect.Width / ScaleFactor * 0.1633, 0, 0)

        SplashScreenImage3.Height = splashImageRect.Height / ScaleFactor
        SplashScreenImage3.Width = splashImageRect.Width / ScaleFactor
        ScaleFactor = 1
End Sub

Этот код по очереди устанавливает размеры для каждого изображения, помещенного на страницу. В случае с первым и третьим изображением, нам достаточно установить ширину и высоту изображения равными тем, что мы изъяли из класса SplashScreen с поправкой на коэффициент масштабирования. Обратите внимания, что последней строчкой переменной ScaleFactor присваивается единица и при повторном вызове данной процедуры поправки на коэффициент масштабирования фактически происходить не будет. Связано это с той же проблемой по которой на мобильных не работает решение из официального руководства. По какой-то причине класс SplashScreen пи первом запуске сообщает размер изображения с учетом коэффициента масштабирования, а при дальнейших манипуляциях, таких как поворот устройства, уже без него. Именно поэтому при инициализации данный коэффициент сохраняется для первой манипуляции и сбрасывается до единицы для дальнейших.

Абсолютно не исключено, что в одной из дальнейших сборок Windows 10 данная странность будет устранена, и нам понадобится изменить данный код, но пока он исправно работает на всех вышедших сборках вплоть до Fall Creators Update.

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

К слову, размер достаточно задать только для одной из сторон, например, для ширины, так как изображение квадратное и пропорционально изменит высоту на тот же размер. При установке размера кроме поправки на коэффициент масштабирования так же корректируем размер исходя из основного изображения. Точнее берем за основу ширину изображения и высчитываем какую долю от этого размера занимают лопасти. В моем случае это 0,3073.

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

Запускаем приложение и видим, что на данном этапе результат визуально не отличается от базового экрана-заставки. Теперь запускаем приложение на мобильном и видим небольшое смещение нашего изображения относительно базового. Вызвано это тем, что базовый экран позиционирует себя по середине экрана полностью игнорируя такие элементы интерфейса как строка состояния вверху телефона или нижняя виртуальная панель кнопок, что присутствует на некоторых моделях. Давайте внесем требуемые изменения в код.

Сначала подготовим переменную для хранения высоты строки состояния:

Friend statusBarRect As Rect
После в процедуре инициализации страницы запросим ее размер:
If Metadata.ApiInformation.IsTypePresent("Windows.UI.ViewManagement.StatusBar") Then
       statusBarRect = StatusBar.GetForCurrentView.OccludedRect
End If

Этот код интересен сразу двумя моментами. Во-первых, здесь мы впервые сталкиваемся с адаптивным кодом, который я упоминал в предыдущей статье. Суть условия в том, что мы проверяем наличие нужного класса на конечном устройстве и если он есть, то код внутри условия выполнится без ошибок.

Несмотря на адаптивный код, обращение к StatusBar не приведет к успеху, и мы получим сообщение о об отсутствии такого класса. Связано это с тем, что приложения UWP изначально имеют только общий код, который способен выполняться на всех поддерживаемых устройствах без каких-либо манипуляций. Ссылки на классы специфические для конкретной платформы, добавляются отдельно. Строка состояния присуща только мобильным устройствам, поэтому сначала нужно добавить ссылку на нужную библиотеку классов. Переходим в меню «Проект > Добавить ссылку…». Находим слева раздел «Universal Windows» и подраздел «Расширения». В списке библиотек находим библиотеку «Windows Mobile Extensions for the UWP» и выбираем версию равную минимальной версии нашего приложения.

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

Напоследок откорректируем код позиционирования и установки размеров изображений:

Private Sub SetSizeImage()
        SplashScreenImage1.Height = splashImageRect.Height / ScaleFactor
        SplashScreenImage1.Width = splashImageRect.Width / ScaleFactor
        SplashScreenImage1.Margin = New Thickness(0, -statusBarRect.Height, 0, 0)

        SplashScreenImage2.Width = splashImageRect.Width / ScaleFactor * 0.3073
        SplashScreenImage2.Margin = New Thickness(-splashImageRect.Width / ScaleFactor * 0.547, -splashImageRect.Width / ScaleFactor * 0.1633 - statusBarRect.Height, 0, 0)

        SplashScreenImage3.Height = splashImageRect.Height / ScaleFactor
        SplashScreenImage3.Width = splashImageRect.Width / ScaleFactor
        SplashScreenImage3.Margin = New Thickness(0, -statusBarRect.Height, 0, 0)

        ScaleFactor = 1
End Sub

Добавленный код сдвигает каждое изображение вверх на размер равный высоте строки состояния и теперь при запуске приложения на любом устройстве наше изображение находится на том же месте, что и базовое. К сожалению это решение все равно не полное. Некоторые модели телефонов не имеют аппаратных кнопок, а вместо них кнопки управления телефоном располагаются на виртуальной панели самого телефона, что приводит к такому же эффекту смещения и, как я не искал, так и не смог найти информации о том, где получить высоту данной панели, да и в целом получить информацию о ее наличии или отсутствии, чтобы так же внести необходимы сдвиги. Если вы располагаете данной информацией, то обязательно делитесь ею в комментариях.

Теперь, когда все необходимые корректировки внесены, осталось добавить реакцию на изменения размеров окна или ориентации устройства:

Private Sub ExtendedSplash_OnResize(sender As Object, e As WindowSizeChangedEventArgs)
        If splash IsNot Nothing Then
            splashImageRect = splash.ImageLocation
            SetSizeImage()
        End If
End Sub

Тут все предельно просто. Проверяем не является ли объект, хранящий класс SplashScreen пустым и, если нет, то изымаем из него информацию о размере, а затем повторно выполняем процедуру SetSizeImage.

С имитацией базового экрана мы разобрались. Теперь переходим к тому месту, где мы извлекаем из этого пользу. Как я уже упомянул выше, после закрытия базового экрана-заставки возникает соответствующее событие, которое приводит нас в процедуру DismissedEventHandler. Эта процедура выполняется в другом потоке, поэтому использовать ее для наших задач не удобно. Отредактируем процедуру следующим образом:

Private Async Sub DismissedEventHandler(sender As SplashScreen, args As Object)
        Await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, New DispatchedHandler(AddressOf DismissExtendedSplash))
End Sub

Эта строка вызывает новую процедуру с именем DismissExtendedSplash в основном потоке и уже в ней мы можем выполнять все те задачи, ради которых создавался расширенный экран заставка. Заметьте, что тут мы используем ключевые слова Async и Await, которые являются частью асинхронного программирования. В двух словах это работает так: ключевое слово Await говорит, что данный код будет выполнятся как бы в фоне, при этом основной поток не блокируется и интерфейс остается отзывчивым. Await возможно применить только к асинхронным процедурам/функциям и только если процедура/функция, в которой мы его используем имеет пометку Async.

Процедура DismissExtendedSplash, к которой мы перешли будет выглядеть так:

Private Async Sub DismissExtendedSplash()
    LoadingSatatusTextBox.Text = "Загрузка данных..."
    Await Task.Delay(New TimeSpan(0, 0, 15))
   
    Dim rootFrame As New Frame
    rootFrame.Navigate(GetType(MainPage))
    Window.Current.Content = rootFrame
End Sub

Ее содержимое добавлено исключительно для наглядности. В полноценном приложении тут будет происходить загрузка настроек, проверка покупок, синхронизация с облаком и прочие длительные начальные действия. Для имитации этих действий добавлены две первые строчки. Одна устанавливает текст для отображения статуса загрузки, а вторая приостанавливает выполнение на 15 секунд. Конечно же в хорошем приложении даже длительные стартовые задачи, не должны занимать 15 секунд, но в нашем случае это простая условность.

После истечения заданного времени выполнение переходит далее, где мы создаем новый элемент Frame, осуществляем в нем навигацию к основной странице и подменяем им корневой контент приложения. Эти строчки обязательно должны бить в конце данной процедуры, что бы после выполнения всех длительных задач пользователь увидел основную страницу приложения.
Результат: после запуска приложения мы уже видим, как спустя доли секунды внизу появляется текст и через 15 секунд открывается основная страница.

Шаг 5. Добавление индикатора загрузки


Хоть мы и добавили текст, показывающий статус загрузки приложения, страница все равно большую часть времени остается статичной и выглядит «зависшей». Исправить положение поможет анимация вращения для лопастей мельницы.

Модифицируем XAML страницы следующим образом:

<Grid Background="{StaticResource BackgroundBrush}">
        <Grid.Resources>
            <Storyboard x:Name="ImageRotateAnimation">
                <DoubleAnimation Storyboard.TargetName="ImageRotate" Storyboard.TargetProperty="Angle" To="360" BeginTime="0:0:0.5" Duration="0:0:5" RepeatBehavior="Forever"/>
            </Storyboard>
        </Grid.Resources>
        <Image x:Name="SplashScreenImage3" Source="Assets/SplashScreens/SplashScreen3.png" Canvas.ZIndex="3" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        <Image x:Name="SplashScreenImage2" Source="Assets/SplashScreens/SplashScreen2.png" Canvas.ZIndex="2" HorizontalAlignment="Center" VerticalAlignment="Center" RenderTransformOrigin="0.5 0.5">
            <Image.RenderTransform>
                <RotateTransform x:Name="ImageRotate" Angle="0"/>
            </Image.RenderTransform>
        </Image>
        <Image x:Name="SplashScreenImage1" Source="Assets/SplashScreens/SplashScreen1.png" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        <TextBlock x:Name="LoadingSatatusTextBox" HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,20" TextAlignment="Center" Foreground="{StaticResource ForegroundBrush}" FontWeight="Bold"/>
</Grid>

Изменения коснулись двух участков разметки. Во-первых, для второго изображения была добавлена трансформация наклона на 0 градусов. Фактически это сделано для того, чтобы дать трансформации имя и получить возможность обращаться к ней по имени в анимации.

Во-вторых, в ресурсы корневого элемента добавлен объект Storyboard, внутри которого расположена анимация Double. В ней мы указываем имя объекта и свойства с типом Double к которому применится анимация. Анимация является бесконечной и протекает от стартового до заданного положения за пять секунд. Тема анимации довольно обширна и очень интересна, поэтому однозначно еще будет затронута в данном цикле.

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

ImageRotateAnimation.Begin

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



Полный пример можно скачать по этой ссылке.

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

Комментарии (2)


  1. DonAlPAtino
    30.03.2018 15:30

    Вы бы что ли github завели…


    1. lxgdark
      30.03.2018 16:09

      Просто не нашел аргументов в пользу публикации на GitHub. Основной проект я делать открытым не планирую, а примеры к статьям будут появляться не так часто. Тут я скорее руководствовался возможностью быстро проверить работоспособность конкретного решения с конкретными изображениями. К тому же в качестве Git я использую VSTS.
      Впрочем, если у вас есть аргументы, буду рад их услышать )))