Одним из основных элементов управления в 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.
Да, теперь представляю, как то работает. Согласен, ваша правка определённо к месту здесь.