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

Привіт, Хабр.

Багато розробники, які цікавляться розробкою користувацьких інтерфейсів (та й просто користувачі 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.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.