Как стать автором
Обновить

QML: анимированная иконка-«бутерброд» в стиле Material Design за 20 минут

Время на прочтение6 мин
Количество просмотров38K
Привет, Хабр.

Многие разработчики, интересующиеся разработкой пользовательских интерфейсов (да и просто пользователи Android) уже успели ознакомиться с новой концепцией интерфейса Material Design, активно продвигаемой Google в рамках выпуска Android 5.0. Знакомясь с руководством по оформлению приложений и внимательно разглядев недавно обновившийся Google Play, я обратил внимание на один очень симпатичный компонент — иконку меню (в народе известную как hamburger icon), анимированно превращающуюся в иконку «назад», и решил реализовать такой компонент на Qt с использованием декларативного языка описания интерфейса QML.



В этой статье я расскажу, как реализовать такой компонент и с какими проблемами и сложностями можно столкнуться в процессе. Ссылка на полный исходный код в конце поста.

Примечания
Сразу приношу извинения за использование gif-анимации на главной странице, но без неё, к сожалению, становится совсем непонятно, о чём идёт речь.

Все скриншоты в статье будут приведены в масштабе 3:1 для удобства рассмотрения. Для понимания написанного требуются базовые представления о синтаксисе языка QML и поддерживаемых им возможностях.

Скачать дистрибутив библиотеки Qt и среды разработки Qt Creator для вашей ОС можно на официальном сайте Qt.

Кстати, поддерживаемый Хабром тег source не поддерживает подсветку для исходников на QML, что может слегка затруднять чтение кода. Спасает только то, что количество кода мы постараемся минимизировать, насколько это возможно.

Подготовка


Для начала запустим Qt Creator и создадим проект типа «Интерфейс Qt Quick» — в данном примере мы будем использовать чистый QML и не напишем ни строчки кода на C++.

Сразу же создадим отдельный QML-файл для нашего компонента, так как мы предполагаем его использовать в дальнейшем и в других проектах, и установим стандартные размеры иконки для AppBar из Material Design: 24×24.
MenuBackButton.qml
import QtQuick 2.2

Item {
  id: root
  width: 24
  height: 24
}

Подготовим корневой элемент проекта к отображению нашей иконки и зададим ему яркий фон в стиле Material Design:
main.qml
import QtQuick 2.2

Rectangle {
  width: 48
  height: 48
  color: "#9c27b0"

  MenuBackIcon {
    id: menuBackIcon
    anchors.centerIn: parent
  }
}


Структура компонента


Давайте внимательно посмотрим на наш компонент вблизи, чтобы разобраться, из чего он состоит и как всё это превратить в код.



Для начала мы можем сразу заметить, что у нашей иконки есть два основных состояния: «меню» и «назад». Все остальные состояния являются промежуточными частями анимации при переходе между этими двумя. Сразу описываем это в QML:
MenuBackButton.qml
...
  state: "menu"
  states: [
    State {
      name: "menu"
    },

    State {
      name: "back"
    }
  ]
...

Для удобства отладки добавим в корневой элемент main.qml небольшой фрагмент, позволяющий нам переключать состояния иконки щелчком мыши:
main.qml
...
  MouseArea {
    anchors.fill: parent
    onClicked: menuBackIcon.state = menuBackIcon.state === "menu" ? "back" : "menu"
  }
...


Состояние «меню»


Как несложно заметить, наша иконка состоит из трёх прямоугольников, которые в состоянии «меню» имеют одинаковый размер и располагаются друг над другом. Для получения элемента, пиксель-в-пиксель соответствующего оригинальному, я сделал несколько скриншотов приложения Google Play на своём смартфоне и подбирал координаты и размеры элементов, сравнивая получающийся результат со скриншотами.

Давайте добавим эти элементы в код:
MenuBackButton.qml
...
  Rectangle {
    id: bar1
    x: 2
    y: 5
    width: 20
    height: 2
  }

  Rectangle {
    id: bar2
    x: 2
    y: 10
    width:
    height:
  }

  Rectangle {
    id: bar3
    x: 2
    y: 15
    width: 20
    height: 2
  }
...

Теперь можно запустить получившееся приложение и убедиться, что иконка в этом состоянии выглядит ровно так, как нам нужно.


Из чего состоит анимация


Для того, чтобы перейти к описанию состояния «назад», попробуем ещё раз внимательно приглядеться к тому, как выглядит анимация. Если этого не сделать сейчас, нам с некоторой вероятностью придётся переделывать уже написанный код, чего, конечно же, делать не хотелось бы.

Переход между состояниями компонента состоит из двух параллельно выполняемых анимаций:
  • верхний и нижний прямоугольники уменьшаются в ширине, поворачиваются на 45° и смещаются к краю среднего прямоугольника, образуя «стрелку»
  • все эти элементы одновременно переворачиваются на 180°

Я, конечно же, сразу не рассмотрел и попытался сделать анимацию поворота каждого из элементов в отдельности. Кода не сохранил и воспроизводить его совершенно не хочется, потому что получилась страшная ерунда. Не повторяйте мою ошибку :)

На самом деле, при попиксельном рассмотрении выясняется, что средний прямоугольник также анимируется — у него немного уменьшается ширина.

Состояние «назад»


Теперь, когда мы знаем, на какие грабли нам наступать не стоит, давайте опишем состояние «назад». Как мы уже выяснили, оно будет состоять из иконки «вперёд», перевёрнутой на 180° вокруг своей оси.

Для того, чтобы реализовать это в коде, мы используем QML-элемент PropertyChanges, позволяющий указать, каким образом должны меняться свойства элемента при переходе к другому состоянию. Заменяем описание состояния «back» на следующий код:
MenuBackButton.qml
...
    State {
      name: "back"
      PropertyChanges { target: root; rotation: 180 }
      PropertyChanges { target: bar1; rotation: 45; width: 13; x: 9.5; y: 8 }
      PropertyChanges { target: bar2; width: 17; x: 3; y: 12 }
      PropertyChanges { target: bar3; rotation: -45; width: 13; x: 9.5; y: 16 }
    }
...

Обратите внимание, что установленное для корневого элемента свойство rotation действует также на все его дочерние элементы. Запускаем, щёлкаем мышкой по элементу… Ура! :)


При использовании данных координат всё выглядит отлично, но если сместить какие-то из элементов на полпикселя или пиксель в сторону, повёрнутые на угол, отличный от кратного 90°, прямоугольники могут начать выглядеть, прямо скажем, странно. Дело в том, что свойство antialiasing элемента Rectangle по умолчанию включено только у прямоугольников со скруглёнными углами. Поэтому для получения более гладко выглядящей анимации и избежания разного рода проблем с отрисовкой, стоит к каждому из объявленных нами прямоугольников добавить установку свойства:
    antialiasing: true


Анимация перехода


Теперь, описав оба нужных нам состояния элемента, пришло время позаботиться об анимации перехода между ними. Для этого мы воспользуемся элементами QML Transition и PropertyAnimation.

Также, для большей гибкости в использовании, сразу объявим свойство animationDuration, которое позволит в дальнейшем менять длительность перехода, не влезая в код нашего элемента:
MenuBackButton.qml
...
  property int animationDuration: 350
...
  transitions: [
    Transition {
      PropertyAnimation { target: root; duration: animationDuration; easing.type: Easing.InOutQuad }
      PropertyAnimation { target: bar1; properties: "rotation, width, x, y";
                          duration: animationDuration; easing.type: Easing.InOutQuad }
      PropertyAnimation { target: bar2; properties: "rotation, width, x, y";
                          duration: animationDuration; easing.type: Easing.InOutQuad }
      PropertyAnimation { target: bar3; properties: "rotation, width, x, y";
                          duration: animationDuration; easing.type: Easing.InOutQuad }
    }
  ]
...


В элементе Transition обычно указываются свойства «to» и «from», задающие, на какую именно смену состояний этот переход должен действовать. Но так как состояний в нашем случае всего два и анимация перехода в обе стороны практически одинаковая, эти свойства можно не устанавливать.

Обратите внимание на свойство easing.type — этим свойством мы задаём кривую скорости анимации элементов. Дело в том, что анимация, которая выполняется с постоянной скоростью, выглядит обычно не слишком эстетично. Любое движение в реальном мире должно иметь период увеличения скорости при начале движения и период уменьшения скорости при его окончании. Собственно, на это же ссылается Google в документе по Material Design.

Проверяем:


Победа? Не совсем.


Мы почти закончили, но анимация вращения при переходе обратно в состояние «menu» работает не совсем так, как бы нам хотелось. В принципе, всё логично: угол поворота меняется в обратную сторону с 180° до 0°. Но это очень просто изменить:
...
      RotationAnimation { target: root; direction: RotationAnimation.Clockwise;
                          duration: animationDuration; easing.type: Easing.InOutQuad }
...

Элемент RotationAnimation предназначен специально для таких случаев и позволяет указывать, в каком именно направлении будет осуществляться поворот. При желании, мы можем добавить нашему элементу свойство, позволяющее задавать направление анимации из вызывающего кода.

Результат и ссылки


Данное руководство можно считать завершённым. Мы получили готовый компонент, пригодный к повторному использованию с минимальными доработками. Могу разве что предложить добавить свойство «color» для задания цвета элемента в случае использования его на светлом фоне.

Большим достоинством QML с моей точки зрения является как раз то, что код визуальных компонентов на нём во многих случаях получается легко читаемым и весьма компактным. Весь элемент у меня лично занял всего 60 строк кода и для него даже как-то неудобно создавать отдельный репозиторий на github, так что даю ссылку на gist.
Теги:
Хабы:
Всего голосов 56: ↑37 и ↓19+18
Комментарии19

Публикации

Истории

Работа

QT разработчик
9 вакансий

Ближайшие события

25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань