Как же отрисовать огонь? Нам нужно придумать реалистичное движение пикселей, изменение цветов. На самом деле алгоритм очень прост и уже описан не раз. Давайте реализуем его в Android.
Базовая подготовка
Создадим новый пустой проект с единственным Activity. Создадим и добавим туда кастомное
FireView.<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="<http://schemas.android.com/apk/res/android>" xmlns:app="<http://schemas.android.com/apk/res-auto>" xmlns:tools="<http://schemas.android.com/tools>" android:layout_width=«match_parent» android:layout_height=«match_parent» tools:context=».MainActivity»> <com.otopba.fireview.FireView android:layout_width=«0dp» android:layout_height=«0dp» app:layout_constraintBottom_toBottomOf=«parent» app:layout_constraintEnd_toEndOf=«parent» app:layout_constraintStart_toStartOf=«parent» app:layout_constraintTop_toTopOf=«parent» /> </androidx.constraintlayout.widget.ConstraintLayout>
Алгоритм
В оригинальном алгоритме всего 37 значений температуры: самая горячая зона внизу экрана — это значение 36 (белый цвет), и чем выше, тем пламя холоднее и темнее — значения приближаются к 0 (чёрный цвет).
Чтобы языки пламени выглядели реалистично, нужно добавить случайные флуктуации пикселей по горизонтали и вертикали. Каждый пиксель вычисляется так: из строки ниже случайным образом выбираем один пиксель, затем охлаждаем на случайное значение и помещаем в случайную позицию в текущей строке. Отрисовываем пламя строка за строкой, затем сменяем кадр и всё повторяем.
Подготовка
Зададим исходную палитру в виде массива int’ов:
private companion object { private val firePalette = intArrayOf( -0xf8f8f9, -0xe0f8f9, -0xd0f0f9, -0xb8f0f9, -0xa8e8f9, -0x98e0f9, -0x88e0f9, -0x70d8f9, -0x60d0f9, -0x50c0f9, -0x40b8f9, -0x38b8f9, -0x20b0f9, -0x20a8f9, -0x20a8f9, -0x28a0f9, -0x28a0f9, -0x2898f1, -0x3090f1, -0x3088f1, -0x3080f1, -0x3078e9, -0x3878e9, -0x3870e9, -0x3868e1, -0x4060e1, -0x4060e1, -0x4058d9, -0x4058d9, -0x4050d1, -0x4850d1, -0x4848d1, -0x4848c9, -0x303091, -0x202061, -0x101039, -0x1, ) }
Создадим двумерный массив
temp, который будем наполнять индексами температур (от 0 до 36):private lateinit var temp: Array<IntArray>
Для начала нам нужно узнать размеры исходного
View. При изменении размеров View вызовем метод onSizeChanged, в его теле мы и начнем нашу работу:override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) }
После получения размера нам нужно инициализировать массив
temp: h строк (высота экрана) и w столбцов (ширина экрана).temp = Array(h) { IntArray(w) }
Заполним нижнюю строку, самую горячую. Для этого в каждый пиксель строки нужно записать значение, равное размерности палитры минус 1:
for (x in 0 until w) { temp[h - 1][x] = firePalette.size - 1 }
Отрисовка
Весь массив
temp заполнен нулями, за исключением последней строки. Как же нам это отобразить? Всё банально: нам нужен цикл, в котором мы будем идти построчно отрисовывать каждый пиксель.Для отрисовки во
View есть метод onDraw. В качестве аргумента он получает Canvas — «холст», на котором мы будем рисовать. У canvas'а есть множество разных методов отрисовки. Мы выберем canvas.drawPoint, чтобы рисовать каждый пиксель отдельно. Для этого нам нужно указать его координаты (x, y) и цвет (paint). Paint зададим полем класса и лишь будем менять у него цвет.for (y in temp.indices) { for (x in temp[y].indices) { val color = firePalette[temp[y][x]] paint.color = color canvas.drawPoint(x.toFloat(), y.toFloat(), paint) } }
Оптимизируем алгоритм
Если мы запустим такое приложение, то оно будет ооооооочень долго выводить первый кадр нашего
FireView. Дело в том, что сейчас огонь отрисовывается крайне неоптимальным способом. Проблема в огромном размере очереди задач на отрисовку. Лучше один раз отобразить тысячу пикселей, чем тысячу раз по одному пикселю.Давайте воспользуемся bitmap'ом — изображением, наполненным нужными пикселями и их цветами.
bitmap = createBitmap(w, h)
После создания
Bitmap его нужно заполнить. Оставим прежний цикл из onDraw, в нём будем использовать вызов bitmap.setPixel.for (y in temp.indices) { for (x in temp[y].indices) { val color = firePalette[temp[y][x]] paint.color = color bitmap.setPixel(x, y, color) } } canvas.drawBitmap(bitmap, 0f, 0f, paint)
Теперь программа выполняется гораздо быстрее, в нижней части появилась полоса белого цвета:

Делаем огонь
Напомню, что нам нужно случайным образом перемешивать и охлаждать температуры пикселей. Воспользуемся классом
Random для генерации случайных значений.У
Random вызываем метод nextInt, который сгенерирует случайные числа. В качестве аргумента он принимает количество генерируемых значений. Чтобы пиксели огня флуктуировали не только вверх, но и в стороны, зададим для х диапазон из четырёх значений от -1 до 2. А по y диапазон 0-6 будет всегда из положительных чисел, потому что огонь поднимается только вверх. Кроме того, по мере движения огонь охлаждается, поэтому добавим случайное изменение температуры от 0 до 2.Перепишем карту пикселей, исходя из новых случайных значений: к текущим значениям прибавляем случайные смещения. Не забудьте учитывать граничные случаи и проверять, не вышли ли пиксели за пределы экрана: минимальные и максимальные значения не должны быть меньше 0 и больше размерностей массива.
for (y in 0 until temp.size - 1) { for (x in temp[y].indices) { val dx = random.nextInt(3) - 1 val dy = random.nextInt(6) val dt = random.nextInt(2) val x1 = min(temp[y].size - 1, max(0, x + dx)) val y1 = min(temp.size - 1, y + dy) temp[y][x] = max(0, temp[y1][x1] - dt) } }
Запустим получившийся код:

Появились небольшие языки пламени, но жизни в этом огне нет. Дело в том, что Android не будет просто так перерисовывать экран и тратить ресурсы. Необходимо явным образом сообщить, что
View нужно перерисовать. Для этого внутри метода onDraw вызовем метод invalidate. Конечно, в этом случае будет выполняться бесконечная перерисовка, но поскольку у нас горит бесконечный огонь, такое решение допустимо.
Теперь языки пламени двигаются, но очень медленно. Попробуем уменьшить размерность нашего
bitmap'а: определим коэффициент масштабирования scale. Теперь будем использовать не исходные значения высоты и ширины, а поделённые на коэффициент масштабирования. Уменьшим таким образом площадь bitmap'а в четыре раза и запустим вновь.
Получилось маленькое окошечко, в котором горит огонь, причём гораздо быстрее, чем в предыдущем варианте. А чтобы растянуть пламя на весь экран, нужно масштабировать
Canvas.Можно ли ещё больше повысить скорость отрисовки? Да, если применить более оптимальный алгоритм или увеличить
scale.
