Displaying images in android

Содержание
  1. Displaying images in Android app: maintainable, testable, painless. Part II
  2. Making transformations
  3. Creating a delegate to display transaction
  4. Creating a delegate for generated images
  5. Creating customized transformation
  6. Testing
  7. Conclusion
  8. Android. Вывод изображений, различные способы
  9. Отображение картинок в Android-приложении: делегаты, тесты и никакой боли
  10. Авторизуйтесь
  11. Отображение картинок в Android-приложении: делегаты, тесты и никакой боли
  12. Lead Android Engineer в Revolut
  13. Приложение Revolut
  14. Как работает стандартный способ отображения картинок
  15. Как улучшить адаптер
  16. Как отображать картинки
  17. Как объединить делегаты
  18. Как запустить трансформации
  19. Как создать делегат для отображения транзакции
  20. Как создать делегат для генерируемой картинки
  21. Как создать кастомную трансформацию
  22. Как тестировать
  23. Кейс 1 — Перевод контакту без аватарки
  24. Кейс 2 — Перевод контакту с аватаркой
  25. Кейс 3 — Покупка в магазине, у которого в системе есть аватар
  26. Кейс 4 — Покупка в магазине без аватарки
  27. Выводы

Displaying images in Android app: maintainable, testable, painless. Part II

Last week we published the first part of this article about building maintainable and scalable system for displaying dozens of thousands of images in your Android app.

Part I contained the approach description and the delegates building instructions. The second part describes the transformation and testing processes and sums-up the results.

Please check the first part before you start reading. Let’s go.

Making transformations

Why do we need transformations? Let’s pretend that we have a contact’s (or a merchant’s) userpic uploaded from the web. The userpic can have any form and size, whilst in Revolut app, we need to display it round-shaped at 40 х 40 dp.

To set the model and make it behave accordingly let’s take UrlImage class as an example. Every image to be transformed should have relevant settings. We set it by creating TransformableImage interface with transformations property:

The class may look like this:

To display images, we use Glide. Naturally, all our transformations correspond to this library.

That’s how we create the transformations array.

To avoid extra work, mark the fields as nullable (this lets you implement only the needed transformations), and pay attention to the transformations’ order.

Imagine that we have a very large image we need to rotate, scale and crop into a circle. Let’s compare two scenarios:

In the first one we start with image rotation, then crop it into a circle and only after that we scale it. In the second scenario, we start with scaling.

Obviously, the second one seems more reasonable, because rotating and cropping of a small image demand fewer hardware resources.

Earlier we created the array that we’ll pass to Glide when it displays an image by URL. Now we build an object RequestOptions and pass the array to the object. If we pass an empty array, Glide will fail. So it is mandatory to add a verification.

We’re going to reuse transformations in different delegates, that’s why we can put them to the extension method applyImageTransformations.

Also, we add a method to the interface TransformableImage — getGlideTransformsArray. The interface and the extension method applyImageTransformations are marked as internal. It helps to avoid the leak of abstraction — Glide is not a part of the public interface. It’s important if we want to change Glide to any other library.

The code looks like this:

Creating a delegate to display transaction

We already know how the adapter works. Let’s create a delegate to display the transaction. The basic version looks like this:

To simplify the code we skip displaying the text. We can make this delegate display transactions with pictures from web or resources, with contact userpic generated from the initial letters.

We start by modifying the model.

In each case, we use specific parameters gathered in one place. The image will be displayed like this:

This solution has some disadvantages:

  • hard to extend;
  • the order is important, and it may not be obvious which order to choose;
  • the logic is inside the adapter (in the delegate).

Let’s try to use image delegates. In the model, we leave only the image to display instead of all other parameters.

The transaction list will look like this:

Its behavior becomes more obvious, and also we take the logic out of the adapter.

Creating a delegate for generated images

Let’s take a particular case of creating a delegate that generates an image of two symbols. First of all, we define requirements for this delegate: it must be able to display the letters and adjust the image.

Hence, the model looks like this:

To adjust the background we use ImageTransformations.

Now let’s proceed to bitmap generation. For example, we can use TextDrawable, where the image is built with Canvas. Then we handle the bitmap and set it in ImageView.

As we use the extension, the delegate takes only a couple of lines. Here is how it works.

The first version with basic settings is:

At the second stage, we crop the image into a circle:

At the third stage, we rotate the image. So we can display the userpic icon as we need to follow the design guidelines.

Creating customized transformation

Let’s say, we need to flip the image horizontally. In order to do so, we build a transformation class framework.

If we use Glide, the basic class is BitmapTransformation: Glide makes our life easier because it contains TransformationUtils with methods we need. All we have to do is to add the transformation to others.

Читайте также:  Календарь рабочих дней для андроид

Testing

One of the main reasons why we use this solution is testability.

Let’s see how the clean architecture may look like, and see how data goes to the UI layer. We use the transaction list as data.

It looks pretty regular. The database returns the models list, and on the repository level, we map them into models of the domain level. Then it sends them to UI level. Each mapping stage is covered with tests.

This is how the domain transaction’s model looks like:

It contains the transaction’s id, amount and date. How does it understand if it’s a money transfer or a purchase? Where does it take the title or URL? The answer is the sealed class.

We see two transaction types — money transfer and purchase. Each of them has a unique set of parameters.

What is the model for the UI layer? Let’s go back to the delegate for the adapter RecyclerView.

Delegate model can be perfectly used as a UI model.

Here are some cases we can test only with the help of delegates.

Case 1 — Money transfer to a contact without a userpic.

We expect that if there is no URL for userpic, the model to display the initial letters is created.

Case 2 — Money transfer to a contact with the userpic.

We expect that UrlImage with a transformation will be created.

Case 3 — Purchase in a shop with the userpic.

Same as the second: we expect that UrlImage with a transformation will be created.

Case 4 — Purchase in a shop without a userpic.

Here we can make an additional test: every purchase can belong to a separate category, so the icons will differ. We can also check if we map categories in the relevant icons.

Conclusion

We benefit from using delegates.

First, we remove the logic that shouldn’t be in the adapter: it shouldn’t be responsible for the image source choice, depending on its parameters.

Secondly, we don’t depend on image loading and adjusting the method anymore. We can replace Glide with any other library at any moment.

Thirdly, we can check that image type we need is displayed, in other words, we can test the data displaying.

Источник

Android. Вывод изображений, различные способы

Эта статья будет полезна начинающим разработчикам, здесь я предложу несколько вариантов вывода изображений на Android. Будут описаны следующие способы:

Обычный метод – стандартный способ, используя ImageView. Рассмотрены варианты загрузки картинки из ресурса, а также из файла на SD карте устройства.

Продвинутый вариант — вывод изображения, используя WebView. Добавляется поддержка масштабирования и прокрутки картинки при помощи жестов.

“Джедайский” способ – улучшенный предыдущий вариант. Добавлен полноэкранный просмотр с автоматическим масштабированием изображения при показе и поддержкой смены ориентации устройства.

Исходники тестового проекта на GitHub github.com/Voldemar123/andriod-image-habrahabr-example

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

Итак, задача — предположим, в нашем приложении необходимо вывести изображение на экран.
Картинка может размерами превышать разрешение экрана и иметь различное соотношение сторон.
Хранится она либо в ресурсах приложения, либо на External Storage — SD карте.

Также допустим, мы уже записали на карту памяти несколько изображений (в тестовом проекте – загружаем из сети). Храним их в каталоге данных нашего приложения, в кеше.

public static final String APP_PREFS_NAME = Constants.class.getPackage().getName();
public static final String APP_CACHE_PATH =
Environment.getExternalStorageDirectory().getAbsolutePath() +
«/Android/data/» + APP_PREFS_NAME + «/cache/»;

Layout, где выводится картинка

android:layout_width=»match_parent»
android:layout_height=»match_parent»
android:orientation=»vertical» >

Масштабирование по умолчанию, по меньшей стoроне экрана.
В Activity, где загружаем содержимое картинки

private ImageView mImageView;
mImageView = (ImageView) findViewById(R.id.imageView1);

Из ресурсов приложения (файл из res/drawable/img3.jpg)

Задавая Bitmap изображения

FileInputStream fis = new FileInputStream(Constants.APP_CACHE_PATH + this.image);
BufferedInputStream bis = new BufferedInputStream(fis);

Bitmap img = BitmapFactory.decodeStream(bis);

Или передать URI на изображение (может хранится на карте или быть загружено из сети)

mImageView.setImageURI( imageUtil.getImageURI() );
Uri.fromFile( new File( Constants.APP_CACHE_PATH + this.image ) );

Этот способ стандартный, описан во множестве примеров и поэтому нам не особо интересен. Переходим к следующему варианту.

Предположим, мы хотим показать большое изображение (например фотографию), которое размерами превышает разрешение нашего устройства. Необходимо добавить прокрутку и масштабирование картинки на экране.

android:layout_width=»match_parent»
android:layout_height=»match_parent»
android:orientation=»vertical» >

В Activity, где загружаем содержимое

protected WebView webView;
webView = (WebView) findViewById(R.id.webView1);

установка черного цвета фона для комфортной работы (по умолчанию – белый)

включаем поддержку масштабирования

больше места для нашей картинки

webView.setPadding(0, 0, 0, 0);

полосы прокрутки – внутри изображения, увеличение места для просмотра

загружаем изображение как ссылку на файл хранящийся на карте памяти

webView.loadUrl(imageUtil.getImageFileLink() );
«file:///» + Constants.APP_CACHE_PATH + this.image;

Теперь мы хотим сделать так, чтобы картинка при показе автоматически масштабировалась по одной из сторон, при этом прокрутка остается только в одном направлении.
Например, для просмотра фотографий более удобна ландшафтная ориентация устройства.
Также при смене ориентации телефона масштаб изображения должен автоматически меняться.
Дополнительно расширим место для просмотра изображения на полный экран.

В AndroidManifest.xml для нашей Activity добавляем

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

@Override
public void onConfigurationChanged(Configuration newConfig) <
super.onConfigurationChanged(newConfig);
changeContent();
>

В приватном методе описана логика пересчета масштаба для картинки
Получаем информацию о размерах дисплея. Из-за того, что мы изменили тему Activity, теперь WebView раскрыт на полный экран, никакие другие элементы интерфейса не видны. Видимый размер дисплея равен разрешению экрана нашего Android устройства.

Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();

int width = display.getWidth();
int height = display.getHeight();

Размеры изображения, выбранного для показа

Bitmap img = imageUtil.getImageBitmap();

Читайте также:  Как сделать чтоб андроид говорил кто звонит

int picWidth = img.getWidth();
int picHeight = img.getHeight();

Меняем масштаб изображения если его высота больше высоты экрана. Прокрутка теперь будет только по горизонтали.

if (picHeight > height)
val = new Double(height) / new Double(picHeight);

Подбрасываем в WebView специально сформированный HTML файл, содержащий изображение.

webView.loadDataWithBaseURL(«/»,
imageUtil.getImageHtml(picWidth, picHeight),
«text/html»,
«UTF-8»,
null);

StringBuffer html = new StringBuffer();

Такой способ я применил из-того, что после загрузки изображения в WebView через метод loadUrl, как в прошлом варианте, setInitialScale после поворота устройства не изменяет масштаб картинки. Другими словами, показали картинку, повернули телефон, масштаб остался старый. Очень похоже на то, что изображение как-то кешируется.

Я не нашел в документации упоминания об этом странном поведении. Может быть местные специалисты скажут, что я делаю не так?

Источник

Отображение картинок в Android-приложении: делегаты, тесты и никакой боли

Авторизуйтесь

Отображение картинок в Android-приложении: делегаты, тесты и никакой боли

Lead Android Engineer в Revolut

В приложениях мы показываем десятки тысяч картинок из разных источников — загружаем из сети, локально, генерируем. Плодить разные сущности кода для их отображения трудоёмко и неэффективно, логичнее делать обобщения. Расскажу, как мы построили систему отображения картинок из любых источников и снизили уровень собственной боли от этого процесса.

Приложение Revolut

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

У нашего списка транзакций насчитывается несколько десятков типов ячеек. Для примера мы возьмем пять:

Разные типы транзакций, где мы показываем картинку

В каждом случае картинка взята из отдельного источника или сгенерирована.

Как работает стандартный способ отображения картинок

Создадим адаптер для такого списка.

Так будет выглядеть стандартный шаблон адаптера для RecyclerView . Реализуем биндинг значений:

Появляется портянка условий, потому что внутри адаптера для каждого вида транзакции мы строим отдельную логику. Можно усложнить и использовать свой ViewType под каждый источник. Тем более к этому подталкивает контракт адаптера:

Учитывая, что в нашем случае может быть несколько десятков видов транзакций, стандартный способ реализации адаптера не подходит.

Как улучшить адаптер

Можем выделить два основных подхода к расширению — ViewType или делегаты. Остальные не упоминаю специально: по своей сути они будут похожи на второй подход.

Первый вариант — ViewType — можно использовать, когда приложение простое, то есть содержит один простой список и, например, пару экранов. Нам этот способ не подходит, потому что такие адаптеры нельзя переиспользовать. Если мы будем расширять адаптер, добавляя новые ViewType , адаптер будет неконтролируемо расти. Кроме того, под каждый экран нам придётся создавать свои адаптеры.

Revolut , Удалённо , По итогам собеседования

Второй подход — с делегатами — выглядит привлекательнее. Он позволяет не создавать разные адаптеры под каждый экран, а использовать делегаты. Четыре года назад об этом писал Ханс Дорфман, и на GitHub можно найти много библиотек с реализацией такого подхода. Мы будем использовать реализацию самого Дорфмана.

Смотрим на пример простого делегата, который отображает ProgressBar.

Внутри делегата, как и в стандартном адаптере, создаем ViewHolder . Происходит биндинг. Главное отличие от стандартного адаптера в том, что у каждого делегата есть своя модель. Она будет использоваться, чтобы отобразить нужный тип ячейки. В свою очередь, у каждой модели есть интерфейс ListItem с полем listId и методом calculatePayloads внутри.

Перейдём к реализации адаптера, который умеет отображать делегаты.

В этой реализации видно, зачем нужен интерфейс ListItem — его удобно использовать для ListDiffCallback , чтобы DiffUtil не обновлял ячейки, которые не изменились, и не запускал лишние анимации. Кроме того, так как для моделей используется Data class, нам из коробки доступен equals . Вся работа с DiffUtil сводится к правильному созданию модели делегата.

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

Благодаря делегатам создание адаптера под каждый экран упрощается.

Как отображать картинки

Теперь уберём логику загрузки и отображения картинок из адаптера, разгрузим onBindViewHolder . Мы должны реализовать две сущности — модель картинки и делегат, который будет уметь загружать и отображать её. Рассмотрим пример модели, где загружаем картинку из ресурсов.

Сначала сделаем интерфейс Image . Затем опишем набор параметров для ResourceImage , по которым хотим настраивать отображение. В данном случае — id ресурса картинки и цвета, если хотим её закрасить.

Теперь перейдём к делегату загрузки и определим его интерфейс. Отсюда понятно, зачем нам интерфейс Image .

Каждый делегат должен уметь делать две вещи:

  1. определять, умеет ли он отображать переданную картинку или нет;
  2. отображать картинку в ImageView .

Так будет выглядеть делегат загрузки картинки из ресурсов.

  • метод suitsFor() проверяет, что image — ResourceImage ;
  • внутри метода displayTo() мы устанавливаем картинку в ImageView и, если colorRes не null , то выставляем tint .

Это самый простой из возможных делегатов.

Как объединить делегаты

Объединим все поддерживаемые делегаты в одном месте и сократим интерфейс взаимодействия до метода displayTo() .

Обращаю внимание на строку 18. При помощи метода first() мы находим первый подходящий делегат для отображения картинки. Если нужный делегат не найден, возможен краш, и это не ошибка проектирования. Мы намеренно придерживаемся принципа fail-fast, чтобы быстро избавиться от неочевидного поведения. Например, когда картинка не отобразилась, а мы не знаем причину.

Как запустить трансформации

Разберёмся, зачем в отображении транзакции могут понадобиться трансформации. Предположим, у нас есть аватарка контакта или продавца, которую мы получаем из сети. Она может иметь любую форму и размер, но в приложении Revolut мы должны отобразить её круглой и определённого размера — 40х40 dp.

Аватары в приложении Revolut

Настроим модель и добьёмся такого поведения.

Возьмём UrlImage . Любая картинка, которой нужна поддержка трансформаций, должна иметь соответствующие настройки. Можно ввести интерфейс TransformableImage со свойством transformations :

Читайте также:  Индикатор для мт5 андроид

Класс настроек может выглядеть так:

Для отображения картинок используем Glide. Соответственно, трансформации ориентированы под эту библиотеку.

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

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

Представим, что на входе — очень широкая картинка, которую надо повернуть, отмасштабировать и скруглить. Сравним два сценария развития событий.

В первом случае сначала мы поворачиваем картинку на 90 градусов, затем закругляем и только после этого меняем ширину. Во втором случае мы сначала меняем ширину.

Второй сценарий эффективнее, так как поворот и скругление «дешевле» делать на меньших изображениях.

Вернёмся к реализации. Ранее мы создали массив, который теперь должны передать в Glide, когда он будет отображать картинку по URL. Создаём объект RequestOptions и передаём ему массив. Помним, что нельзя передавать пустой массив — Glide упадёт. Поэтому обязательно добавляем проверку.

Так как будем переиспользовать трансформации в разных делегатах, будет удобно вынести их в экстеншн applyImageTransformations .

Также добавляем метод в интерфейс TransformableImage — getGlideTransformsArray() . Сам интерфейс и экстеншен applyImageTransformations помечены как internal . Так мы избегаем утечки абстракции, и конечный пользователь моделей и делегатов не знает, что используется внутри — в публичных интерфейсах Glide не виден. Удобно, если захотим заменить Glide на другую библиотеку.

В итоге код сокращается до такого вида:

Как создать делегат для отображения транзакции

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

Базовая реализация адаптера выглядит так:

Отображение текста убрано для упрощения. Мы научим этот делегат отображать транзакции с картинками из сети, из ресурсов, показывать аватар контакта, который создаётся из инициалов.

Сначала модифицируем модель.

В каждом случае передаём свои параметры, все в одном месте. Примерно так будет отображаться картинка:

Сразу видим минусы:

  • такое решение тяжело расширять;
  • важен порядок, который в свою очередь может быть неочевидным;
  • бизнес-логика находится внутри адаптера (делегата).

Начнём сначала и попробуем использовать делегаты. Сделаем несколько доработок.

В модели вместо всех параметров оставляем только картинку на отображение:

В итоге список транзакций примет такой вид:

Поведение становится более явным, и мы вынесли логику из адаптера.

Как создать делегат для генерируемой картинки

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

Модель будет выглядеть следующим образом:

Для настройки фона используем ImageTransformations .

Перейдём к генерации битмапы. Можем использовать обертку TextDrawable , где внутри отрисовка идёт при помощи Canvas . Далее эту битмапу нужно обработать и установить в ImageView .

За счёт использования экстеншена реализация делегата занимает пару строк. Покажу, как он работает.

Первый вариант, где заданы базовые настройки:

Во втором варианте добавляем трансформацию скругления:

И в третьем — поворачиваем картинку. Нам ничего не стоит отображать иконку аватара в том виде, в котором это требуется согласно дизайну:

Как создать кастомную трансформацию

Представим, что нам надо сделать флип по горизонтали. Сначала создадим каркас класса трансформации:

В случае с Glide базовый класс должен быть BitmapTransformation . Glide снова упрощает жизнь, так как содержит TransformationUtils с нужными методами. Остаётся лишь добавить эту трансформацию к остальным.

Как тестировать

Одна из главных причин, почему стоит использовать именно этот способ работы с картинками — тестирование.
Нарисуем примерную схему архитектуры (clean) и покажем, как данные доходят до слоя UI. В качестве данных будем рассматривать список транзакций.

Пример чистой архитектуры

Получилась довольно стандартная схема. База данных возвращает список моделей, на уровне репозитория мы мапим их в модели доменного уровня. Тот, в свою очередь, передаст их на уровень выше — до UI. Каждый этап маппинга моделей покрывается тестами.

Рассмотрим, как может выглядеть доменная модель транзакции:

У неё есть доступ к id транзакции, сумме и дате. Как понять, что показывать — это денежный перевод или покупка в магазине? Откуда брать название, URL? Нам помогут sealed class.

Здесь мы видим два типа транзакций — перевод и покупка. Каждый имеет уникальный набор параметров.

Далее разберёмся, что является моделью для слоя UI, и для этого вспомним, как выглядел наш делегат для адаптера RecyclerView .

Модель делегата отлично подходит в качестве UI-модели.

Рассмотрим несколько сценариев, которые можем протестировать только за счёт использования делегатов для адаптера и картинок.

Кейс 1 — Перевод контакту без аватарки

Проверяем, создаётся ли модель картинки для отображения инициалов, если отсутствует URL аватара .

Кейс 2 — Перевод контакту с аватаркой

Ожидаем, что будет создана UrlImage с одной трансформацией.

Кейс 3 — Покупка в магазине, у которого в системе есть аватар

Идентично кейсу 2: ожидаем, что будет создана UrlImage с одной трансформацией.

Кейс 4 — Покупка в магазине без аватарки

В данном случае можем сделать дополнительную проверку: каждая покупка может относиться к разным категориям, и иконки будут отличаться. Можем также проверить, мапим ли каждую категорию в нужную иконку.

Выводы

Отображение картинок с помощью делегатов даёт несколько преимуществ.

Во-первых, мы освобождаем адаптер от логики, которой в нём быть не должно. Он не должен отвечать за выбор источника картинки в зависимости от набора параметров.

Во-вторых, мы абстрагировались от способа загрузки и обработки изображений. Теперь в любой момент времени мы почти без боли можем заменить Glide на что-то другое.

В-третьих, как следствие, мы можем тестировать отображение нужного типа картинки. То есть фактически тестировать отображение данных на экране.

Источник

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