Разработка для SailfishOS: меню

  • Tutorial
Здравствуйте! Очередное продолжение цикла статей о разработке для мобильной платформы SaifishOS. На этот раз я хочу рассказать о том, как в приложении реализовать различного вида меню. Данная тема заслуживает отдельной статьи, поскольку меню в SailfishOS сами по себе выглядят достаточно интересно и не похожи на меню в других мобильных платформах.

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

Полноразмерный скриншот


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

Полноразмерный скриншот


Если потянуть на экране вниз, то появится то самое меню:



Интересной особенностью взаимодействия с меню в SailfishOS является то, что выбрать какой-либо пункт меню можно двумя способами. Можно просто сделать свайп вниз, пока меню не появится полностью (как на скриншоте выше). Тогда оно останется на экране и можно будет просто ткнуть в нужный пункт меню. А можно потянуть вниз не до конца и тогда по мере появления меню его пункты будут подсвечиваться, как на скриншоте снизу:



Если в этот момент убрать палец с экрана, то будет выбран подсвеченный пункт меню.

PullDownMenu


Реализация такого меню в Sailfish довольно проста. Для этого в Sailfish Silica присутствует компонент PullDownMenu. Однако, данный компонент имеет ряд особенностей, которые необходимо знать перед тем как начинать его использовать.

Во-первых, поскольку само меню вызывается с помощью свайп жеста, то PullDownMenu можно использовать только внутри контейнеров, которые данный жест позволяют. В Sailfish Silica такими контейнерами являются:

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

  • SilicaListView — компонент отображающий список элементов, который наследует стандартный QML компонент ListView.

  • SilicaGridView — так же как и предыдущий компонент, используется для отображения списка элементов, но не в виде вертикального списка, а в виде сетки. Наследует стандартный QML компонент GridView.

  • SilicaWebView — компонент для отображения веб содержимого, наследует стандартный QML компонент WebView.

Во-вторых, содержимым PullDownMenu должны быть компоненты типа MenuItem или MenuLabel (на самом деле нет, см. текст ниже). Первый представляет собой интерактивный пункт меню и имеет ряд следующих свойств:

  • text — непосредственно текст пункта меню.
  • color — цвет текста, определенного в предыдущем свойстве.
  • horizontalAlignment — горизонтальное выравнивание текста пункта меню. Может быть одним из следующих значений: Text.AlignLeft, Text.AlignRight, Text.AlignHCenter (используется по умолчанию) или Text.AlignJustify.
  • down — значение данного свойства равно true, когда пункт меню был выбран.

Помимо этих свойств, MenuItem так же содержит достаточно большое количество свойств вида font. для настройки шрифта пункта меню (о них можно прочитать в документации) и обработчик сигнала onClicked(), в котором определяются действия, которые необходимо выполнить при выборе данного пункта меню.

MenuLabel представляет собой статичный пункт меню, который просто отображает некоторый текст и не может быть нажат. Такие пункты используются, например, в качестве заголовка меню или как разделители между интерактивными пунктами меню. Естественно, MenuLabel содержит меньше свойств, чем PullDownMenu:

  • text — текст пункта меню.
  • color — цвет текста, определенного в предыдущем свойстве.
  • verticalOffset — вертикальный отступ.

UPD: Конечно-же, содержимым PullDownMenu, как и других меню, рассмотренных в данной статье, может быть любой компонент, а не только MenuItem или MenuLabel. Однако, в случае использования других компонентов всю логику взаимодействия разработчику придется реализовывать самому. Да и выглядеть такое меню будет уже не нативно и, следовательно, использовать подобные возможности стоит только в редких случаях. В стандартных же ситуациях достаточно MenuItem и MenuLabel, поэтому использование других компонентов внутри меню в данной статье рассмотрено не будет.

Минимальный пример страницы с PullDownMenu будет выглядеть следующим образом:

Page {
    id: page
    SilicaFlickable {
        anchors.fill: parent
        contentHeight: column.height

        PullDownMenu {
            MenuItem {
                text: qsTr("Пункт меню 3")
                onClicked: console.log("Нажат третий пункт меню")
            }
            MenuLabel {
                text: qsTr("Подраздел")
            }
            MenuItem {
                text: qsTr("Пункт меню 2")
                onClicked: console.log("Нажат второй пункт меню")
            }
            MenuItem {
                text: qsTr("Пункт меню 1")
                onClicked: console.log("Нажат первый пункт меню")
            }
            MenuLabel {
                text: qsTr("Меню приложения")
            }
        }

        Column {
            id: column

            width: page.width
            spacing: Theme.paddingLarge
            PageHeader {
                title: qsTr("Моё приложение")
            }
            Label {
                text: "Привет, Хабр!"
                width: page.width
                horizontalAlignment: Text.AlignHCenter
                font.pixelSize: Theme.fontSizeExtraLarge
            }
        }
    }
}

Само же меню будет выглядеть так:



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

Свойства PullDownMenu


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

  • spacing — расстояние между нижним краем меню и верхней границей контента страницы. По умолчанию это значение равно нулю.
  • topMargin — расстояние между верхним краем меню (то же самое, что верхней кран экрана) и верхним краем самого верхнего пункта меню. По умолчанию это значение равно Theme.itemSizeSmall.
  • bottomMargin — расстояние между нижним краем самого нижнего пункта меню и нижним краем самого меню.

Интересной особенностью последнего свойства (bottomMargin) является то, что его значение по умолчанию меняется в зависимости от содержимого меню. Если самый нижний элемент меню это MenuLabel, то значение свойства равно 0. В противном же случае значение свойства равно высоте компонента MenuLabel. Посмотреть разницу можно на примерах ниже:


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

Помимо размеров можно так же менять другие параметры внешнего вида PullDownMenu с помощью следующих свойств:

  • background — позволяет описать компонент, который будет использоваться в качестве фона меню.
  • backgroundColor — цвет фона меню. Примечательно, что система сама применяет градиент к указанному цвету.
  • highlightColor — цвет подсветки выбранного элемента меню, а так же индикатора меню, отображаемого сверху экрана, когда само меню закрыто.
  • menuIndicator — позволяет описать компонент, который будет использоваться в качестве индикатора меню, отображаемого сверху экрана, когда само меню закрыто.

Можно изменить цвета в меню из примера выше на следующие:

backgroundColor: "red"
highlightColor: "green"

Тогда получится меню следующего вида:



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

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

  • active — свойство логического типа, значение которого равно true, если меню полностью или частично присутствует на экране.
  • busy — так же свойство логического типа. Если его значение установить равным true, то индикатор меню вверху экрана начнет «пульсировать». При этом само меню будет все так же доступно. Данное свойство удобно использовать, если необходимо показать пользователю, что какой-то процесс все еще выполняется.
  • flickable — с помощью этого свойства можно указать flickable компонент, с помощью которого будет активироваться меню. Т.е. вместо того, чтобы помещать PullDownMenu внутри какого-либо контейнера, можно просто указать этот контейнер значением для данного свойства.
  • quickSelect — свойство логического типа, которое позволяет включить функцию быстрого выбора для меню. Данная функция работает только на меню всего с одним пунктом. Если эта функция активирована, то при любой прокрутке меню (в том числе и до конца) будет автоматически выбран его единственный пункт.

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

Метод close() позволяет вручную закрыть меню. При этом можно в качестве аргумента данного метода можно указать true и тогда меню закроется мгновенно, без анимации. Например, в нижеприведенном коде, при выборе пункта меню «Пункт меню 2», меню закроется без анимации:

PullDownMenu {
    id: menu
    MenuItem {
        text: qsTr("Пункт меню 2")
        onClicked: menu.close(true)
    }
    MenuItem {
        text: qsTr("Пункт меню 1")
        onClicked: console.log("Нажат первый пункт меню")
    }
}

PushUpMenu


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

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

В коде такой пример будет выглядеть так:

Page {
    id: page
    SilicaListView {
        PushUpMenu {
            MenuItem {
                text: qsTr("Пункт меню 3")
                onClicked: console.log("Нажат третий пункт меню")
            }
            MenuItem {
                text: qsTr("Пункт меню 2")
                onClicked: console.log("Нажат второй пункт меню")
            }
            MenuItem {
                text: qsTr("Пункт меню 1")
                onClicked: console.log("Нажат первый пункт меню")
            }
            MenuLabel {
                text: qsTr("Меню приложения")
            }
        }

        id: listView
        model: 20
        anchors.fill: parent
        header: PageHeader {
            title: "Простой список"
        }
        delegate: BackgroundItem {
            id: delegate
            Label {
                x: Theme.paddingLarge
                text: "Элемент #" + index
                anchors.verticalCenter: parent.verticalCenter
                color: delegate.highlighted ? Theme.highlightColor : Theme.primaryColor
            }
        }
        VerticalScrollDecorator {}
    }
}

В результате, добраться до меню можно только если прокрутить весь список до конца:

ContextMenu


Последний тип меню в SailfishOS, который будет рассмотрен в данной статье, это контекстное меню. Оно реализуется с помощью компонента ContextMenu и представляет собой всплывающее меню, которое можно ассоциировать с каким-либо элементом пользовательского интерфейса. Содержимое такого меню описывается так же, как и для PushUpMenu и PullDownMenu, с помощью компонентов MenuItem и MenuLabel.

Чаще всего такие меню используются для реализации контекстного меню элементов списка. Для этого у компонента ListItem, который используется для описания делегатов списка, есть специальное свойство menu. Так можно добавить контекстное меню к элементам списка из последнего примера. Для этого придется немного изменить делегат, чтобы он был реализован через ListItem, и добавить к нему само меню:

delegate: ListItem {
    id: delegate
    Label {
        id: label
        x: Theme.paddingLarge
        text: "Элемент #" + index
        anchors.verticalCenter: parent.verticalCenter
        color: delegate.highlighted ? Theme.highlightColor : Theme.primaryColor
    }
    menu: ContextMenu {
        MenuLabel {
            text: "Контекстное меню"
        }
        MenuItem {
            text: "Выделить жирным"
            onClicked: label.font.bold = !label.font.bold
        }
        MenuItem {
            text: "Выделить курсивом"
            onClicked: label.font.italic = !label.font.italic
        }
    }
}

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



При выборе пунктов данного меню изменится стиль текста элемента списка:

Контекстное меню закроется, если выбрать один из пунктов меню или просто тапнуть вне этого меню. Однако, компонент ListItem так же содержит методы hideMenu() и showMenu(), которые позволяют скрыть или показать контекстное меню вручную. Последнему методу можно в качестве параметра передать список свойств компонента ContextMenu, которые будут применены к меню (свойства компонента ContextMenu будут рассмотрены чуть позже). Кроме того, стандартное поведение для контекстного меню элемента списка можно изменить, установив свойству showMenuOnPressAndHold компонента ListItem значение false. В этом случае контекстное меню не будет появляться при долгом тапе по элементу. Наконец, узнать показано контекстное меню или нет можно с помощью свойства menuOpen компонента ListItem.

Контекстное меню можно показывать и вне списка, связав их с обычными элементами интерфейса. Для этого компонент ContextMenu имеет метод show(), которому в качестве аргумента передается элемент, относительно которого должно быть показано меню. При этом меню будет прикреплено к нижней границе данного элемента, а при показе будет выезжать вверх. Минимальный пример с таким меню может быть таким:

Page {
    id: page
    SilicaFlickable {
        id: flickab
        anchors.fill: parent
        contentHeight: column.height

        Column {
            id: column

            width: page.width
            spacing: Theme.paddingLarge
            PageHeader {
                title: qsTr("Моё приложение")
            }
            Button {
                id: button
                text: "Нажми меня"
                width: page.width
                onClicked: contextMenu.show(label)
            }
            Label {
                id: label
                height: page.height / 2
                text: "Просто текст"
                verticalAlignment: Text.AlignBottom
            }
        }
        ContextMenu {
            id: contextMenu
                MenuLabel {
                    text: "Контекстное меню"
                }
                MenuItem {
                    text: qsTr("Пункт меню 1")
                    onClicked: console.log("Нажат первый пункт меню")
                }
        }
    }
}

Выглядит такая страница вот так:



А при нажатии на кнопку из нижнего края надписи выезжает меню:



Здесь нужно отметить, что высота надписи не случайно была сделана такой большой (в половину страницы). Дело в том, что контекстное меню показывается именно внутри компонента, указанного в методе show(), и если меню окажется больше этого элемента, то оно будет попросту обрезано. Для демонстрации данной особенности можно в примере выше у надписи сделать обычную высоту, а между кнопкой и надписью поместить белый прямоугольник:

Button {
    id: button
    text: "Нажми меня"
    width: page.width
    onClicked: contextMenu.show(label)
}
Rectangle {
    color: "white"
    height: page.height / 2
    width: parent.width
}
Label {
    id: label
    text: "Просто текст"
}

Тогда страница будет выглядеть следующим образом:



А меню при открытии обрежется:



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

Label {
    id: label
    text: "Просто текст"
    height: contentHeight + (contextMenu.visible ? contextMenu.height : 0)
}

Тогда сама страница будет выглядеть так:

А при нажатии на кнопку меню визуально будет выезжать снизу надписи:


Закрыть контекстное меню можно с помощью метода hide(), а узнать открыто оно или нет с помощью свойства active. Кроме этого, компонент ContextMenu так же содержит свойство closeOnActivation, с помощью которого можно установить, должно ли закрываться меню при выборе какого-либо из его пунктов. А свойство hasContent поможет узнать, есть ли у меню какое-либо содержимое. Данное свойство используется и самой системой: если значение hasContent равно false, то меню не будет показано, даже при вызове метода show().

Наконец ContextMenu содержит обработчик сигнала onActivated(), который вызывается каждый раз, когда был выбран какой-либо пункт меню. Аргументом обработчика является индекс выбранного пункта меню.

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

Автор: Денис Лаурэ

UPD: Текст статьи был обновлен, были учтены все присланные замечания. Спасибо всем, кто прислал комментарии и замечания.
Поделиться публикацией

Комментарии 3

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

    А не логичней было бы сделать динамическое изменение размера элемента в зависимости от активности контекстного меню? Зачем показывать неочень красиво выглядещий пример?
      0
      Можно было бы сделать и динамически, но на мой взгляд, приведенный пример более наглядно и ярко демонстрирует данную особенность контекстного меню.
        +1
        Прислали еще комментариев по данному примеру, видимо он и вправду был выбран неудачно. Обновил статью, добавив более «реальный» пример.

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

      Самое читаемое