Использование особенных функций ОС Аврора

При разработке приложений для ОС Аврора существует ряд функциональных возможностей и шаблонных решений, которых следует придерживаться для улучшения качества вашего приложения. Модуль Sailfish.Silica предоставляет доступ к этим возможностям для приведения пользовательских интерфейсов в соответствие со стандартами приложений ОС Аврора.

Стек страниц приложения

Каждое приложение ОС Аврора содержит стек страниц, в котором содержатся экраны с содержимым для отображения в окне приложения. Пользователь может переключаться между этими экранами (или, другими словами, страницами); при этом на экране устройства одновременно может показываться только одна страница. Чтобы показать новую страницу, ее следует поместить в верх стека. Чтобы закрыть текущую страницу и вернуться к предыдущей, следует удалить страницу из стека.

Страницы создаются с помощью типа Page, а стек страниц доступен в виде объекта типа PageStack, к которому можно получить доступ через свойство pageStack окна приложения. Первая страница в стеке, отображаемая после запуска приложения, задается через свойство initialPage окна приложения.

Для добавления страницы в стек следует вызвать метод push() стека страниц, в который передается URL файла QML либо объект типа Component с корневой страницей Page. Для удаления страницы из вершины стека (т.е. отображаемой в данный момент страницы) следует вызвать метод pop() стека страниц. Для обращения к странице на вершине стека используется свойство currentPage, а для получения количества страниц в стеке используется свойство depth.

Ниже приведен пример стека страниц в действии. Здесь страницы добавляются в стек с методом push() и удаляются методом pop():

 import QtQuick 2.2
 import Sailfish.Silica 1.0

 ApplicationWindow {
     initialPage: pageComponent

     Component {
         id: pageComponent

         Page {
             PageHeader {
                 title: "Количество страниц: " + pageStack.depth
             }

             Column {
                 width: parent.width
                 anchors.centerIn: parent
                 spacing: Theme.paddingLarge

                 Button {
                     text: "Добавить страницу"
                     anchors.horizontalCenter: parent.horizontalCenter
                     onClicked: {
                         pageStack.push(pageComponent)
                     }
                 }

                 Button {
                     text: "Удалить страницу"
                     anchors.horizontalCenter: parent.horizontalCenter
                     onClicked: {
                         pageStack.pop()
                     }
                 }
             }
         }
     }
 }

Если провести по экрану слева направо или коснуться индикатора предыдущей страницы в левом верхнем углу экрана, произойдет удаление страницы из стека аналогично вызову метода pop().

Если страница определена в отдельном QML-файле, то для добавления ее в стек предпочтительнее использовать URL этой страницы в вызове метода push() вместо передачи компонента Component:

 Button {
     text: "Добавить страницу"
     onClicked: pageStack.push(Qt.resolvedUrl("MyPage.qml"))
 }

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

Имеется еще несколько операций, которые можно выполнить с помощью стека страниц PageStack, а именно:

  • При вызове метода push() в него можно передавать набор первоначальных свойств для страницы либо PageStackAction.Immediate в качестве значения параметра operationType для отключения анимации при добавлении новой страницы;
  • При вызове метода pop() в него можно передавать страницу, расположенную ниже в стеке; в этом случае будут удалены все страницы, расположенные выше в стеке;
  • Вместо добавления новой страницы в стек методом push() можно заменять текущую страницу на вершине стека методом replace();
  • Вместо метода push() можно использовать метод pushAttached() для добавления новой страницы в стек без ее отображения на экране; жестом смахивания вперед пользователь сможет отобразить эту страницу.

Дополнительную информацию можно получить в описании типа PageStack.

Использование диалогов для подтверждения ввода

Диалог — это специальный тип страницы, на которой отображается содержимое или запрашивается ввод пользователя, требующие от него подтверждения или отмены. Тип Dialog поддерживает жесты смахивания вперед для подтверждения действия и смахивания назад (так же как и в обычных страницах Page) для отмены действия. Кроме того, диалоги используют тип DialogHeader (вместо PageHeader) для отображения кнопок "Принять" и "Отменить" в верхней части страницы.

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

 import QtQuick 2.2
 import Sailfish.Silica 1.0

 ApplicationWindow {
     initialPage: firstPage

     Component {
         id: firstPage
         Page {
             PageHeader { id: header }

             Button {
                 text: "Показать диалог"
                 anchors.centerIn: parent
                 onClicked: {
                     pageStack.push(dialogComponent)
                 }
             }
         }
     }

     Component {
         id: dialogComponent
         Dialog {
             property string selectedOption: options.currentItem.text

             DialogHeader {
                 id: header
                 title: "Выберите опцию"
             }

             ComboBox {
                 id: options
                 label: "Опции:"
                 anchors.top: header.bottom
                 width: parent.width

                 menu: ContextMenu {
                     MenuItem { text: "Опция 1" }
                     MenuItem { text: "Опция 2" }
                     MenuItem { text: "Опция 3" }
                 }
             }
         }
     }
 }

В объекте типа Dialog имеются сигналы accepted и rejected, которые испускаются после принятия (нажатие кнопки "Принять" или выполнение жеста смахивания вперед) или отклонения (нажатие кнопки "Отмена" или выполнение жеста смахивания назад) диалога, соответственно. Давайте получим обратную связь после подтверждения выбора опции в диалоге. Для этого изменим первую страницу так, чтобы на ней отображался результат после испускания сигнала accepted():

 Component {
     id: firstPage
     Page {
         PageHeader { id: header }

         Button {
             text: "Показать диалог"
             anchors.centerIn: parent
             onClicked: {
                 var dialog = pageStack.push(dialogComponent)
                 dialog.accepted.connect(function() {
                     header.title = "Выбрана " + dialog.selectedOption
                 })
             }
         }
     }
 }

Обратите внимание, что метод PageStack::push() возвращает экземпляр страницы, добавленной в стек; в данном случае это экземпляр объекта с идентификатором dialogComponent. Так мы сможем подключиться к сигналу accepted().

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

Управление компоновкой и ориентацией пользовательского интерфейса

Использование объекта Theme для создания масштабируемых компоновок

Объект Theme настроен таким образом, что значения его свойств, отвечающих за размеры и отступы, динамически адаптируются в соответствии с пропорциями экрана и его разрешением. Этому объекту следует отдавать предпочтение вместо жестко заданных размеров и отступов для компонентов приложения. Использование объекта Theme обеспечивает единство стиля оформления с другими приложениями ОС Аврора. Например:

  • Для задания отступов слева и справа между границами страницы и ее содержимым следует использовать свойство Theme.horizontalPageMargin.
  • В объектах ListItem, состоящих только лишь из значка и текстовой метки, размер значка следует указывать равным Theme.iconSizeMedium, а размер шрифта текстовой метки — равным Theme.fontSizeMedium (стандартный размер шрифта для объектов Label).
  • В объектах ListItem, состоящих из двух строк текста, высота элемента (свойство contentHeight) обычно устанавливается равной Theme.itemSizeMedium.
  • Ширину одиночных кнопок следует устанавливать равной Theme.buttonWidthLarge.

Дополнительную информацию можно получить в описании типа Theme.

Динамическое обновление компоновки при смене ориентации экрана

При повороте устройства и смене ориентации экрана приложения ОС Аврора могут использовать соответствующие свойства для адаптации своего пользовательского интерфейса.

Для этого необходимо выполнить следующее:

  • Установить свойству allowedOrientations страницы значение для поддержки всех необходимых ориентаций экрана.
  • С помощью свойств страницы isPortrait, isLandscape и orientation соответствующим образом обновлять пользовательский интерфейс при смене ориентации экрана.

В качестве примера рассмотрим приложение, которое показывает изображение и его основные метаданные (имя файла, ширина и высота). В портретной ориентации экрана изображение Image центрируется по горизонтали в верхней части экрана, а в столбце из объектов типа DetailItem показываются метаданные изображения:

 import QtQuick 2.2
 import Sailfish.Silica 1.0

 ApplicationWindow {
     initialPage: Component {
         Page {
             id: page

             PageHeader {
                 id: header
                 title: "Параметры изображения"
             }

             Image {
                 id: image
                 anchors {
                     top: header.bottom
                     horizontalCenter: parent.horizontalCenter
                 }
                 sourceSize.width: page.width - (Theme.horizontalPageMargin * 2)
                 fillMode: Image.PreserveAspectFit
                 source: "/home/nemo/Pictures/img_0001.jpg"
             }

             Column {
                 anchors {
                     top: image.bottom
                     topMargin: Theme.paddingLarge

                     left: parent.left
                     leftMargin: Theme.horizontalPageMargin

                     right: parent.right
                     rightMargin: Theme.horizontalPageMargin
                 }

                 DetailItem { label: "Имя файла"; value: image.source }
                 DetailItem { label: "Ширина"; value: image.width }
                 DetailItem { label: "Высота"; value: image.height }
             }
         }
     }
 }

Чтобы страница начала реагировать на изменения ориентации, ее свойству allowedOrientations следует задать значение, соответствующее всем поддерживаемым ориентациям экрана. Значение может быть любой комбинацией из: Orientation.Portrait, Orientation.PortraitInverted, Orientation.Landscape, Orientation.LandscapeInverted. Кроме того, есть специальное значение Orientation.All для поддержки всех возможных ориентаций экрана, которое мы сейчас и используем в нашем примере:

 Page {
     allowedOrientations: Orientation.All

     // остальной код страницы...
 }

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

Однако, как в этом можно убедиться после запуска приложения, новая компоновка пользовательского интерфейса работает не самым лучшим образом в портретной ориентации экрана: если изображение имеет большие размеры, то область с метаданными может выйти за пределы экрана. Вместо того, чтобы добавлять контейнер SilicaFlickable для включения прокрутки, мы можем изменить компоновку пользовательского интерфейса так, чтобы она автоматически адаптировалась под портретную ориентацию экрана: изображение можно показывать слева, а область с метаданными — справа. Определить текущую ориентацию экрана можно с помощью значения свойства orientation. Также для удобства имеются два булевых свойства isPortrait и isLandscape, которые возвращают истинное значение, если ориентация портретная и альбомная, соответственно.

Внесем следующие изменения в нашем примере:

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

Ниже приведен пример кода с соответствующими изменениями:

 Image {
     id: image

     anchors {
         top: header.bottom
         horizontalCenter: page.isPortrait ? parent.horizontalCenter : undefined
     }

     sourceSize.width: {
         var maxImageWidth = Screen.height/2
         var leftMargin = Theme.horizontalPageMargin
         var rightMargin = page.isPortrait ? Theme.horizontalPageMargin : 0
         return maxImageWidth - leftMargin - rightMargin
     }

     // rest of image code...
 }

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

 Column {
     anchors {
         // в альбомной ориентации верх столбца привязывается к нижней границе заголовка (вместо нижней границы изображения)
         top: page.isPortrait ? image.bottom : header.bottom
         topMargin: page.isPortrait ? Theme.paddingLarge : 0

         // в альбомной ориентации левая граница столбца привязывается к правой границе изображения с
         // отступом Theme.paddingLarge между ними
         left: page.isPortrait ? parent.left : image.right
         leftMargin: page.isPortrait ? Theme.horizontalPageMargin : Theme.paddingLarge

         right: parent.right
         rightMargin: Theme.horizontalPageMargin
     }

     // остальной код столбца...
 }

Дополнительно можно настроить анимации перехода при смене одной ориентации на другую с помощью свойства orientationTransitions. Например, с его помощью можно анимировать изменение положения элементов на странице.

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

Жизненный цикл приложения

Состояния приложения

ОС Аврора является по-настоящему многозадачной операционной системой. Пользователь может при необходимости отправлять приложения в фоновый режим. Для поддержки таких режимов приложения могут обладать двумя состояниями:

  • активное состояние: приложение занимает все доступное пространство экрана;
  • фоновый режим: приложение представляется в виде обложки на домашнем экране.

Определить состояние приложения можно с помощью свойства Qt.application.state. Значение данного свойства равно Qt.ApplicationActive, когда приложение работает в обычном режиме (активное состояние), и Qt.ApplicationInactive, когда приложение работает в фоновом режиме.

При работе в фоновом режиме приложение должно минимизировать потребление ресурсов. Все анимации по возможности должны быть приостановлены, а само приложение должно освободить неиспользуемые ресурсы:

 Label {
     // Вращающаяся текстовая метка
     text: "Hello world!"
     anchors.centerIn: parent
     RotationAnimation on rotation {
         from: 0
         to: 360
         duration: 2000
         loops: Animation.Infinite
         running: Qt.application.state == Qt.ApplicationActive // but only when active
     }
 }

Обложки приложения

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

Ниже приведен пример приложение, которое отображает на экране палитру цветов (тип ColorPicker). После выбора цвета и перевода приложения в фоновый режим выбранный цвет будет отображаться на обложке:

 import QtQuick 2.2
 import Sailfish.Silica 1.0

 ApplicationWindow {
     id: appWindow

     property color selectedColor

     initialPage: Component {
         Page {
             ColorPicker {
                 onColorClicked: appWindow.selectedColor = color
             }
         }
     }

     cover: Component {
         Cover {
             Rectangle {
                 anchors.fill: parent
                 color: appWindow.selectedColor
             }
         }
     }
 }

Обратите внимание, что если приложение работает в фоновом режиме, все анимации и ресурсоёмкие задачи на обложке следует запускать только тогда, когда значением свойства обложки status является Cover.Active.

В примере выше свойство ApplicationWindow::cover задано как элемент типа Component. Более предпочтительным способом описания обложки приложения будет реализация ее в виде отдельного QML-файла. URL этого файла передается в свойство cover, что в конечном итоге ускоряет запуск приложения за счет экономии времени на компиляцию QML-компонента обложки.

Действия для обложки

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

Каждое действие для обложки описывается в объекте CoverAction. Все такие действия помещаются в контейнер CoverActionList, который, в свою очередь, помещается в объект Cover. В каждом объекте CoverAction в свойстве iconSource задается путь к значку, который отображается на кнопке действия. Само действие описывается в обработчике сигнала onTriggered. В предыдущем примере (с палитрой) кнопкой на обложке приложения можно было бы сбрасывать цвет на белый:

 ApplicationWindow {
     cover: Component {
         Cover {
             id: appCover

             CoverActionList {
                 CoverAction {
                     iconSource: "icon.png"
                     onTriggered: appWindow.selectedColor = "white"
                 }
             }

             // остальной код обложки...
         }
     }
 }

Остановка приложения

Пользователь может закрыть приложение в любое время. В этом случае все необходимые действия по освобождению ресурсов можно выполнять в обработчике Component.onDestruction компонента ApplicationWindow.