Evgenii Legotckoi
Evgenii Legotckoi28 ноября 2015 г. 10: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 определит, какой компонент необходимо загрузить.

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

Видеоурок

Рекомендуем хостинг TIMEWEB
Рекомендуем хостинг TIMEWEB
Стабильный хостинг, на котором располагается социальная сеть EVILEG. Для проектов на Django рекомендуем VDS хостинг.

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

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

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

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

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

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

Добрый день!

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

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


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

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

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

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

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

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

Комментарии

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

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:50баллов,
  • Очки рейтинга-4
m
  • molni99
  • 26 октября 2024 г. 8:37

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:80баллов,
  • Очки рейтинга4
m
  • molni99
  • 26 октября 2024 г. 8:29

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:20баллов,
  • Очки рейтинга-10
Последние комментарии
ИМ
Игорь Максимов22 ноября 2024 г. 19:51
Django - Урок 017. Кастомизированная страница авторизации на Django Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii Legotckoi31 октября 2024 г. 21:37
Django - Урок 064. Как написать расширение для Python Markdown Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZE19 октября 2024 г. 15:19
Читалка fb3-файлов на Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь Максимов5 октября 2024 г. 14:51
Django - Урок 064. Как написать расширение для Python Markdown Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas55 июля 2024 г. 18:02
QML - Урок 016. База данных SQLite и работа с ней в QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Сейчас обсуждают на форуме
Evgenii Legotckoi
Evgenii Legotckoi24 июня 2024 г. 22:11
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey115 ноября 2024 г. 14:04
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProject4 июня 2022 г. 10:49
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…
9
9Anonim25 октября 2024 г. 16:10
Машина тьюринга // Начальное состояние 0 0, ,<,1 // Переход в состояние 1 при пустом символе 0,0,>,0 // Остаемся в состоянии 0, двигаясь вправо при встрече 0 0,1,>…

Следите за нами в социальных сетях