Освой Android играючи
/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000
Bitmap.Config
Получить Bitmap из ImageView
Изменение размеров - метод createScaledBitmap()
Кадрирование - метод createBitmap()
Меняем цвета каждого пикселя
Конвертируем Bitmap в байтовый массив и обратно
Сжимаем картинку
Как раскодировать Bitmap из Base64
Вычисляем средние значения цветов
Генерируем разноцветные квадраты с буквами программно
Вам часто придётся иметь дело с изображениями котов, которые хранятся в файлах JPG, PNG, GIF. По сути, любое изображение, которое мы загружаем из графического файла, является набором цветных точек (пикселей). А информацию о каждой точке можно сохранить в битах. Отсюда и название - карта битов или по-буржуйски - bitmap. У нас иногда используется термин растр или растровое изображение. В Android есть специальный класс android.graphics.Bitmap для работы с подобными картинками.
Существуют готовые растровые изображения в файлах, о которых поговорим ниже. А чтобы создать с нуля объект Bitmap программным способом, нужно вызвать метод createBitmap():
Bitmap bitmap = Bitmap.createBitmap(100, 100,
Bitmap.Config.ARGB_8888);
В результате получится прямоугольник с заданными размерами в пикселях (первые два параметра). Третий параметр отвечает за информацию о прозрачности и качестве цвета (в конце статьи есть примеры).
Очень часто нужно знать размеры изображения. Чтобы узнать его ширину и высоту в пикселах, используйте соответствующие методы:
int width = bitmap.getWidth();
int height = bitmap.getHeight();
Кроме размеров, желательно знать цветовую схему. У класса Bitmap есть метод getConfig(), который возвращает перечисление Bitmap.Config.
Всего существует несколько элементов перечисления.
Конфигурация RGB_565 была очень популярна на старых устройствах. С увеличением памяти и мощности процессоров данная конфигурация теряет актуальность.
В большинстве случаев вы можете использовать ARGB_8888.
Получив объект в своё распоряжение, вы можете управлять каждой его точкой. Например, закрасить его синим цветом.
public void onClick(View view) {
ImageView imageView = findViewById(R.id.imageView);
Bitmap bitmap = Bitmap.createBitmap(100, 100,
Bitmap.Config.ARGB_8888);
// Закрашиваем синим цветом
bitmap.eraseColor(Color.BLUE);
// Выведем результат в ImageView для просмотра
imageView.setImageBitmap(bitmap);
}
Чтобы закрасить отдельную точку, используйте метод setPixel() (парный ему метод getPixel позволит узнать информацию о точке). Закрасим красной точкой центр синего прямоугольника из предыдущего примера - имитация следа от лазерной указки. Котам понравится.
bitmap.eraseColor(Color.BLUE);
bitmap.setPixel(50, 50, Color.RED);
В нашем случае мы создали растровое изображение самостоятельно и можем на него воздействовать. Но если вы загрузите готовое изображение из файла и попытаетесь добавить к нему красную точку, то можете получить крах программы. Изображение может быть неизменяемым, что-то типа "Только для чтения", помните об этом.
Созданный нами цветной прямоугольник и управление отдельными точками не позволят вам нарисовать фигуру, не говоря уже о полноценном рисунке. Класс Bitmap не имеет своих методов для рисования, для этого есть метод Canvas (Холст), на котором вы можете размещать объекты Bitmap.
Когда вы размещали в разметке активности компонент ImageView и присваивали атрибуту android:src ресурс из папок drawable-xxx, то система автоматически выводила изображение на экран.
Если нужно программно получить доступ к битовой карте (изображению) из ресурса, то используется такой код:
// Конвертируем Drawable в Bitmap
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.catpic);
int mPhotoWidth = bitmap.getWidth();
int mPhotoHeight = bitmap.getHeight();
// присваиваем ImageView
imageView.setImageBitmap(bitmap);
Обратный процес конвертации из Bitmap в Drawable:
Drawable drawable = new BitmapDrawable(bitmap); // устаревший конструктор Drawable drawable = new BitmapDrawable(getResources(), bitmap); // новый конструктор
Изображение можно сохранить, например, на SD-карту в виде файла (кусок кода):
try {
FileOutputStream fos = new FileOutputStream(dirname + "cat.jpg");
bitmap.compress(CompressFormat.JPEG, 75, fos);
fos.flush();
fos.close();
} catch (Exception e) {
Log.e("MyLog", e.toString());
}
Каждая точка изображения представлена в виде 4-байтного целого числа. Сначала идёт байт прозрачности - значение 0 соответствует полной прозрачности, а 255 говорит о полной непрозрачности. Промежуточные значения позволяют делать полупрозрачные изображения. Этим искусством в совершенстве владел чеширский кот, который умело управлял всеми точками своего тела и растворялся в пространстве, только улыбка кота долго ещё висела в воздухе (что-то я отвлёкся).
Следующие три байта отвечают за красный, зелёный и синий цвет, которые работают по такому же принципу. Т.е. значение 255 соответствует насыщенному красному цвету и т.д.
Так как любое изображение кота - это набор точек, то с помощью метода getPixels() мы можем получить массив этих точек, сделать с этой точкой что-нибудь нехорошее (поменять прозрачность или цвет), а потом с помощью родственного метода setPixels() записать новые данные обратно в изображение. Так можно перекрасить чёрного кота в белого и наоборот. Если вам нужна конкретная точка на изображении, то используйте методы getPixel()/setPixel(). Подобный подход используется во многих графических фильтрах. Учтите, что операция по замене каждой точки в большом изображении занимает много времени. Желательно проводить подобные операции в отдельном потоке.
На этом базовая часть знакомства с битовой картой закончена. Теперь подробнее.
Учитывая ограниченные возможности памяти у мобильных устройств, следует быть осторожным при использовании объекта Bitmap во избежание утечки памяти. Не забывайте освобождать ресурсы при помощи метода recycle(), если вы в них не нуждаетесь. Например:
Bitmap image = ..... (ваш код)
...
if(image != null) {
image.recycle();
image = null;
}
Почему это важно? Если не задумываться о ресурсах памяти, то можете получить ошибку OutOfMemoryError. На каждое приложение выделяется ограниченное количество памяти (heap size), разное в зависимости от устройства. Например, 16мб, 24мб и выше. Современные устройства как правило имеют 24мб и выше, однако это не так много, если ваше приложение злоупотребляет графическими файлами.
Bitmap на каждый пиксель тратит в общем случае 2 или 4 байта (зависит от битности изображения – 16 бит RGB_555 или 32 бита ARGB_888). Можно посчитать, сколько тратится ресурсов на Bitmap, содержащий изображение, снятое на 5-мегапиксельную камеру.
При соотношении сторон 4:3 получится изображение со сторонами 2583 х 1936. В конфигурации RGB_555 объект Bitmap займёт 2592 * 1936 * 2 = около 10Мб, а в ARGB_888 (режим по умолчанию) в 2 раза больше – чуть более 19Мб.
Во избежание проблем с памятью прибегают к помощи методов decodeXXX() класса BitmapFactory.
Если установить атрибут largeHeap в манифесте, то приложению будет выделен дополнительный блок памяти.
Ещё одна потенциальная проблема. У вас есть Bitmap и присвоили данный объект кому-то. Затем объект был удалён из памяти, а ссылка на него осталась. Получите крах приложения с ошибкой типа "Exception on Bitmap, throwIfRecycled".
Возможно, лучше сделать копию.
Bitmap src = // Ваш источник
Bitmap newBitmap = src.copy(src.getConfig(), src.isMutable() ? true : false);
Если в ImageView имеется изображение, то получить Bitmap можно следующим образом:
Bitmap bitmap = ((BitmapDrawable)image.getDrawable()).getBitmap();
Но с этим способом нужно быть осторожным. Например, если в ImageView используются элементы LayerDrawable, то возникнет ошибка. Можно попробовать такой вариант.
Bitmap bitmap = ((BitmapDrawable)((LayerDrawable)image.getDrawable()).getDrawable(0)).getBitmap();
Более сложный вариант, но и более надёжный.
imageView.setDrawingCacheEnabled(true);
imageView.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
imageView.layout(0, 0,
imageView.getMeasuredWidth(), imageView.getMeasuredHeight());
imageView.buildDrawingCache(true);
Bitmap bitmap = Bitmap.createBitmap(imageView.getDrawingCache());
imageView.setDrawingCacheEnabled(false);
С помощью метода createScaledBitmap() можно изменить размер изображения.
Будем тренироваться на кошках. Добавим картинку в ресурсы (res/drawable). В разметку добавим два элемента ImageView
ImageView firstImageView = findViewById(R.id.imageView1);
ImageView secondImageView = findViewById(R.id.imageView2);
// Конвертируем Drawable в Bitmap и выводим в ImageView
Bitmap bmOriginal = BitmapFactory.decodeResource(getResources(),
R.drawable.catlove);
firstImageView.setImageBitmap(bmOriginal);
// Вычисляем ширину и высоту изображения
int width = bmOriginal.getWidth();
int height = bmOriginal.getHeight();
// Половинки
int halfWidth = width / 2;
int halfHeight = height / 2;
// Выводим уменьшенную в два раза картинку во втором ImageView
Bitmap bmHalf = Bitmap.createScaledBitmap(bmOriginal, halfWidth,
halfHeight, false);
secondImageView.setImageBitmap(bmHalf);
В последнем параметре у метода идёт булева переменная, отвечающая за сглаживание пикселей. Обычно его применяют, когда маленькое изображение увеличивают в размерах, чтобы улучшить качество картинки. При уменьшении, как правило, в этом нет такой необходимости.
Существует несколько перегруженных версий метода Bitmap.createBitmap(), с помощью которых можно скопировать участок изображения.
Описываемый ниже код не является оптимальным и очень ресурсоёмкий. На больших изображениях код будет сильно тормозить. Приводится для ознакомления. Чтобы вывести часть картинки, можно сначала создать нужный Bitmap с заданными размерами, занести в массив каждый пиксель исходного изображения, а затем этот же массив вернуть обратно. Но, так как мы уже задали другие размеры, то часть пикселей не выведутся.
ImageView image1 = findViewById(R.id.imageView1);
ImageView image2 = findViewById(R.id.imageView2);
// Конвертируем Drawable в Bitmap и выводим в ImageView
Bitmap bmOriginal = BitmapFactory.decodeResource(getResources(),
R.drawable.catlove);
image1.setImageBitmap(bmOriginal);
// Вычисляем ширину и высоту изображения
int width = bmOriginal.getWidth();
int height = bmOriginal.getHeight();
// Половинки
int halfWidth = width / 2;
int halfHeight = height / 2;
// Выводим верхнюю левую четвертинку картинки
Bitmap bmUpRightPartial = Bitmap.createBitmap(halfWidth, halfHeight,
Bitmap.Config.ARGB_8888);
int[] pixels = new int[halfWidth * halfHeight];
bmOriginal
.getPixels(pixels, 0, halfWidth, 0, 0, halfWidth, halfHeight);
bmUpRightPartial
.setPixels(pixels, 0, halfWidth, 0, 0, halfWidth, halfHeight);
image2.setImageBitmap(bmUpRightPartial);
По аналогии мы можем вывести и нижнюю правую часть изображения:
// Выводим нижнюю правую четвертинку
Bitmap bmDownRightPartial = Bitmap.createBitmap(halfWidth, halfHeight,
Bitmap.Config.ARGB_8888);
int[] pixels5 = new int[halfWidth * halfHeight];
bmOriginal.getPixels(pixels5, 0, halfWidth, halfWidth, halfHeight,
halfWidth, halfHeight);
bmDownRightPartial
.setPixels(pixels5, 0, halfWidth, 0, 0, halfWidth, halfHeight);
image2.setImageBitmap(bmDownRightPartial);
Немного модифицировав код, мы можем кадрировать центр исходного изображения. Предварительно придётся проделать несколько несложных вычислений.
// Центр 1/4
Bitmap bmCenterPartial = Bitmap.createBitmap(halfWidth, halfHeight,
Bitmap.Config.ARGB_8888);
int[] pixels = new int[halfWidth * halfHeight];
bmOriginal.getPixels(pixels, 0, halfWidth, halfWidth / 2,
halfHeight / 2, halfWidth, halfHeight);
bmCenterPartial
.setPixels(pixels, 0, halfWidth, 0, 0, halfWidth, halfHeight);
image2.setImageBitmap(bmCenterPartial);
Скриншот приводить не буду, проверьте самостоятельно.
Через метод getPixels() мы можем получить массив всех пикселей растра, а затем в цикле заменить определённым образом цвета в пикселе и получить перекрашенную картинку. Для примера возьмем стандартный значок приложения, поместим его в ImageView, извлечём информацию из значка при помощи метода decodeResource(), применим собственные методы замены цвета и полученный результат поместим в другие ImageView:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<ImageView
android:id="@+id/image1"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/image2"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/image3"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/image4"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
Код для класса активности:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
ImageView image1 = findViewById(R.id.image1);
ImageView image2 = findViewById(R.id.image2);
ImageView image3 = findViewById(R.id.image3);
ImageView image4 = findViewById(R.id.image4);
Bitmap bmOriginal = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
image1.setImageBitmap(bmOriginal);
int width = bmOriginal.getWidth();
int height = bmOriginal.getHeight();
Bitmap bmDublicated2 = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Bitmap bmDublicated3 = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Bitmap bmDublicated4 = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
int[] srcPixels = new int[width * height];
bmOriginal.getPixels(srcPixels, 0, width, 0, 0, width, height);
int[] destPixels = new int[width * height];
swapGB(srcPixels, destPixels);
bmDublicated2.setPixels(destPixels, 0, width, 0, 0, width, height);
image2.setImageBitmap(bmDublicated2);
swapRB(srcPixels, destPixels);
bmDublicated3.setPixels(destPixels, 0, width, 0, 0, width, height);
image3.setImageBitmap(bmDublicated3);
swapRG(srcPixels, destPixels);
bmDublicated4.setPixels(destPixels, 0, width, 0, 0, width, height);
image4.setImageBitmap(bmDublicated4);
}
void swapGB(int[] src, int[] dest) {
for (int i = 0; i < src.length; i++) {
dest[i] = (src[i] & 0xffff0000) | ((src[i] & 0x000000ff) << 8)
| ((src[i] & 0x0000ff00) >> 8);
}
}
void swapRB(int[] src, int[] dest) {
for (int i = 0; i < src.length; i++) {
dest[i] = (src[i] & 0xff00ff00) | ((src[i] & 0x000000ff) << 16)
| ((src[i] & 0x00ff0000) >> 16);
}
}
void swapRG(int[] src, int[] dest) {
for (int i = 0; i < src.length; i++) {
dest[i] = (src[i] & 0xff0000ff) | ((src[i] & 0x0000ff00) << 8)
| ((src[i] & 0x00ff0000) >> 8);
}
}
На скриншоте представлен оригинальный значок и три варианта замены цветов.
Ещё один пример, где также в цикле меняем цвет каждого пикселя Green->Blue, Red->Green, Blue->Red (добавьте на экран два ImageView):
public class BitmapProcessingActivity extends Activity {
ImageView imageView_Source, imageView_Dest;
Bitmap bitmap_Source, bitmap_Dest;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
imageView_Source = findViewById(R.id.source);
imageView_Dest = findViewById(R.id.dest);
bitmap_Source = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
imageView_Dest.setImageBitmap(processingBitmap(bitmap_Source));
}
private Bitmap processingBitmap(Bitmap src){
Bitmap dest = Bitmap.createBitmap(
src.getWidth(), src.getHeight(), src.getConfig());
for(int x = 0; x < src.getWidth(); x++){
for(int y = 0; y < src.getHeight(); y++){
// получим каждый пиксель
int pixelColor = src.getPixel(x, y);
// получим информацию о прозрачности
int pixelAlpha = Color.alpha(pixelColor);
// получим цвет каждого пикселя
int pixelRed = Color.red(pixelColor);
int pixelGreen = Color.green(pixelColor);
int pixelBlue = Color.blue(pixelColor);
// перемешаем цвета
int newPixel= Color.argb(
pixelAlpha, pixelBlue, pixelRed, pixelGreen);
// полученный результат вернём в Bitmap
dest.setPixel(x, y, newPixel);
}
}
return dest;
}
}
public byte[] getByteArrayfromBitmap(Bitmap bitmap) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bitmap.compress(CompressFormat.PNG, 0, bos);
return bos.toByteArray();
}
public Bitmap getBitmapfromByteArray(byte[] bitmap) {
return BitmapFactory.decodeByteArray(bitmap , 0, bitmap.length);
}
В предыдущем примере вызывался метод compress(). Несколько слов о нём. В первом аргументе передаём формат изображения, поддерживаются форматы JPEG, PNG, WEBP. Во втором аргументе указываем степень сжатия от 0 до 100, 0 - для получения малого размера файла, 100 - максимальное качество. Формат PNG не поддерживает сжатие с потерей качества и будет игнорировать данное значение. В третьем аргументе указываем файловый поток.
bitmap.compress(CompressFormat.PNG, 0, bos); // см. пример выше
// ещё один пример в начале статьи
FileOutputStream fos = new FileOutputStream(dirname + "cat.jpg");
bitmap.compress(CompressFormat.JPEG, 75, fos);
Если изображение передаётся в текстовом виде через Base64-строку, то воспользуйтесь методом, позволяющим получить картинку из этой строки:
public static Bitmap convertBase64StringToBitmap(String source) {
byte[] rawBitmap = Base64.decode(source.getBytes(), Base64.DEFAULT);
Bitmap bitmap = BitmapFactory.decodeByteArray(rawBitmap, 0, rawBitmap.length);
return bitmap;
}
int[] averageARGB(Bitmap pic) {
int A, R, G, B;
A = R = G = B = 0;
int pixelColor;
int width = pic.getWidth();
int height = pic.getHeight();
int size = width * height;
for (int x = 0; x < width; ++x) {
for (int y = 0; y < height; ++y) {
pixelColor = pic.getPixel(x, y);
A += Color.alpha(pixelColor);
R += Color.red(pixelColor);
G += Color.green(pixelColor);
B += Color.blue(pixelColor);
}
}
A /= size;
R /= size;
G /= size;
B /= size;
int[] average = {A, R, G, B};
return average;
}
Конвертируем Drawable в Bitmap и выводим в ImageView
Displaying Bitmaps Efficiently
На StackOverFlow есть интересный пример программной генерации цветных квадратов с первой буквой слова. В пример квадрат используется как значок к приложению. Также популярен этот приём в списках. Квадраты также заменять кружочками.