Evgenii Legotckoi
28 ноября 2015 г. 21:06

QML - Урок 019. Navigation Drawer в Qt Qml Android

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

  1. import QtQuick 2.5
  2. import QtQuick.Controls 1.4
  3. import QtQuick.Window 2.0
  4. import QtQuick.Layouts 1.1
  5.  
  6. ApplicationWindow {
  7. visible: true
  8. width: 700
  9. height: 480
  10. title: qsTr("Hello World")
  11.  
  12. // Пересчёт независимых от плотности пикселей в физические пиксели устройства
  13. readonly property int dpi: Screen.pixelDensity * 25.4
  14. function dp(x){ return (dpi < 120) ? x : x*(dpi/160); }
  15.  
  16. // Application Bar
  17. Rectangle {
  18. id: menuRect
  19. anchors.top: parent.top
  20. anchors.left: parent.left
  21. anchors.right: parent.right
  22. height: dp(48)
  23. color: "#4cd964"
  24.  
  25. // Иконка-Гамбургер
  26. Rectangle {
  27. anchors.top: parent.top
  28. anchors.bottom: parent.bottom
  29. anchors.left: parent.left
  30.  
  31. width: dp(48)
  32. color: "#4cd964"
  33.  
  34. Rectangle {
  35. anchors.top: parent.top
  36. anchors.topMargin: dp(16)
  37. anchors.left: parent.left
  38. anchors.leftMargin: dp(14)
  39. width: dp(20)
  40. height: dp(2)
  41. }
  42.  
  43. Rectangle {
  44. anchors.top: parent.top
  45. anchors.topMargin: dp(23)
  46. anchors.left: parent.left
  47. anchors.leftMargin: dp(14)
  48. width: dp(20)
  49. height: dp(2)
  50. }
  51.  
  52. Rectangle {
  53. anchors.top: parent.top
  54. anchors.topMargin: dp(30)
  55. anchors.left: parent.left
  56. anchors.leftMargin: dp(14)
  57. width: dp(20)
  58. height: dp(2)
  59. }
  60.  
  61. MouseArea {
  62. anchors.fill: parent
  63.  
  64. onClicked: {
  65. nav.toggle()
  66. }
  67. }
  68. }
  69.  
  70. }
  71.  
  72. // Loader для смены Фрагментов
  73. Loader {
  74. id: loader
  75. anchors.top: menuRect.bottom
  76. anchors.left: parent.left
  77. anchors.right: parent.right
  78. anchors.bottom: parent.bottom
  79. source: "Fragment1.qml"
  80.  
  81. // Функция для смены содержимого Loader
  82. function loadFragment(index){
  83.  
  84. switch(index){
  85. case 0:
  86. loader.source = "Fragment1.qml"
  87. break;
  88. case 1:
  89. loader.source = "Fragment2.qml"
  90. break;
  91. case 2:
  92. loader.source = "Fragment3.qml"
  93. break;
  94. default:
  95. loader.source = "Fragment1.qml"
  96. break;
  97. }
  98. }
  99. }
  100.  
  101. NavigationDrawer {
  102. id: nav
  103. Rectangle {
  104. anchors.fill: parent
  105. color: "white"
  106.  
  107. // Список с пунктами меню
  108. ListView {
  109. anchors.fill: parent
  110.  
  111. delegate: Item {
  112. height: dp(48)
  113. anchors.left: parent.left
  114. anchors.right: parent.right
  115.  
  116. Rectangle {
  117. anchors.fill: parent
  118. anchors.margins: dp(5)
  119. color: "whitesmoke"
  120.  
  121. Text {
  122. text: fragment
  123. anchors.fill: parent
  124. font.pixelSize: dp(20)
  125.  
  126. renderType: Text.NativeRendering
  127. horizontalAlignment: Text.AlignHCenter
  128. verticalAlignment: Text.AlignVCenter
  129. }
  130.  
  131. MouseArea {
  132. anchors.fill: parent
  133.  
  134. // По нажатию на пункт меню заменяем компонент в Loader
  135. onClicked: {
  136. loader.loadFragment(index)
  137. }
  138. }
  139. }
  140. }
  141.  
  142. model: navModel
  143. }
  144. }
  145. }
  146.  
  147. // Модель данных для списка с пунктами меню
  148. ListModel {
  149. id: navModel
  150.  
  151. ListElement {fragment: "Fragment 1"}
  152. ListElement {fragment: "Fragment 2"}
  153. ListElement {fragment: "Fragment 3"}
  154. }
  155. }

Fragment1.qml и остальные фрагменты

Код Fragment1.qml и остальных фрагментов идентичен за исключением того, что у них у всех разный цвет фона.

  1. import QtQuick 2.5
  2.  
  3. Rectangle {
  4. anchors.fill: parent
  5. color: "green"
  6.  
  7. Text {
  8. text: "Fragment 1"
  9. color: "white"
  10. anchors.top: parent.top
  11. anchors.right: parent.right
  12. anchors.margins: dp(50)
  13. font.pixelSize: dp(30)
  14.  
  15. renderType: Text.NativeRendering
  16. }
  17.  
  18. }

NavigationDrawer.qml

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

  1. import QtQuick 2.5
  2. import QtQuick.Window 2.0
  3.  
  4. Rectangle {
  5. id: panel
  6.  
  7. // Пересчёт независимых от плотности пикселей в физические пиксели устройства
  8. readonly property int dpi: Screen.pixelDensity * 25.4
  9. function dp(x){ return (dpi < 120) ? x : x*(dpi/160); }
  10.  
  11. // Свойства Navigation Drawer
  12. property bool open: false // Состояние Navigation Drawer - Открыт/Закрыт
  13. property int position: Qt.LeftEdge // Положение Navigation Drawer - Слева/Справа
  14.  
  15. // Функции открытия и закрытия Navigation Drawer
  16. function show() { open = true; }
  17. function hide() { open = false; }
  18. function toggle() { open = open ? false : true; }
  19.  
  20. // Внутренние свойства Navigation Drawer
  21. readonly property bool _rightEdge: position === Qt.RightEdge
  22. readonly property int _closeX: _rightEdge ? _rootItem.width : - panel.width
  23. readonly property int _openX: _rightEdge ? _rootItem.width - width : 0
  24. readonly property int _minimumX: _rightEdge ? _rootItem.width - panel.width : -panel.width
  25. readonly property int _maximumX: _rightEdge ? _rootItem.width : 0
  26. readonly property int _pullThreshold: panel.width/2
  27. readonly property int _slideDuration: 260
  28. readonly property int _openMarginSize: dp(20)
  29.  
  30. property real _velocity: 0
  31. property real _oldMouseX: -1
  32.  
  33. property Item _rootItem: parent
  34.  
  35. on_RightEdgeChanged: _setupAnchors()
  36. onOpenChanged: completeSlideDirection()
  37.  
  38. width: (Screen.width > Screen.height) ? dp(320) : Screen.width - dp(56)
  39. height: parent.height
  40. x: _closeX
  41. z: 10
  42.  
  43. function _setupAnchors() {
  44. _rootItem = parent;
  45.  
  46. shadow.anchors.right = undefined;
  47. shadow.anchors.left = undefined;
  48.  
  49. mouse.anchors.left = undefined;
  50. mouse.anchors.right = undefined;
  51.  
  52. if (_rightEdge) {
  53. mouse.anchors.right = mouse.parent.right;
  54. shadow.anchors.right = panel.left;
  55. } else {
  56. mouse.anchors.left = mouse.parent.left;
  57. shadow.anchors.left = panel.right;
  58. }
  59.  
  60. slideAnimation.enabled = false;
  61. panel.x = _rightEdge ? _rootItem.width : - panel.width;
  62. slideAnimation.enabled = true;
  63. }
  64.  
  65. function completeSlideDirection() {
  66. if (open) {
  67. panel.x = _openX;
  68. } else {
  69. panel.x = _closeX;
  70. Qt.inputMethod.hide();
  71. }
  72. }
  73.  
  74. function handleRelease() {
  75. var velocityThreshold = dp(5)
  76. if ((_rightEdge && _velocity > velocityThreshold) ||
  77. (!_rightEdge && _velocity < -velocityThreshold)) {
  78. panel.open = false;
  79. completeSlideDirection()
  80. } else if ((_rightEdge && _velocity < -velocityThreshold) ||
  81. (!_rightEdge && _velocity > velocityThreshold)) {
  82. panel.open = true;
  83. completeSlideDirection()
  84. } else if ((_rightEdge && panel.x < _openX + _pullThreshold) ||
  85. (!_rightEdge && panel.x > _openX - _pullThreshold) ) {
  86. panel.open = true;
  87. panel.x = _openX;
  88. } else {
  89. panel.open = false;
  90. panel.x = _closeX;
  91. }
  92. }
  93.  
  94. function handleClick(mouse) {
  95. if ((_rightEdge && mouse.x < panel.x ) || mouse.x > panel.width) {
  96. open = false;
  97. }
  98. }
  99.  
  100. onPositionChanged: {
  101. if (!(position === Qt.RightEdge || position === Qt.LeftEdge )) {
  102. console.warn("SlidePanel: Unsupported position.")
  103. }
  104. }
  105.  
  106. Behavior on x {
  107. id: slideAnimation
  108. enabled: !mouse.drag.active
  109. NumberAnimation {
  110. duration: _slideDuration
  111. easing.type: Easing.OutCubic
  112. }
  113. }
  114.  
  115. NumberAnimation on x {
  116. id: holdAnimation
  117. to: _closeX + (_openMarginSize * (_rightEdge ? -1 : 1))
  118. running : false
  119. easing.type: Easing.OutCubic
  120. duration: 200
  121. }
  122.  
  123. MouseArea {
  124. id: mouse
  125. parent: _rootItem
  126.  
  127. y: _rootItem.y
  128. width: open ? _rootItem.width : _openMarginSize
  129. height: _rootItem.height
  130. onPressed: if (!open) holdAnimation.restart();
  131. onClicked: handleClick(mouse)
  132. drag.target: panel
  133. drag.minimumX: _minimumX
  134. drag.maximumX: _maximumX
  135. drag.axis: Qt.Horizontal
  136. drag.onActiveChanged: if (active) holdAnimation.stop()
  137. onReleased: handleRelease()
  138. z: open ? 1 : 0
  139. onMouseXChanged: {
  140. _velocity = (mouse.x - _oldMouseX);
  141. _oldMouseX = mouse.x;
  142. }
  143. }
  144.  
  145. Connections {
  146. target: _rootItem
  147. onWidthChanged: {
  148. slideAnimation.enabled = false
  149. panel.completeSlideDirection()
  150. slideAnimation.enabled = true
  151. }
  152. }
  153.  
  154. Rectangle {
  155. id: backgroundBlackout
  156. parent: _rootItem
  157. anchors.fill: parent
  158. opacity: 0.5 * Math.min(1, Math.abs(panel.x - _closeX) / _rootItem.width/2)
  159. color: "black"
  160. }
  161.  
  162. Item {
  163. id: shadow
  164. anchors.left: panel.right
  165. anchors.leftMargin: _rightEdge ? 0 : dp(10)
  166. height: parent.height
  167.  
  168. Rectangle {
  169. height: dp(10)
  170. width: panel.height
  171. rotation: 90
  172. opacity: Math.min(1, Math.abs(panel.x - _closeX)/ _openMarginSize)
  173. transformOrigin: Item.TopLeft
  174. gradient: Gradient{
  175. GradientStop { position: _rightEdge ? 1 : 0 ; color: "#00000000"}
  176. GradientStop { position: _rightEdge ? 0 : 1 ; color: "#2c000000"}
  177. }
  178. }
  179. }
  180. }

Итог

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

Ссылка на скачивание проекта в zip-архиве: QML Navigation Drawer

Видеоурок

Вам это нравится? Поделитесь в социальных сетях!

Константин Козлов
  • 17 февраля 2018 г. 13:50

Не работает, если запихать Navigation Drawer в элемент List View. Drawer срабатывает только для первого элемента в списке.

Константин Козлов
  • 17 февраля 2018 г. 19:11

Уже разобрался, где ошибка. В 126-й строке должно быть не

y: _rootItem.y
, а
y: 0
Кроме того, QML ругается на 135-ю строку. Думаю, там должно быть
drag.onActiveChanged: if (drag.active) holdAnimation.stop()
Evgenii Legotckoi
  • 18 февраля 2018 г. 18:23

Добрый день!

Вполне возможно, что есть некоторые несостыковки из-за версий компонентов QML.
А вообще, попробуйте Navigation Drawer из последних компонентов Qt. Дело в том, что этот урок несколько устарел, а Qt Company выпустила свои собственные компоненты, в составе которых есть и Navigation Drawer. Вы можете ознакомиться с этими компонентами в примере Qt Gallery, который присутствует в составе примеров в Qt Creator. Думаю, что это будет для ваших задач и целей лучше.
Константин Козлов
  • 18 февраля 2018 г. 19:28

Я считаю, что это всё же ошибка, т. к. координаты Item'а всегда задаются относительно родителя. Соответственно, чтобы MouseArea занимала по высоте весь родительский элемент, её координата Y должна быть равна 0, а не координате Y родительского элемента.


А по поводу Drawer'а, появившегося в Qt 5.6, я не хочу его использовать, т. к. желаю сохранить совместимость с Qt 5.5.
Evgenii Legotckoi
  • 18 февраля 2018 г. 20:00

Ясно, вопросы обратной совместимости.

Да, проверил, как-то криво работает, если не поправить 126-ю строку, в том случае, если Navigation Drawer требуется поместить в какой-то внутренний объект, хотя для меня сам по себе User Case странный. Получается, что вы допускаете случай, в котором Drawer может быть развёрнут лишь на часть окна приложения, иначе я не вижу смысла помещать его в какое-либо иное место, кроме самого окна приложения.
Константин Козлов
  • 18 февраля 2018 г. 20:29

Это как раз мой Use Case, потому-то я и задал тут вопрос.
Дело в том, что я использую Ваш Dawer не только для главного меню приложения, но и для контекстного меню элементов ListView.

Evgenii Legotckoi
  • 18 февраля 2018 г. 20:42

Да, теперь представляю, как то работает. Согласен, ваша правка определённо к месту здесь.

Комментарии

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