- PVSM.RU - https://www.pvsm.ru -
После поста [1] о подходе Apple к кодированию видео в JPEG, решил рассказать о своем подобном «велосипеде» под Android.
В своем мобильном проекте решили мы сделать превьюшки оружия не статической картинкой, а видео. Подразумевалось, что художники нарисуют красивые анимации, может даже в 3д, но что-то не сложилось и нам выдали простейшие зацикленные 1-1.5 секундные ролики в разрешении 256х256. В iOS версию они встроились замечательно, а вот в Android пришлось повоевать с MediaPlayer и SurfaceView, но все-равно получились некоторые «корявости» — содержимое SurfaceView не перемещалось вслед за родительским View, была заметная пауза при воспроизведении, и пр.
Разумным решением было бы разбить анимации на кадры и оформить в xml для AnimationDrawable, но для 15 видов оружия это значило бы мусорку из 5000+ кадров по 10-15 кб каждый. Потому была сделана своя реализация AnimationDrawable, работающая с sprite sheet и относительно быстрый метод конверсии видео в такой формат.
Карту спрайтов не мудрствуя лукаво решили делать горизонтальную, без файла описания. Это не идеально практично, но и для 1-2 секундных анимаций не критично.
Исходное видео разбивается на спрайты через ffmpeg:
ffmpeg -i gun.avi -f image2 gun-%02d.png
Если получается больше 32 кадров, то добавляется параметер -r 25 или -r 20, чтобы уменьшить fps. Ограничение в 32 кадра взято из максимального разумного размера картинки по горизонтали в 8192 пикселя. Это можно обойти более сложным расположением спрайтов, но для последовтельности на 1-1.5 секунды этого достаточно.
Получается вот такая россыпь файлов:
Для сборки sprite sheet я использую Zwoptex [2], но подойдет любой похожий инструмент, или даже самописный скрипт.
В примере с пистолетом получился png файл размером 257кб и разрешением 8192х256. После обрезки до 7424x256 и обработки через сайт TinyPNG [3] он уменьшился до 101кб без потери качества. При желании можно еще и сохранить его в JPG с небольшой потерей качества и уменьшить до 50-70кб. Для сравнения, оригинальное видео в .MP4 с высоким качеством занимает те же 100кб. Для более сложных анимаций PNG может получиться в 2-3 раза больше оригинального видео, что на самом деле тоже не критично.
В первоначальном варианте ставка была сделана на то, что Bitmap.createBitmap [4] создает не новую картинку, а подмножество существующей в соответствии с описанием:
Returns an immutable bitmap from the specified subset of the source bitmap.
Конструктор загружает картинку, разбивает ее на кадры и добавляет их в AnimationDrawable. Анимации в нашем случае хранятся в assets для получения доступа по имени, но код очень просто адаптируется и для работы с R.drawable.*
public class AssetAnimationDrawable extends AnimationDrawable {
public AssetAnimationDrawable(Context context, String asset, int frames,
int fps) throws IOException {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Config.RGB_565; // A.G.: use 16-bit mode without alpha for animations
this.bitmap = BitmapFactory.decodeStream(context.getResources()
.getAssets().open(asset), null, options);
int width = bitmap.getWidth() / frames;
int height = bitmap.getHeight();
int duration = 1000 / fps; // A.G.: note the little gap cause of integer division.
// i.e. duration would be 33 for 30fps, meaning 990ms for 30 frames.
for (int i = 0; i < frames; i++) {
Bitmap frame = Bitmap.createBitmap(bitmap, i * width, 0, width, height);
addFrame(new BitmapDrawable(frame), duration);
}
}
}
Используется класс, как обычный AnimationDrawable:
AnimationDrawable animation = new AssetAnimationDrawable(getContext(), "movies/gun.jpg", 28, 25);
animation.setOneShot(false);
previewImage.setImageDrawable(animation);
animation.start();
К сожалению, опыты показали, что создается не immutable ссылка на оригинал, а новое изображение для каждого кадра, потому это решение оказалось достаточно ресурсоемко, хотя и работает отлично для многих ситуаций.
Следующий вариант уже заметно сложнее и наследуется напрямую от Drawable. В конструкторе sprite sheet загружается в член класса, а в методе draw рисуется текущий кадр. Также класс реализует интерфейс Runnable по аналогии с оригинальным AnimationDrawable для анимации.
@Override
public void draw(Canvas canvas) {
canvas.drawBitmap(m_bitmap, m_frameRect, copyBounds(), m_bitmapPaint);
}
@Override
public void run() {
long tick = SystemClock.uptimeMillis();
if (tick - m_lastUpdate >= m_duration) {
m_frame = (int) (m_frame + (tick - m_lastUpdate) / m_duration)
% m_frames;
m_lastUpdate = tick; // TODO: time shift for incomplete frames
m_frameRect = new Rect(m_frame * m_width, 0, (m_frame + 1)
* m_width, m_height);
invalidateSelf();
}
scheduleSelf(this, tick + m_duration);
}
public void start() {
run();
}
public void stop() {
unscheduleSelf(this);
}
public void recycle() {
stop();
if (m_bitmap != null && !m_bitmap.isRecycled())
m_bitmap.recycle();
}
В методе run() выполняется расчет текущего кадра и постановка задачи в очередь. Точность у приведенного кода будет не идеальная, потому что не учитывается дробное время кадра (например, когда tick — m_lastUpdate будет на 1мс меньше, чем duration), но в нашей задаче это было не актуально, а желающие могут доработать класс своими силами.
Полный код на paste2: paste2.org/p/2240487 [5]
Хочу обратить внимание на метод recycle(), который очищает m_bitmap. В большинстве случаев он не нужен, но у нас можно быстро прокликать покупки в магазине, из-за чего создается несколько AssetAnimationDrawable и может закончиться память, потому при создании новой анимации мы очищаем ресурсы старой.
Конечно же, подход далеко не идеален и не подойдет для больших анимаций или существенно отличающихся кадров, но для нашей задачи он подошел отлично, без заметного увеличения проекта и визуальных багов.
Минусы:
Плюсы:
Если кому-то это будет интересно, могу отдельно рассказать об опыте интеграции небольших видео в GUI в MonoTouch для iOS проекта. Документации по Mono относительно мало, а подводных камней там достаточно.
Автор: Nomad1
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/15436
Ссылки в тексте:
[1] поста: http://habrahabr.ru/post/151637
[2] Zwoptex: http://zwopple.com/zwoptex/
[3] TinyPNG: http://tinypng.org/
[4] Bitmap.createBitmap: http://developer.android.com/reference/android/graphics/Bitmap.html#createBitmap(android.graphics.Bitmap, int, int, int, int)
[5] paste2.org/p/2240487: http://paste2.org/p/2240487
Нажмите здесь для печати.