Як створювати кастомні UI-елементи з анімацією в Android без тонни непотрібного коду
Всім привіт! Мене звати Андрій, і я пишу Android-додатки в компанії Genesis Media для наших медіапроектів в Африці. У цій статті розповім про те, як створювати кастомні анімовані Android view c використанням шейдеров і матриць перетворень.
Цей текст буде корисний як початківцям, так і досвідченим Android-розробників, які хочуть покращити свої навички створення кастомних UI-елементів.
Графічні Android-обмеження
Android надає набір UI-елементів: картки, floating action button, navigation menu і багато інших. Використовувати тільки стандартні елементи для створення програми — моветон. Плюс іноді дизайнер хоче бачити елементи не такими, якими вони представлені у системі.
Наприклад, у стандартного ProgressBar є обмежений набір параметрів для зміни: колір, ширина, висота та інші. Але якщо дизайнер хоче чогось особливого — анімований градієнт з блискітками і єдинорогами, треба переписувати весь код, тому що немає відповідних параметрів для кастомізації і неможливо нічого зробити в цьому view елементі. Тому ми створюємо свій кастомный елемент.
Нещодавно мені надійшла цікава задача: потрібно було створити щось на зразок кастомного ProgressBar. Варіантів виконання було два: кастомизировать нативний андроидовский ProgressBar або написати щось своє.
Якщо поглянути на діфку, швидко стає зрозуміло, що доведеться використовувати саме другий варіант (з базових елементів таке не збереш).
Подумавши пару хвилин і не знайшовши швидкого рішення у своїй голові, я звернувся до чужих напрацювань та ідей. Stack Overflow пропонував кілька варіантів (link 1 , link 2 ), вимагають створення безлічі нових файлів і коду в такій кількості, що це виглядало дуже костыльно. Наприклад, створити кілька XML-файлів, в кожному з яких визначити Shape з градієнтною всередині. Плюс створити ще один файл, який описує анімацію між цими файлами. Для нашого випадку не дуже підходить.
Коротше кажучи, я так і не зміг знайти відповідного рішення своєї задачі, тому придумав його самостійно. Ми створимо кастомный view-елемент, в якому самі на canvas отрисуем необхідне зображення, а потім анимируем його.
Реалізуємо кастомный UI-елемент
Створимо наш клас, успадкувавши від View. У методі onMeasure будемо визначати розміри нашого GradientProgressBar. Дуже зручно знати ширину і висоту GradientProgressBar, так як можна буде прив'язатися до цих розмірами креслення чого б то не було.
class GradientProgressBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) { private var parentWidth = 0f private var parentHeight = 0f override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) parentWidth = MeasureSpec.getSize(widthMeasureSpec).toFloat() parentHeight = MeasureSpec.getSize(heightMeasureSpec).toFloat() } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) } }
Спробуємо насамперед промалювати підкладку під наш ProgressBar. Вона статична і змінюватися не буде.
@ColorInt private val greyColor = -0x8F8F8F private val backgroundRect = RectF() private val paint = Paint() override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) parentWidth = MeasureSpec.getSize(widthMeasureSpec).toFloat() parentHeight = MeasureSpec.getSize(heightMeasureSpec).toFloat() backgroundRect.set(0f, 0f, parentWidth, parentHeight) } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) paint.style = Paint.Style.FILL paint.isAntiAlias = true paint.color = greyColor canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, paint) }
Відмінно. Тепер створимо шейдер, яким будемо замальовувати ProgressBar.
@ColorInt private val yellow = -0x006300 @ColorInt private val orange = -0x0021e9 private fun createShader(): Shader { return LinearGradient(0f, 0f, 300f, parentHeight, yellow, orange, Shader.TileMode.REPEAT) }
Шейдер описує зміна кольору в просторі. Від точки з координатами (0, 0) до точки (300f, parentHeight) колір буде лінійно змінюватися від yellow до orange. Але що буде далі, за межами зазначеного нами діапазону? Shader.TileMode.REPEAT — цей параметр говорить, що далі шейдер буде просто повторюватися. Нас це влаштовує.
Застосуємо шейдер до екземпляру класу Paint і намалюємо прямокутник до середини довжини нашого view-елемента поверх сірої підкладки.
private val progressRect = RectF() private val progressPaint = Paint() private val shader = createShader() override fun onDraw(canvas: Canvas) { super.onDraw(canvas) paint.style = Paint.Style.FILL paint.isAntiAlias = true progressPaint.isAntiAlias = true progressPaint.style = Paint.Style.FILL paint.color = greyColor canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, paint) progressPaint.shader = shader progressRect.set(0f, 0f, parentWidth/2f , parentHeight) canvas.drawRoundRect(progressRect, parentHeight/2f, parentHeight/2f, progressPaint) }
Але як тепер анімувати наш шейдер?
Рубрика «Шкідливі поради». Пересоздаем шейдер з новими параметрами:
LinearGradient(10f, 0f, 310f, parentHeight, yellow, orange, Shader.TileMode.REPEAT)
Тут шейдер зрушать на десять пікселів вправо порівняно з попереднім варіантом. По ідеї, можна було б анімувати шляхом перетворення шейдера, але garbage collector спасибі не скаже. Метод onDraw викликається 60 разів в секунду, а це означає, що 60 разів в секунду буде створюватися новий примірник шейдерів — це не дуже добре.
Тому заліземо в исходники класу Shader. Впадає в очі наявність поля і методи:
private Matrix mLocalMatrix; public void setLocalMatrix(@Nullable Matrix localM)
А значить, ми можемо змінювати наш вже створений екземпляр шейдера, не створюючи новий. Клас Matrix дозволяє задавати різні види перетворень: зсув, поворот, стиснення і т. д.
Заводимо ще пару полів:
private val transformMatrix = Matrix() private val rotationAngle = 30f private var matrixTransitionOffset = 10f override fun onDraw(canvas: Canvas) { super.onDraw(canvas) paint.style = Paint.Style.FILL paint.isAntiAlias = true progressPaint.isAntiAlias = true progressPaint.style = Paint.Style.FILL paint.color = greyColor canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, paint)
Задамо матриці зсув по осі Х:
transformMatrix.setTranslate(matrixTransitionOffset, 0f)
А заодно і поворот:
transformMatrix.postRotate(rotationAngle)
Застосовуємо матрицю до нашого шейдеру:
shader.setLocalMatrix(transformMatrix) progressPaint.shader = shader progressRect.set(0f, 0f, parentWidth/2f , parentHeight) canvas.drawRoundRect(progressRect, parentHeight/2f, parentHeight/2f, progressPaint) }
Для анімації зсуву треба змінювати значення поля matrixTransitionOffset. Змінювати будемо від нуля до довжини шейдерів (в нашому випадку це 300 пікселів). Так вдасться добитися ефекту постійно біжить градієнта.
private var transitionAnimator: ValueAnimator? = null private var matrixTransitionOffset = 0f set(value) { field = value postInvalidateOnAnimation() } override fun onAttachedToWindow() { super.onAttachedToWindow() transitionAnimator = ValueAnimator.ofFloat(0f, 300f).apply { addUpdateListener { matrixTransitionOffset = it.animatedValue as Float } duration = 500L repeatMode = ValueAnimator.RESTART repeatCount = ValueAnimator.INFINITE interpolator = LinearInterpolator() start() } } override fun onDetachedFromWindow() { transitionAnimator?.cancel() super.onDetachedFromWindow() }
Аналогічним чином не становить праці анімувати значення довжини ProgressBar. Та й будь-який інший параметр.
Разом
На вирішення таких завдань цим способом ви витратите максимум годину, а елемент займе не більш ніж 20 рядків коду. Ось повна реалізація . Мені здається, що подібним чином реалізований анімований placeholder в додатку Facebook.
Тепер ви можете створювати свої кастомні графічні елементи з допомогою класів Shader, Gradient і Matrix. Без них теж можна, але буде проблематично, довго і негарно.
Опубліковано: 27/03/20 @ 11:00
Розділ Різне
Рекомендуємо:
Перші кроки в NLP: розглядаємо Python-бібліотеку TensorFlow та нейронні мережі в реальному завданні
Як ІТ-спеціалісти працюють віддалено на карантині. Фотоогляд
13 способів професійного розвитку для менеджерів і не тільки
PM дайджест #24: як мітинги підвищують продуктивність команди, список питань для зустрічей 1:1
Strategi bermain Poker online secara benar