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