Одним из основных элементов управления в Android приложении является Navigation Drawer , но в QML нет готового компонента для этого элемента, зато парни из Cutehacks сделали свой Navigation Drawer , код этого компонента выложен на гитхабе. Я давно уже хотел запустить этот код на живом Android устройстве и вот руки наконец-то до него дотянулись.
Я подробно изучил этот код и немного его подправил, поскольку в той версии было некоторое несоответствие Material Design в том плане, что панелька раскрывалась на 78 процентов от ширины экрана вне зависимости от ориентации. А Material Design рекомендует в портретной ориентации раскрывать Navigation Drawer так, чтобы он не доходил до противоположного края на 56 dip в случае со смартфонами и на 64 dip в случае с планшетами, но сделаем хотя бы для смартфонов, а в ландшафтной ориентации был не более, чем 320 dip шириной. Что я и поправил, также выпилив малую часть часть ненужного на данный момент кода и немного переименовав переменные под себя.
Что касается величины dip , то есть пикселей независимых от плотности экрана устройства, то это уже вопрос правильного масштабирования элементов интерфейса .
Предлагаю Вашему вниманию пример использования данного Navigation Drawer для смены трёх фрагментов в объект Loader с помощью трёх пунктов меню, которые будут находиться в данном Navigation Drawer .
Структура проекта для работы с Navigation Drawer
Структура проект будет сходна со структурой проекта из статьи по изучению работы с компонентом Loader .
- QmlNavigationDrawer.pro - профайл проекта;
- main.cpp - основной файл исходных кодов приложения;
- main.qml - основной файл кодов qml;
- Fragment1.qml - первый фрагмент для замены в Loader;
- Fragment2.qml - второй фрагмент;
- Fragment3.qml - третий фрагмент.
- NavigationDrawer.qml - сам объект Navigation Drawer.
main.cpp создаётся по умолчанию и не подвергается изменениям, поэтому его листинг приводится не будет, также как и профайла проекта.
main.qml
В нашем приложении будет Application Bar , который является объектом типа Rectangle, и в него Мы поместим иконку-гамбургер, по нажатию на которую будем открывать и закрывать Navigation Drawer посредством функции toggle() , при этом он будет перекрывать Application Bar в соответствии с рекомендациями Material Design . А ниже Application Bar всё оставшееся пространство будет занимать компонент Loader, в котором мы и будем менять фрагменты.
Также поместим в коде объект Navigation Drawer , в который поместим ListView. В данном ListView будет располагаться список пунктов меню. По нажатию пункт меню будет вызывать функцию смены компонента в Loader, передавая в данную функцию свой индекс. И уже по индекс Loader определит, какой компонент необходимо загрузить.
- import QtQuick 2.5
- import QtQuick.Controls 1.4
- import QtQuick.Window 2.0
- import QtQuick.Layouts 1.1
- ApplicationWindow {
- visible: true
- width: 700
- height: 480
- title: qsTr("Hello World")
- // Пересчёт независимых от плотности пикселей в физические пиксели устройства
- readonly property int dpi: Screen.pixelDensity * 25.4
- function dp(x){ return (dpi < 120) ? x : x*(dpi/160); }
- // Application Bar
- Rectangle {
- id: menuRect
- anchors.top: parent.top
- anchors.left: parent.left
- anchors.right: parent.right
- height: dp(48)
- color: "#4cd964"
- // Иконка-Гамбургер
- Rectangle {
- anchors.top: parent.top
- anchors.bottom: parent.bottom
- anchors.left: parent.left
- width: dp(48)
- color: "#4cd964"
- Rectangle {
- anchors.top: parent.top
- anchors.topMargin: dp(16)
- anchors.left: parent.left
- anchors.leftMargin: dp(14)
- width: dp(20)
- height: dp(2)
- }
- Rectangle {
- anchors.top: parent.top
- anchors.topMargin: dp(23)
- anchors.left: parent.left
- anchors.leftMargin: dp(14)
- width: dp(20)
- height: dp(2)
- }
- Rectangle {
- anchors.top: parent.top
- anchors.topMargin: dp(30)
- anchors.left: parent.left
- anchors.leftMargin: dp(14)
- width: dp(20)
- height: dp(2)
- }
- MouseArea {
- anchors.fill: parent
- onClicked: {
- nav.toggle()
- }
- }
- }
- }
- // Loader для смены Фрагментов
- Loader {
- id: loader
- anchors.top: menuRect.bottom
- anchors.left: parent.left
- anchors.right: parent.right
- anchors.bottom: parent.bottom
- source: "Fragment1.qml"
- // Функция для смены содержимого Loader
- function loadFragment(index){
- switch(index){
- case 0:
- loader.source = "Fragment1.qml"
- break;
- case 1:
- loader.source = "Fragment2.qml"
- break;
- case 2:
- loader.source = "Fragment3.qml"
- break;
- default:
- loader.source = "Fragment1.qml"
- break;
- }
- }
- }
- NavigationDrawer {
- id: nav
- Rectangle {
- anchors.fill: parent
- color: "white"
- // Список с пунктами меню
- ListView {
- anchors.fill: parent
- delegate: Item {
- height: dp(48)
- anchors.left: parent.left
- anchors.right: parent.right
- Rectangle {
- anchors.fill: parent
- anchors.margins: dp(5)
- color: "whitesmoke"
- Text {
- text: fragment
- anchors.fill: parent
- font.pixelSize: dp(20)
- renderType: Text.NativeRendering
- horizontalAlignment: Text.AlignHCenter
- verticalAlignment: Text.AlignVCenter
- }
- MouseArea {
- anchors.fill: parent
- // По нажатию на пункт меню заменяем компонент в Loader
- onClicked: {
- loader.loadFragment(index)
- }
- }
- }
- }
- model: navModel
- }
- }
- }
- // Модель данных для списка с пунктами меню
- ListModel {
- id: navModel
- ListElement {fragment: "Fragment 1"}
- ListElement {fragment: "Fragment 2"}
- ListElement {fragment: "Fragment 3"}
- }
- }
Fragment1.qml и остальные фрагменты
Код Fragment1.qml и остальных фрагментов идентичен за исключением того, что у них у всех разный цвет фона.
- import QtQuick 2.5
- Rectangle {
- anchors.fill: parent
- color: "green"
- Text {
- text: "Fragment 1"
- color: "white"
- anchors.top: parent.top
- anchors.right: parent.right
- anchors.margins: dp(50)
- font.pixelSize: dp(30)
- renderType: Text.NativeRendering
- }
- }
NavigationDrawer.qml
А вот и код самого Navigation Drawer, в котором расположился наш список фрагментов. Данный код можно взять и сразу использовать, или основательно помедитировать над ним, чтобы досконально разобраться во всех нюансах его реализации и возможно улучшить его.
- import QtQuick 2.5
- import QtQuick.Window 2.0
- Rectangle {
- id: panel
- // Пересчёт независимых от плотности пикселей в физические пиксели устройства
- readonly property int dpi: Screen.pixelDensity * 25.4
- function dp(x){ return (dpi < 120) ? x : x*(dpi/160); }
- // Свойства Navigation Drawer
- property bool open: false // Состояние Navigation Drawer - Открыт/Закрыт
- property int position: Qt.LeftEdge // Положение Navigation Drawer - Слева/Справа
- // Функции открытия и закрытия Navigation Drawer
- function show() { open = true; }
- function hide() { open = false; }
- function toggle() { open = open ? false : true; }
- // Внутренние свойства Navigation Drawer
- readonly property bool _rightEdge: position === Qt.RightEdge
- readonly property int _closeX: _rightEdge ? _rootItem.width : - panel.width
- readonly property int _openX: _rightEdge ? _rootItem.width - width : 0
- readonly property int _minimumX: _rightEdge ? _rootItem.width - panel.width : -panel.width
- readonly property int _maximumX: _rightEdge ? _rootItem.width : 0
- readonly property int _pullThreshold: panel.width/2
- readonly property int _slideDuration: 260
- readonly property int _openMarginSize: dp(20)
- property real _velocity: 0
- property real _oldMouseX: -1
- property Item _rootItem: parent
- on_RightEdgeChanged: _setupAnchors()
- onOpenChanged: completeSlideDirection()
- width: (Screen.width > Screen.height) ? dp(320) : Screen.width - dp(56)
- height: parent.height
- x: _closeX
- z: 10
- function _setupAnchors() {
- _rootItem = parent;
- shadow.anchors.right = undefined;
- shadow.anchors.left = undefined;
- mouse.anchors.left = undefined;
- mouse.anchors.right = undefined;
- if (_rightEdge) {
- mouse.anchors.right = mouse.parent.right;
- shadow.anchors.right = panel.left;
- } else {
- mouse.anchors.left = mouse.parent.left;
- shadow.anchors.left = panel.right;
- }
- slideAnimation.enabled = false;
- panel.x = _rightEdge ? _rootItem.width : - panel.width;
- slideAnimation.enabled = true;
- }
- function completeSlideDirection() {
- if (open) {
- panel.x = _openX;
- } else {
- panel.x = _closeX;
- Qt.inputMethod.hide();
- }
- }
- function handleRelease() {
- var velocityThreshold = dp(5)
- if ((_rightEdge && _velocity > velocityThreshold) ||
- (!_rightEdge && _velocity < -velocityThreshold)) {
- panel.open = false;
- completeSlideDirection()
- } else if ((_rightEdge && _velocity < -velocityThreshold) ||
- (!_rightEdge && _velocity > velocityThreshold)) {
- panel.open = true;
- completeSlideDirection()
- } else if ((_rightEdge && panel.x < _openX + _pullThreshold) ||
- (!_rightEdge && panel.x > _openX - _pullThreshold) ) {
- panel.open = true;
- panel.x = _openX;
- } else {
- panel.open = false;
- panel.x = _closeX;
- }
- }
- function handleClick(mouse) {
- if ((_rightEdge && mouse.x < panel.x ) || mouse.x > panel.width) {
- open = false;
- }
- }
- onPositionChanged: {
- if (!(position === Qt.RightEdge || position === Qt.LeftEdge )) {
- console.warn("SlidePanel: Unsupported position.")
- }
- }
- Behavior on x {
- id: slideAnimation
- enabled: !mouse.drag.active
- NumberAnimation {
- duration: _slideDuration
- easing.type: Easing.OutCubic
- }
- }
- NumberAnimation on x {
- id: holdAnimation
- to: _closeX + (_openMarginSize * (_rightEdge ? -1 : 1))
- running : false
- easing.type: Easing.OutCubic
- duration: 200
- }
- MouseArea {
- id: mouse
- parent: _rootItem
- y: _rootItem.y
- width: open ? _rootItem.width : _openMarginSize
- height: _rootItem.height
- onPressed: if (!open) holdAnimation.restart();
- onClicked: handleClick(mouse)
- drag.target: panel
- drag.minimumX: _minimumX
- drag.maximumX: _maximumX
- drag.axis: Qt.Horizontal
- drag.onActiveChanged: if (active) holdAnimation.stop()
- onReleased: handleRelease()
- z: open ? 1 : 0
- onMouseXChanged: {
- _velocity = (mouse.x - _oldMouseX);
- _oldMouseX = mouse.x;
- }
- }
- Connections {
- target: _rootItem
- onWidthChanged: {
- slideAnimation.enabled = false
- panel.completeSlideDirection()
- slideAnimation.enabled = true
- }
- }
- Rectangle {
- id: backgroundBlackout
- parent: _rootItem
- anchors.fill: parent
- opacity: 0.5 * Math.min(1, Math.abs(panel.x - _closeX) / _rootItem.width/2)
- color: "black"
- }
- Item {
- id: shadow
- anchors.left: panel.right
- anchors.leftMargin: _rightEdge ? 0 : dp(10)
- height: parent.height
- Rectangle {
- height: dp(10)
- width: panel.height
- rotation: 90
- opacity: Math.min(1, Math.abs(panel.x - _closeX)/ _openMarginSize)
- transformOrigin: Item.TopLeft
- gradient: Gradient{
- GradientStop { position: _rightEdge ? 1 : 0 ; color: "#00000000"}
- GradientStop { position: _rightEdge ? 0 : 1 ; color: "#2c000000"}
- }
- }
- }
- }
Итог
В результате написанное приложение будет выглядеть так, как показано на ниже следующих рисунках. Также в видеоуроке показана демонстрация приложения как на Desktop , так и на Android устройстве.
Ссылка на скачивание проекта в zip-архиве: QML Navigation Drawer
Не работает, если запихать Navigation Drawer в элемент List View. Drawer срабатывает только для первого элемента в списке.
Уже разобрался, где ошибка. В 126-й строке должно быть не
, аКроме того, QML ругается на 135-ю строку. Думаю, там должно быть
Добрый день!
Я считаю, что это всё же ошибка, т. к. координаты Item'а всегда задаются относительно родителя. Соответственно, чтобы MouseArea занимала по высоте весь родительский элемент, её координата Y должна быть равна 0, а не координате Y родительского элемента.
Ясно, вопросы обратной совместимости.
Это как раз мой Use Case, потому-то я и задал тут вопрос.
Дело в том, что я использую Ваш Dawer не только для главного меню приложения, но и для контекстного меню элементов ListView.
Да, теперь представляю, как то работает. Согласен, ваша правка определённо к месту здесь.