Анимации в Android по полочкам (Часть 3. «Низкоуровневые» анимации)
Часть 1. Базовые анимации
Часть 2. Комплексные анимации
Часть 3. «Низкоуровневые» анимации
Часть 4. Анимации переходов
Часть 5. Библиотеки для работы с анимацией
Все методы рассмотренные в предыдущих частях хороши и удобны, однако если нам нужно анимировать большое количество объектов, они могут оказаться не подходящими. В данной части мы рассмотрим способы которые нам позволят работать с действительно большим количеством объектов и создавать программно сложные анимации.
Часть 3. «Низкоуровневые» анимации
1. Рисование на канвасе View
Первый способ который мы рассмотрим это рисование в методе onDraw
нашего объекта View
. Реализуется данный способ просто, достаточно переопределить onDraw
и в конце вызвать postInvalidateOnAnimation()
.
В данном примере наш drawable
будет перемещаться по оси x.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
x += resources.getDimension(R.dimen.speed)
drawable.setBounds(x, y, x + size, y + size)
drawable.draw(canvas)
postInvalidateOnAnimation()
}
Пример со снежинками выше будет занимать несколько больше кода, т.к. нам нужно хранить состояние каждой отдельной снежинки отдельно.
class SnowAnimation : View {
...
private lateinit var snowflakes: Array
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
snowflakes = Array(10, {
Snowflake(right - left, bottom - top,
context.getDrawable(R.drawable.snowflake),
resources.getDimension(R.dimen.max_snowflake_size),
resources.getDimension(R.dimen.max_snowflake_speed))
})
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
snowflakes.forEach {
it.update()
it.draw(canvas)
}
postInvalidateOnAnimation()
}
}
internal class Snowflake(private val containerWidth: Int,
private val containerHeight: Int,
private val drawable: Drawable,
private val maxSize: Float,
private val maxSpeed: Float) {
private var size: Double = 0.0
private var speed: Double = 0.0
private var x: Double = 0.0
private var y: Double = 0.0
init {
reset()
}
private fun reset() {
size = Math.random() * maxSize / 2 + maxSize / 2
speed = Math.random() * maxSpeed / 2 + maxSpeed / 2
y = -size;
x = Math.random() * containerWidth
}
fun update() {
y += speed
if (y > containerHeight) {
reset()
}
}
fun draw(canvas: Canvas?) {
if (canvas == null) {
return
}
drawable.setBounds(x.toInt(), y.toInt(), (x + size).toInt(), (y + size).toInt())
drawable.draw(canvas)
}
}
Применение:
- Случаи в которых легче нарисовать анимацию программно
Достоинства:
- Можно создавать анимации зависящие абсолютно от любых параметров
- Нет лишних затрат на объекты View
Недостатки:
- Расчёты анимации и отрисовка происходят в UI thread
2. Рисование на канвасе SurfaceView
Что если расчёт следующего шага анимации будет занимать значительное время? Мы всё ещё можем воспользоваться первым способом и вынести расчёты в отдельный поток. Но это всё равно не приведёт к 100% плавности в анимации т.к. UI thread может быть загружен ещё чем либо помимо нашей анимации.
Android позволяет отвязаться от основного цикла (main loop) отрисовки с помощью компонента SurfaceView
. А раз мы больше не привязаны к основному циклу, то нам придётся держать свой поток для расчётов и отрисовки. SurfaceView
предоставляет коллбэки в которых мы можем запустить и остановить наш поток. В потоке по окончанию расчётов мы будем отрисовывать нашу анимацию.
Реализация той же анимации снежинок будет выглядеть следующим образом:
class MySurfaceView : SurfaceView, SurfaceHolder.Callback {
...
private lateinit var drawThread: DrawThread;
init {
holder.addCallback(this)
}
override fun surfaceCreated(holder: SurfaceHolder) {
//Создаём поток при создании surface
drawThread = DrawThread(getHolder(), context, measuredWidth, measuredHeight)
drawThread.start()
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
var retry = true
//Прерываем поток при уничтожении surface
drawThread.cancel();
//Документация требует чтобы к моменту выхода из этой функции к канвасу, гарантированно, не было обращений. По этому мы дожидаемся завершения нашего потока прежде чем выйти из метода.
while (retry) {
try {
drawThread.join()
retry = false
} catch (e: InterruptedException) {
}
}
}
}
internal class DrawThread(private val surfaceHolder: SurfaceHolder, context: Context, width: Int, height: Int) : Thread() {
private var snowflakes: Array
private var cancelled: Boolean = false
init {
snowflakes = Array(10, {
Snowflake(width, height,
context.getDrawable(R.drawable.snowflake),
context.resources.getDimension(R.dimen.max_snowflake_size),
context.resources.getDimension(R.dimen.max_snowflake_speed))
})
}
override fun run() {
while (!cancelled) {
//Блокируем canvas на время отрисовки
var canvas: Canvas? = surfaceHolder.lockCanvas()
try {
//В отличие от onDraw в View канвас приходит уже с предыдущим состоянием, поэтому если мы не хотим следов от предыдущего кадра, нужно очистить всю поверхность.
canvas?.drawColor(Color.WHITE)
snowflakes.forEach {
it.update()
it.draw(canvas)
}
} finally {
if (canvas != null) {
//Разблокируем canvas после отрисовки
surfaceHolder.unlockCanvasAndPost(canvas)
}
}
}
}
fun cancel() {
cancelled = true
}
}
Применение:
- Случаи в которых легче нарисовать анимацию программно
- Игры
Достоинства:
- Можно создавать анимации зависящие абсолютно от любых параметров
- Нет лишних затрат на объекты View
Недостатки:
- Сложность имплементации
3. OpenGL
Точно также, как и на канвасе, мы можем рисовать используя OpenGL API. Если вы задумали что-либо сложнее чем куб на картинке, то стоит посмотреть в сторону какого-либо движка, например libgdx. К сожалению, даже базовый пример займёт здесь довольно много места, поэтому ограничимся только этим кратким превью.
Применение:
- Сложные эффекты
- 3D
- Игры
Достоинства:
- Высокая производительность и управление памятью, шейдеры
Недостатки:
- Сложность имплементации
Все примеры можно посмотреть и изучить здесь.