Анимации в Android по полочкам (Часть 3. «Низкоуровневые» анимации)

Часть 1. Базовые анимации
Часть 2. Комплексные анимации
Часть 3. «Низкоуровневые» анимации
Часть 4. Анимации переходов
Часть 5. Библиотеки для работы с анимацией

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

Часть 3. «Низкоуровневые» анимации


1. Рисование на канвасе View


image

Первый способ который мы рассмотрим это рисование в методе 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


image
Точно также, как и на канвасе, мы можем рисовать используя OpenGL API. Если вы задумали что-либо сложнее чем куб на картинке, то стоит посмотреть в сторону какого-либо движка, например libgdx. К сожалению, даже базовый пример займёт здесь довольно много места, поэтому ограничимся только этим кратким превью.

Применение:

  • Сложные эффекты
  • 3D
  • Игры


Достоинства:

  • Высокая производительность и управление памятью, шейдеры


Недостатки:

  • Сложность имплементации


Все примеры можно посмотреть и изучить здесь.

© Habrahabr.ru