Android bitmap canvas drawbitmap

Практический опыт работы с Bitmap средствами Android


Не так давно по долгу службы я столкнулся с одной задачей: нужно было придумать и реализовать дизайн медиа-плеера для Android. И если продумать и организовать более или менее сносное размещение элементов управления и информации оказалось делом не хитрым, то чтобы привнести в дизайн какую-то изюминку, пришлось хорошенько подумать. К счастью, в запасе у меня был такой элемент, как картинка с обложкой альбома проигрываемой мелодии. Именно он должен был добавить красок всей картинке.
Однако, будучи просто выведенной среди кнопок и надписей, обложка выглядела бумажным стикером, наклеенным на экран. Я понял, что без обработки изображения здесь не обойтись.

Некоторые раздумья насчёт того, что можно было бы тут придумать увенчались решением сделать для изображения обложки эффект отражения и тени. Сразу оговорюсь, что практическая реализация отражения не является моей оригинальной идеей. Её я подсмотрел в найденной англоязычной статье. В настоящем посте я лишь хочу привести некоторое осмысление производимых над изображением манипуляций, свои дополнения к процессу обработки и отметить некоторые нюансы работы с Bitmap в Android.
Итак, на входе я имел Bitmap с картинкой. Для начала я создал пустой Bitmap размером с оригинальную картинку, который позже должен был стать той же обложкой, но с нанесённой на неё тенью.

Этот метод создаёт изменяемый (что важно) Bitmap.
Здесь обязательно нужно отметить, что при работе с Bitmap’ами необходимо внимательно следить за памятью. Придётся ловить исключения. И ещё один момент: изучение профайлером показало, что перед вызовом метода createBitmap() работает сборщик мусора. Учтите это, если в вашем приложении скорость работы критична.

Далее я создал холст и нанёс на него исходное изображение.

В этом месте отмечу, что всегда, как только Bitmap становится не нужен, его нужно уничтожать методом recycle(). Дело в том, что объект этого типа представляет собой всего лишь ссылку на память с самим изображением и выглядит для сборщика мусора очень маленьким (хотя на самом деле памяти занято много). Это может привести к тому, что память закончится в самый неподходящий момент.

После всей подготовки я нанёс на холст краску с тенью.

RadialGradient в моём случае представляет тень, падающую по полукругу из правого верхнего угла изображения в центр нижней грани. Ему нужно установить центр (может выходить за пределы картинки), радиус, последовательность цветов и расстояния от центра по радиусу для каждого цвета. Для тени использовалось изменение альфы в цветах на радиусе.
LinearGradient использовался для фэйда краёв картинки. Его применение очень похоже на RadialGradient. Нужно задать начало и конец линии, вдоль которой пойдёт градиент, цвета и их позиции на этой линии.

Наконец, я приступил к рисованию отражения. К этому моменту у меня уже был Bitmap с нанесёнными тенями gradBitmap. Опять надо было создавать холст, создавать пустое изображение (на этот раз на треть длиннее оригинального), помещать его на холст и наносить на верх него Bitmap с тенями.

После недолгих приготовлений начиналось самое интересное. Я создал матрицу, переворачивающую изображение снизу вверх. С её помощью создал Bitmap из трети исходного и нанёс его на холст под оригинальным изображением.

Кстати, краткое замечание: в классе Bitmap существует несколько методов createBitmap, и лишь один из них создаёт изменяемые Bitmap’ы, на которых можно рисовать. Остальные для рисования НА них не годятся.

И наконец, нанесение прозрачного градиента для придания эффекта отражения.

Краска наносится на ту часть рисунка, которая является отражением.

Всё. Я получил refCover — Bitmap, на котором изображена обложка альбома с тенью, сглаженными краями и отражением.

P.S. В данной статье для меня был важен не сам факт достижения визуальных эффектов, а способы их получения и нюансы, с ними связанные. Если для кого-то вещи, описанные здесь, очевидны — прекрасно. Всем остальным, я надеюсь, статья поможет в написании своих приложений под Android.

UPD: картинки ДО и ПОСЛЕ

Источник

Полный список

— читаем Bitmap
— выводим его на канву
— получаем информацию о нем

Начинаем тему Bitmap. Без нее в рисовании никуда, т.к. Bitmap – это объект, который хранит в себе изображение. Та же канва, с которой мы обычно работаем, это обертка, которая принимает команды от нас и рисует их на Bitmap, который мы видим в результате.

Мы рассмотрим все основные операции с Bitmap и обязательно разберем интересные материалы с официального сайта по этой теме.

В этом уроке начнем с основ. Посмотрим какие методы есть для создания Bitmap из файла, как вывести его на канву и какую инфу о себе может рассказать Bitmap.

Для получения изображения из файла используется фабрика BitmapFactory. У нее есть несколько decode* методов, которые принимают на вход массив байтов, путь к файлу, поток, файловый дескриптор или идентификатор ресурса. И на выходе мы получаем Bitmap.

Можно заметить, что все эти методы имеют также версии с использованием объекта BitmapFactory.Options. Это очень полезная штука, о ней мы отдельно еще поговорим в одном из следующих уроков.

Самые используемые из методов чтения это, конечно, следующие:

decodeFile(String pathName) – получить Bitmap из файла, указав его путь. Т.е. этим методом можем считать картинку с SD-карты. (getExternalStorageDirectory)

decodeResource(Resources res, int id) – получить Bitmap из drawable-ресурса, указав его ID. Этот метод вернет нам картинку из папок res/drawable нашего приложения.

Читайте также:  Космические мморпг для андроид

Project name: P1571_BitmapRead
Build Target: Android 4.4
Application name: BitmapRead
Package name: ru.startandroid.develop.p1571bitmapread
Create Activity: MainActivity

В конструкторе DrawView мы получаем Bitmap из drawable-ресурса ic_launcher. На вход методу decodeResource мы передали объект ресурсов и ID требуемого ресурса.

Далее в переменную info сформируем строку с инфой о картинке:
getWidth – ширина картинки в px
getHeight – высота картинки в px
getByteCount – число байт, которое занимает картинка (доступен только с API Level 12)
getRowBytes – число байт в одной строке картинки
getConfig – инфа о способе хранения данных о пикселах

И выводим эту инфу в лог.

Настраиваем матрицу, которая повернет картинку на 45 градусов, растянет картинку в два раза в ширину и в три раза в высоту, и переместит ее на 200 вправо и 50 вниз.

Создаем два Rect объекта. rectSrc со сторонами равными половине сторон картинки. Т.е. этот прямоугольник охватывает левую верхнюю четверть картинки. Эту часть мы будем брать для вывода на экран далее в примере. А выводить мы ее будем в прямоугольник rectDst, это просто произвольная область на экране.

В методе onDraw рисуем картинку на канве тремя разными версиями метода drawBitmap. В первом случае просто выводим картинку как есть в точке (50,50). Во втором применяем матрицу, в которой мы уже настроили поворот, трансформацию и перемещение. И третий вариант возьмет от картинки часть, входящую в область rectSrc (мы там задали левую верхнюю четверть) и нарисует ее на канве в области rectDst, применив необходимые трансформации и перемещения.

Слева-направо видим все три варианта вывода. В первом случае без изменений и в указанной точке. Во втором случае преобразования были описаны в матрице. В третьем случае мы отсекли от картинки часть и нарисовали ее в указанной области, канва сама при этом растянула изображение под размеры области.

Info: size = 48 x 48, bytes = 9216 (192), config = ARGB_8888

Размер картинки = 48 (ширина) на 48 (высота). У вас тут могут быть другие цифры, т.к. метод decodeResource учитывает density устройства и вытаскивает картинку из необходимой папки. В моем случае он взял ее из drawable-mdpi.

Далее мы вывели вес картинки в байтах — 9216, и кол-во байтов в одной строке — 192. Тут понятно, что вес картинки = кол-во байтов в строке * высоту = 192 * 48 = 9126.

А если мы разделим кол-во байтов строки на ширину, то получим сколько байтов занимает один пиксел: 192 / 48 = 4 байта.

Это же подтверждает и config = ARGB_8888. Это означает, что на каждый из 4-х ARGB-компонентов пиксела (альфа, красный, зеленый, голубой) выделяется по 8 бит (= 1 байт). Следовательно, пиксел будет весить 4 байта.

Кроме ARGB_8888 есть еще несколько конфигураций:

ALPHA_8 – пиксел содержит в себе инфу только о прозрачности, о цвете здесь инфы нет. Каждый пиксел требует 8 бит (1 байт).

ARGB_4444 — аналог ARGB_8888, только каждому ARGB-компоненту отведено не по 8, а по 4 бита. Соответственно пиксел весит 16 бит (2 байта). С API Level 13 эта конфигурация объявлена устаревшей.

RGB_565 – здесь нет инфы о прозрачности, а трем RGB-компонентам выделено, соответственно по 5,6 и 5 бит. Каждый пиксел будет весить 16 бит или 2 байта.

Все вышенаписанное — это достаточно важные вещи, которые надо понимать и учитывать в разработке. Если, например, ваше приложение работает с картинками, и вы точно знаете, что они будут без прозрачности, то лучше использовать RGB_565. Все ваши картинки в памяти займут в два раза меньше места, чем при использовании дефолтового ARGB_8888. При большом кол-ве картинок это существенная оптимизация!

На размер (а следовательно и вес) изображения также следует обращать внимание. Следите, чтобы ваши картинки в памяти не были размера больше, чем вам нужно. Приведу пример из практики. Как-то пришлось оптимизировать приложение, в котором был экран со списком юзеров с аватарками. Аватарки эти изначально грузились с сайта и кешировались на SD. Для списка использовался memory-кэш, но он переполнялся мгновенно и постоянно подчитывал инфу с SD.

Вскрытие показало, что с сайта картинки грузились в разрешении 200 х 200 и прямо так и сохранялись на SD. В memory-кэш они помещались в том же разрешении, занимая, соответственно, по 200 * 200 * 4 = 160 000 байт каждая! Т.е. 6 картинок в кэше и уже метр памяти занят. А список там на сотни позиций. Конечно, никакого кэша не хватит при скроллинге.

Глянули на layout строки списка. Каждый ImageView, который отображал аватарку, был размером всего 32х32 dp. Т.е. в случае mdpi нам требовалась картинка 32х32 пиксела. Т.е. 32 * 32 * 4 = 4096 байт. Получается, что вместо одной аватарки 200х200 в кэше свободно могли бы разместиться почти 40 аватарок 32х32.

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

В общем, старайтесь использовать минимально-требуемый вам формат и размер. А о том как выбрать формат, поменять размер и использовать кэши мы обязательно поговорим в следующих уроках.

На следующем уроке:

— создаем и меняем Bitmap
— разбираемся с density и mutable

Присоединяйтесь к нам в Telegram:

— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.

— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Kotlin, RxJava, Dagger, Тестирование

— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня

Читайте также:  Андроид для головного устройства камри 70

— новый чат Performance для обсуждения проблем производительности и для ваших пожеланий по содержанию курса по этой теме

Источник

Полный список

— создаем и меняем Bitmap
— разбираемся с density и mutable

В прошлом уроке мы научились читать Bitmap из файла. Сейчас рассмотрим способы его создания. Для этого есть несколько статических методов createBitmap. Все эти методы создания Bitmap можно разделить на три группы:

1) создание на основе другого Bitmap
2) создание из массива цветов
3) создание пустого Bitmap

Рассмотрим эти группы подробно.

Создание на основе другого Bitmap

Эти методы позволяют нам взять готовый Bitmap и скопировать его целиком или частично в новый bitmap. При этом поддерживаются преобразования с помощью матрицы.

В этой группе 4 метода.

параметры:
source – Bitmap-источник, от которого будем брать его часть
x,y – координаты точки, от которой будем отсчитывать часть
width, height – высота и ширина части
m – матрица для применения преобразований к взятой части
filter – если true, то будет включено сглаживание при scale- и rotate-преобразованиях матрицы (но это приведет к некоторой потере в производительности)

Этот метод возьмет от Bitmap-источника часть указанную координатами и размерами, применит к ней матрицу и фильтр и вернет нам как новый Bitmap. (Хотя, вовсе не обязательно это будет новый объект. Об этом подробнее в конце урока.)

Аналогичен методу 1, но без использования матрицы и фильтра. Т.е. просто возьмет указанную часть от Bitmap-источника.

Вызывает метод 2 с параметрами createBitmap(src, 0, 0, src.getWidth(), src.getHeight()). Т.е. указанная часть равна всему Bitmap.

В итоге мы получим тот же Bitmap, что и источник.

Вызывает метод 2 с параметрами createBitmap(src, 0, 0, src.getWidth(),src.getHeight(), m, filter). Т.е. указанная часть равна всему Bitmap. Но дополнительно применяется матрица m, где рассчитаны преобразования, чтобы новый Bitmap получился размерами dstWidth на dstHeight. Также, можно включить сглаживающий фильтр.

В итоге мы получим тот же Bitmap, что и источник, но он будет нужных нам размеров, заданных параметрами dstWidth и dstHeight.

Рассмотрим первый метод этой группы на примере.

Project name: P1581_BitmapCreate
Build Target: Android 4.4
Application name: BitmapCreate
Package name: ru.startandroid.develop.p1581bitmapcreate
Create Activity: MainActivity

В конструкторе DrawView создаем bitmapSource из стандартной иконки ic_launcher. Настраиваем матрицу на поворот и изменение размера. И создаем bitmap на основе bitmapSource. Берем кусок начиная с точки (0,0) размерами в половину ширины и половину высоты. Т.е. получается левая верхняя четверть изображения. Система возьмет эту часть, применит к ней матрицу и фильтр, и выдаст нам, как новый Bitmap.

В onDraw отображаем полученный Bitmap.

А так будет выглядеть полученный Bitmap если фильтр выключить

Создание из массива цветов

Эти методы позволяют нам создать Bitmap из готового массива цветов.

В этой группе 4 метода.

параметры:
display – объект DisplayMetrics, из которого Bitmap возьмет значение densityDpi (зачем он нужен, рассмотрим чуть позже)
colors – массив цветов, т.е. фактически массив пикселов из которых будет состоять созданный Bitmap
offset – отступ от начала colors при чтении его значений
stride – шаг, который будет использован для перемещения по массиву при смене строки Bitmap
width, height – размеры создаваемого Bitmap
config – конфигурация (используемый способ хранения данных о пикселах, мы их рассмотрели подробно на прошлом уроке)

Все параметры в целом понятны. Объясню немного подробнее про stride, насколько мне удалось его понять. Рассмотрим пример, где параметр offset будем считать равным 0, ширину bitmap = 100, stride = 150.

Система создает Bitmap и заполняет его цветами из массива построчно. Но элементы массива она берет не все подряд, а высчитывает индекс первого элемента для каждой новой строки по формуле:
индекс первого элемента для каждой строки = (номер строки – 1) * stride

Т.е для первой строки – индекс первого элемента будет (1-1)*150 = 0. И начиная от него будут взяты цвета для первой строки, т.е. элементы 7, всего 100 элементов, т.к. ширина = 100). Для второй строки индекс первого элемента будет (2-1)*150 = 150. И для второй строки буду взяты цвета 225. И т.д.

В итоге мы получаем Bitmap для которого сами указали значения цветов всех его пикселов.

Вызывает метод 1 с параметрами createBitmap(display, colors, 0, width, width, height, config). Т.е. offset = 0, а stride = ширине Bitmap (цвета для строк будут браться из массива последовательно, без пропусков при переходе на новую строку).

Аналогичен методу 1, но без использования display.

Аналогичен методу 2, но без использования display.

Рассмотрим второй метод этой группы на примере. Перепишем класс DrawView:

В конструкторе DrawView создаем массив цветов количеством 300 * 300 = 90 000 элементов. Число выбрано такое, т.к. картинку мы будем создавать с шириной и высотой 300. Соответственно и цветов (читай пикселов) нам надо будет 300 * 300.

Заполняем первую треть массива полупрозрачным красным цветом, вторую – зеленым, третью – синим.

Создаем bitmap, используя массив цветов и указав: ширину 300, высоту 300, конфигурацию — RGB_565.

Аналогично создаем bitmapAlpha, но конфигурацию укажем ARGB_8888.

В onDraw выводим оба Bitmap-а на канву.

Bitmap-ы получились в целом одинаковые, но первый проигнорил прозрачность красного цвета. Это произошло из-за того, что мы указали ему конфиг RGB_565. Он не поддерживает прозрачность и, при создании Bitmap, аlpha-компонент был отброшен. Сохранилась информация только о цвете. Зато этот Bitmap в два раза меньше весит в памяти.

Создание чистого Bitmap

Эти методы позволяют создать чистый Bitmap без каких-либо данных.

В этой группе 2 метода.

параметры:
display – объект DisplayMetrics, из которого Bitmap возьмет значение densityDpi (зачем он нужен, рассмотрим чуть позже)
width, height – размеры создаваемого Bitmap
config – конфигурация (используемый способ хранения данных о пикселах, мы их рассмотрели подробно на прошлом уроке)

Читайте также:  Навигатор для курьера андроид лучший

Создается чистый Bitmap с указанными характеристиками.

Аналогичен методу 1, но без использования display

Рассмотрим второй метод этой группы на примере. Перепишем класс DrawView:

В конструкторе DrawView создаем чистый Bitmap размером 100х100.

Далее попробуем сами в нем что-нить нарисовать.

Методом setPixel ставим в нем три пиксела (20,20), (70,50) и (30,80) в красный цвет.

Методом setPixels можем менять пикселы не по одному, а массивом. Этот метод аналогичен методам создания Bitmap из массива цветов. Первые три параметра – это массив, offset и stride. Затем идут координаты точки, с которой начнем закраску (40,40), затем размер закрашиваемой области (10,15).

Как вы помните, канва – это всего лишь оболочка для Bitmap. Соответственно и наш Bitmap мы можем обернуть в канву и что-нить на нем с помощью этой канвы нарисовать. В нашем примере оборачиваем свежесозданный Bitmap в канву и рисуем на нем синий круг.

В onDraw выводим результат на экран.

Видим все фигуры, которые мы выводили.

Также, Bitmap имеет методы:

getPixel – получить значение цвета определенного пиксела

getPixels – получить значения цветов набора пикселов

getGenerationId – получить generationId, который меняется каждый раз, когда меняется Bitmap. Т.е. с его помощью можно отслеживать, что Bitmap был изменен.

Density

У Bitmap есть метод setDensity, туда мы можем передать density, к которому должна относиться эта картинка. Зачем это нужно я не очень понимаю, т.к. у нас один экран и density всегда, вроде как, один и тот же. Но штука интересная, рассмотрим на примере как оно работает и на что влияет.

В манифест для Activity добавьте строку:

В конструкторе DrawView создаем три Bitmap размерами 100х100. В bitmapIcon читаем иконку ic_launcher (она размером 48х48 при mdpi). Далее с помощью канвы рисуем иконку на все три Bitmap-а, но с небольшими нюансами.

В первом случае просто рисуем.

Во втором случае рисуем, а затем для bitmap2 ставим density в xhigh.

В третьем случае для bitmap3 ставим density в xhigh, а затем рисуем иконку.

В onDraw выводим все три Bitmap-а и для наглядности рисуем рамки размером 100х100 там же.

У меня на эмуляторе экран с density = mdpi. Если у вас density другое, то и результат будет другим.

Разбираемся, почему так получилось.

С первым случаем все понятно. Нарисовали иконку 48х48 и вывели на канву Bitmap размером 100х100.

Во втором случае мы нарисовали иконку на Bitmap, затем поменяли у него density на xhdpi, а затем уже вывели его на канву. Умная канва при этом спросила у Bitmap каков его density (xhdpi) и сравнила со своим (mdpi). Увидев, что Bitmap имеет density в два раза больший (xhdpi = 2 * mdpi) чем нужно, канва просто уменьшила в два раза стороны Bitmap (т.е. он стал 50х50, это видно на скрине) при выводе и тем самым компенсировала разницу в density.

В третьем случае мы сначала поменяли density на xhdpi, затем обернули его в канву (чтобы отличать от основной канвы, которая в onDraw, назовем ее bm3-канва). Эта bm3-канва автоматически подхватила density = xhdpi от bitmap3. Затем мы рисуем на bitmap3 с помощью bm3-канвы иконку. При этом bm3-канва определила, что иконка имеет density = mdpi (mdpi, т.к. это density устройства и оно всем ставится по умолчанию). В итоге получается, что density иконки в два раза меньше, чем density канвы. И канва, чтобы выровнять density просто увеличивает размер иконки при выводе. Т.к. иконка становится размером 96х96, и занимает почти весь Bitmap, который 100х100. Далее bitmap3 выводится на канву и здесь повторяются все те же рассуждения, что были во втором случае и bitmap в итоге выводится уменьшенным в два раза.

Основная мысль тут такова: канва сравнивает density – свой и у Bitmap, который она собирается рисовать. И если они различны, то выполняется подгонка размеров.

Если убрать из манифеста опцию hardwareAccelerated, то основная канва перестанет подгонять размер Bitmap. Почему так происходит, я не знаю.

Mutable

Свойство mutable означает, что Bitmap может быть изменен (через обертку канвой, методом setPixel, и т.п.). Соответственно immutable означает, что Bitmap не может быть изменен, т.е. он readOnly.

Из рассмотренных нами трех групп методов, третья группа возвращает mutable Bitmap, вторая – immutable Bitmap. А первая группа зависит от параметров и Bitmap-источника:
— если источник immutable и новый Bitmap будет являться точной копией исходника, то мы получим на выходе просто Bitmap-исходник. Т.е. это будет тот же Bitmap объект, и даже не его копия. И он останется immutable.
— если же источник mutable или новый Bitmap чем-то отличен от исходника, то мы получаем новый mutable Bitmap объект

Для определения mutable состояния у Bitmap используется метод isMutable.

На следующем уроке:

— разбираемся с BitmapFactory.Options
— сохраняем Bitmap в файл

Присоединяйтесь к нам в Telegram:

— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.

— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Kotlin, RxJava, Dagger, Тестирование

— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня

— новый чат Performance для обсуждения проблем производительности и для ваших пожеланий по содержанию курса по этой теме

Источник

Оцените статью