Анімовані числа на Android

Красивий і привабливий UI — це важливо. Тому для Android існує величезна кількість бібліотек для гарного відображення елементів дизайну. Часто в додатку потрібно показати поле з числом або який-небудь лічильник. Наприклад, лічильник кількості виділених елементів списку або суму витрат за місяць. Звичайно, така задача легко вирішується за допомогою звичайного TextView, але можна її вирішити елегантно і ще анімацію зміни числа додати:

На YouTube доступні Demo-відео.

В статті піде розповідь про те, як все це реалізувати.

Одна статична цифра

Для кожної з цифр є векторне зображення, наприклад, для 8 це res/drawable/viv_vd_pathmorph_digits_eight.xml:

<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="@dimen/viv_digit_size"
android:height="@dimen/viv_digit_size"
android:viewportHeight="1"
android:viewportWidth="1">

<group
android:translateX="@dimen/viv_digit_translateX"
android:translateY="@dimen/viv_digit_translateY">
<path
android:name="iconPath"
android:pathData="@string/viv_path_eight"
android:strokeColor="@color/viv_digit_color_default"
android:strokeWidth="@dimen/viv_digit_strokewidth"/>
</group>

</vector>

Крім цифр 0-9 також потрібні зображення знака “мінус” (viv_vd_pathmorph_digits_minus.xml) і пусте зображення (viv_vd_pathmorph_digits_nth.xml), яке буде символізувати зникаючий розряд числа під час анімації.
XML-файли зображень відрізняються тільки атрибутом android:pathData. Всі інші атрибути для зручності задаються через окремі ресурси і однакові для всіх векторних зображень.
Зображення цифр 0-9 були взяті тут.

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

Описані векторні зображення представляють собою статичні зображення. Для анімації необхідно додати анімовані векторні зображення (<animated-vector>). Наприклад, для анімації цифри 2 в цифру 5 додаємо файл res/drawable/viv_avd_pathmorph_digits_2_to_5.xml:

<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:drawable="@drawable/viv_vd_pathmorph_digits_zero">

 <target android:name="iconPath">
 <aapt:attr name="android:animation">
<objectAnimator
android:duration="@integer/viv_animation_duration"
android:propertyName="pathData"
android:valueFrom="@string/viv_path_two"
android:valueTo="@string/viv_path_five"
android:valueType="pathType"/>
</aapt:attr>
</target>

</animated-vector>

Тут ми для зручності задаємо тривалість анімації через окремий ресурс. Всього у нас є 12 статичних зображень (0 — 9 + “мінус” + “порожнеча”), кожне з них може бути анімоване в будь-який з решти. Виходить, для повноти потрібно 12 * 11 = 132 файлу анімації. Відрізнятися вони будуть тільки атрибутами android:valueFrom і android:valueTo, і створювати їх вручну — не варіант. Тому напишемо простий генератор:

Генератор файлів анімації

import java.io.File
import java.io.FileWriter

fun main(args: Array<String>) {
 val names = arrayOf(
 "zero", "one", "two", "three",
 "four", "five", "six", "seven",
 "eight", "nine", "nth", "minus"
)

 fun getLetter(i: Int) = when (i) {
 in 0..9 -> i.toString()
 10 -> "n"
 11 -> "m"
 else -> null!!
}

 val dirName = "viv_out"
File(dirName).mkdir()
 for (from in 0..11) {
 for (to in 0..11) {
 if (from == to) continue
 FileWriter(File(dirName, "viv_avd_pathmorph_digits_${getLetter(from)}_to_${getLetter(to)}.xml")).use {
it.write("""
<?xml version="1.0" encoding="utf-8"?>
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:drawable="@drawable/viv_vd_pathmorph_digits_zero">

 <target android:name="iconPath">
 <aapt:attr name="android:animation">
<objectAnimator
android:duration="@integer/viv_animation_duration"
android:propertyName="pathData"
android:valueFrom="@string/viv_path_${names[from]}"
android:valueTo="@string/viv_path_${names[to]}"
android:valueType="pathType"/>
</aapt:attr>
</target>

</animated-vector>

""".trimIndent())
}
}
}
}

Всі разом

Тепер потрібно зв’язати статичні векторні зображення та анімації переходів в одному файлі <animated-selector>, який, як і звичайний <selector>, відображає одне з зображень в залежності від поточного стану. Цей drawable-ресурс (res/drawable/viv_asl_pathmorph_digits.xml) містить оголошення станів зображення і переходів між ними.

Читайте також  Дизайн-система в Figma. Погляд на інтерфейс через компоненти

Стани задаються тегами <item> із зазначенням зображення і атрибуту стану (в даному випадку — app:viv_state_three), що визначає дане зображення. Кожний стан має id, яке використовується для визначення необхідної анімації переходу:

<item
android:id="@+id/three"
android:drawable="@drawable/viv_vd_pathmorph_digits_three"
 app:viv_state_three="true" />

Атрибути станів задаються у файлі res/values/attrs.xml:

<resources>
 <declare-styleable name="viv_DigitState">
 <attr name="viv_state_zero" format="boolean" />
 <attr name="viv_state_one" format="boolean" />
 <attr name="viv_state_two" format="boolean" />
 <attr name="viv_state_three" format="boolean" />
 <attr name="viv_state_four" format="boolean" />
 <attr name="viv_state_five" format="boolean" />
 <attr name="viv_state_six" format="boolean" />
 <attr name="viv_state_seven" format="boolean" />
 <attr name="viv_state_eight" format="boolean" />
 <attr name="viv_state_nine" format="boolean" />
 <attr name="viv_state_nth" format="boolean" />
 <attr name="viv_state_minus" format="boolean" />
</declare-styleable>
</resources>

Анімації переходів між станами задаються тегами <transition> із зазначенням <animated-vector>, що символізує перехід, а також id початкового і кінцевого стану:

<transition
android:drawable="@drawable/viv_avd_pathmorph_digits_6_to_2"
android:fromId="@id/six"
 android:toId="@id/two" />

Вміст res/drawable/viv_asl_pathmorph_digits.xml досить-таки однотипно, і для його створення також використовувався генератор. Цей drawable-ресурс складається з 12 станів і 132 переходів між ними.

CustomView

Тепер, коли у нас є drawable, що дозволяє відображати одну цифру і анімувати її зміну, потрібно створити VectorIntegerView, який буде містити число, що складається з декількох розрядів, і управляти анімаціями. В якості основи був обраний RecyclerView, так як кількість цифр у числі — величина змінна, а RecyclerView — це кращий Android спосіб відображати змінну кількість елементів (цифр) ряд. Крім того, RecyclerView дозволяє управляти анімаціями елементів через ItemAnimator.

DigitAdapter і DigitViewHolder

Почати необхідно з створення DigitViewHolder, що містить одну цифру. View такого DigitViewHolder буде складатися з одного ImageView, у якого android:src="@drawable/viv_asl_pathmorph_digits". Для відображення потрібної цифри в ImageView використовується метод mImageView.setImageState(state, true);. Масив стану state формується виходячи з відображуваної цифри з використанням атрибутів стану viv_DigitState, визначених вище.

Відображення потрібної цифри в `ImageView`

private static final int[] ATTRS = {
R. attr.viv_state_zero,
R. attr.viv_state_one,
R. attr.viv_state_two,
R. attr.viv_state_three,
R. attr.viv_state_four,
R. attr.viv_state_five,
R. attr.viv_state_six,
R. attr.viv_state_seven,
R. attr.viv_state_eight,
R. attr.viv_state_nine,
R. attr.viv_state_nth,
R. attr.viv_state_minus,
};

void setDigit(@IntRange(from = 0, to = VectorIntegerView.MAX_DIGIT) int digit) {
 int[] state = new int[ATTRS.length];

 for (int i = 0; i < ATTRS.length; i++) {
 if (i == digit) {
 state[i] = ATTRS[i];
 } else {
 state[i] = -ATTRS[i];
}
}

 mImageView.setImageState(state, true);
}

Адаптер DigitAdapter відповідає за створення DigitViewHolder і за відображення потрібної цифри в потрібному DigitViewHolder.

Для коректної анімації перетворення одного числа до іншого використовується DiffUtil. З його допомогою розряд десятків анимируется в розряд десятків, сотні — сотні, десятки мільйонів — в десятки мільйонів і так далі. Знак “мінус” завжди залишається сам собою та можуть з’являтися або зникати, перетворюючись в порожнє зображення (viv_vd_pathmorph_digits_nth.xml).

Читайте також  Лайфхак від дизайнера нетворкінгу. Як знайти ментора, отримати користь від заходу і створити собі ім'я

Для цього в DiffUtil.Callback в методі areItemsTheSame повертається true, тільки якщо порівнюються однакові розряди чисел. “Мінус” є особливим розрядом, і “мінус” з попереднього числа дорівнює “мінусу” з нового числа.

У методі areContentsTheSame порівнюються символи, що стоять на певних позиціях в попередньому і новому числах. Саму реалізацію можна побачити в DigitAdapter.

DigitItemAnimator

Анімація зміни числа, а саме, перетворення, поява і зникнення цифр, буде контролюватися спеціальним аніматором для RecyclerViewDigitItemAnimator. Для визначення тривалості анімацій використовується той же integer-ресурс, що і в <animated-vector>, описаних вище:

private final int animationDuration;

DigitItemAnimator(@NonNull Resources resources) {
 animationDuration = resources.getInteger(R. integer.viv_animation_duration);
}

@Override public long getMoveDuration() { return animationDuration; }

@Override public long getAddDuration() { return animationDuration; }

@Override public long getRemoveDuration() { return animationDuration; }

@Override public long getChangeDuration() { return animationDuration; }

Основна частина DigitItemAnimator — це перевизначення методів анімації. Анімація появи цифри (метод animateAdd) виконується як перехід від порожнього зображення до потрібної цифри або знаку “мінус”. Анімація зникнення (метод animateRemove) виконується як перехід від відображення цифри або знаку “мінус” до порожнього зображенню.

Для виконання анімації зміни цифри спочатку зберігається інформація про попередню відображувану цифру з допомогою перевизначення методу recordPreLayoutInformation. Після чого в методі animateChange виконується перехід від попередньої відображуваної цифри до нової.

RecyclerView.ItemAnimator вимагає, щоб при перевизначенні методів анімації обов’язково викликалися методи, що символізують закінчення анімації. Тому в кожному з методів animateAdd, animateRemove і animateChange присутній виклик відповідного методу з затримкою, рівною тривалості анімації. Наприклад, в методі animateAdd викликається метод dispatchAddFinished з затримкою, рівною @integer/viv_animation_duration:

@Override
public boolean animateAdd(final RecyclerView.ViewHolder holder) {
 final DigitAdapter.DigitViewHolder digitViewHolder = (DigitAdapter.DigitViewHolder) holder;
 int a = digitViewHolder.d;
digitViewHolder.setDigit(VectorIntegerView.DIGIT_NTH);
digitViewHolder.setDigit(a);
 holder.itemView.postDelayed(new Runnable() {
@Override
 public void run() {
dispatchAddFinished(holder);
}
 }, animationDuration);
 return false;
}

VectorIntegerView

Перед створенням CustomView потрібно визначити його xml-атрибути. Для цього додамо <declare-styleable> файл res/values/attrs.xml:

<declare-styleable name="VectorIntegerView">
 <attr name="viv_vector_integer" format="integer" />
 <attr name="viv_digit_color" format="color" />
</declare-styleable>

Створюваний VectorIntegerView буде мати 2 xml-атрибута для кастомізації:

Читайте також  Чому герої відеоігор майже ніколи не роздягаються

  • viv_vector_integer число, що відображається при створенні view (0 за замовчуванням).
  • viv_digit_color колір цифр (за замовчуванням чорний).

Інші параметри VectorIntegerView можуть бути змінені через заміщення ресурсів в додатку (як це зроблено в демо-додатку):

  • @integer/viv_animation_duration визначає тривалість анімації (400мс за замовчуванням).
  • @dimen/viv_digit_size визначає розмір однієї цифри (24dp за замовчуванням).
  • @dimen/viv_digit_translateX застосовується до всіх векторних зображень цифр, щоб вирівняти їх по горизонталі.
  • @dimen/viv_digit_translateY застосовується до всіх векторних зображень цифр, щоб вирівняти їх по вертикалі.
  • @dimen/viv_digit_strokewidth застосовується до всіх векторних зображень цифр.
  • @dimen/viv_digit_margin_horizontal застосовується до всіх view цифр (DigitViewHolder) (-3dp за замовчуванням). Це потрібно, щоб зробити пробіли між цифрами менше, так як векторні зображення цифр — квадратні.

Перевизначені ресурси будуть застосовані до всіх VectorIntegerView в додатку.

Всі ці параметри задаються через ресурси, так як зміна розмірів VectorDrawable або тривалості анімації AnimatedVectorDrawable через код неможливо.

Додавання VectorIntegerView в XML розмітки виглядає наступним чином:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<com.qwert2603.vector_integer_view.VectorIntegerView
android:id="@+id/vectorIntegerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:viv_digit_color="#ff8000"
 app:viv_vector_integer="14" />

</FrameLayout>

Згодом можна змінити коротке число в коді, передавши BigInteger:

final VectorIntegerView vectorIntegerView = findViewById(R. id.vectorIntegerView);
vectorIntegerView.setInteger(
vectorIntegerView.getInteger().add(BigInteger.ONE),
 /* animated = */ true
);

Заради зручності є метод для передачі числа типу long:

vectorIntegerView.setInteger(1918L, false);

Якщо в якості animated передано false, то для адаптера буде викликаний метод notifyDataSetChanged, і нове число буде відображено без анімацій.

При перестворенні VectorIntegerView відображене число зберігається з використанням методів onSaveInstanceState і onRestoreInstanceState.

Исходники

Вихідний код доступний на github (директорія library). Там же знаходиться demo додаток, що використовує VectorIntegerView (директорія app).

Також є демо-апк (minSdkVersion 21).

Степан Лютий

Обожнюю технології в сучасному світі. Хоча частенько і замислююся над тим, як далеко вони нас заведуть. Не те, щоб я прям і знаюся на ядрах, пікселях, коллайдерах і інших парсеках. Просто приходжу в захват від того, що може в творчому пориві вигадати людський розум.

You may also like...

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *