Анимации в 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<Snowflake>
    
        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<Snowflake>
        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
    • Игры

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

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

    Недостатки:

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

    Все примеры можно посмотреть и изучить здесь.
    Поделиться публикацией
    Похожие публикации
    Ммм, длинные выходные!
    Самое время просмотреть заказы на Фрилансим.
    Мне повезёт!
    Реклама
    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.