Новый современный набор инструментов пользовательского интерфейса Jetpack Compose был анонсирован компанией Google более года назад, и, наконец, в июле была выпущена стабильная версия 1.0. Также многие компании, такие как Twitter, lyft, Square, уже адаптировали Jetpack Compose на своих производственных уровнях, потому что он очень интуитивный, мощный и упрощает всю структуру пользовательского интерфейса, если правильно его использовать. Эта новая парадигма структуры пользовательского интерфейса поменяет в дальнейшем очень многое, также нам придется приложить еще немало усилий для миграции предыдущих вещей, связанных с пользовательским интерфейсом, таких как загрузка изображений из Url.  

Загрузка и отрисовка изображений для Jetpack Compose

Когда Google анонсировал Jetpack Compose 1.0.0-alpha01, я задался вопросом, как мы должны перенести все предыдущие системы, связанные с пользовательским интерфейсом, в проекты Jetpack Compose? Простое получение изображений из Url (сети) и рисование на компонуемых Image потребовало бы совершенно иного подхода, чем раньше. В то время была выпущена библиотека accompanist Криса Бейнса, и она меня очень вдохновила. Эта библиотека поддерживала Coil (позже появилась поддержка Glide), но я хотел использовать и предоставить как можно больше вариантов для выбора библиотек загрузки изображений. Потому что миграция целых систем загрузки изображений (например, с Glide на Coil) может доставить разработчикам немало хлопот. Поэтому Landscapist был создан для поддержки многих вариантов, таких как Glide, Coil и Fresco для Jetpack Compose.

Landscapist

Landscapist - это библиотека загрузки изображений для Jetpack Compose. Есть три варианта: Glide, Coil и Fresco. Таким образом, мы можем выбирать по своему вкусу. Эта библиотека также поддерживает анимацию загрузки, например, эффект мерцания и круговое раскрытие. Если вы хотите узнать больше о применении этой библиотеки, то можете обратиться к демонстрационным проектам, использующим Landscapist для рисования изображений.

GlideImage

Мы можем загружать и рисовать изображения из Url с помощью компонуемой функции GlideImage, как показано ниже. Это довольно просто.

GlideImage(
  imageModel = poster.poster
)

Мы также можем предоставить основные атрибуты, такие же, как у компонуемого Image, например contentScale и modifier.

GlideImage(
  imageModel = poster.poster,
  contentScale = ContentScale.Crop,
  modifier = Modifier
)

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

GlideImage(
  imageModel = imageUrl,
  // Crop, Fit, Inside, FillHeight, FillWidth, None
  contentScale = ContentScale.Crop,
  // shows an image with a circular revealed animation.
  circularRevealedEnabled = true,
  // shows a placeholder ImageBitmap when loading.
  placeHolder = ImageBitmap.imageResource(R.drawable.placeholder),
  // shows an error ImageBitmap when the request failed.
  error = ImageBitmap.imageResource(R.drawable.error)
)

Различные варианты компоновки на основе состояний запроса

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

 GlideImage(
 imageModel = poster.poster,
 modifier = modifier,
 // shows a progress indicator when loading an image.
 loading = {
   ConstraintLayout(
     modifier = Modifier.fillMaxSize()
   ) {
     val indicator = createRef()
     CircularProgressIndicator(
       modifier = Modifier.constrainAs(indicator) {
         top.linkTo(parent.top)
         bottom.linkTo(parent.bottom)
        start.linkTo(parent.start)
        end.linkTo(parent.end)
       }
     )
   }
 },
 // shows an error text message when request failed.
 failure = {
   Text(text = "image request failed.")
 })

Если мы хотим настроить свою композицию успеха, мы также можем настроить результат пользовательского интерфейса успеха, как показано ниже. Лямбда success передает состояние запроса, и мы можем получить ImageBitmap из его инстанса.

GlideImage(
  imageModel = poster.poster,
    success = {
      Image(
        bitmap = it.imageBitmap!!,
        contentDescription = null,
        modifier = Modifier
          .width(128.dp)
          .height(128.dp)
        )
   }
 )

LocalGlideRequestBuilder

CompositionLocal - одна из наиболее часто используемых концепций в Jetpack Compose, и в официальном справочнике Android она описывается следующим образом.

Compose передает данные через дерево композиции в явном виде посредством параметров к компонуемым функциям. Часто это самый простой и лучший способ передачи данных по дереву. CompositionLocals можно использовать как неявный способ передачи данных через композицию.

Landscapist также поддерживает CompositionLocal для предоставления RequestBuilder (основа запроса в Glide) в компонуемом потоке данных.

// customize the RequestBuilder as needed
val requestBuilder = Glide.with(LocalView.current)
  .asBitmap()
  .thumbnail(0.1f)
  .transition(BitmapTransitionOptions.withCrossFade())

CompositionLocalProvider(LocalGlideRequestBuilder provides requestBuilder) {
  // This will automatically use the value of current RequestBuilder in the hierarchy.
  GlideImage(
    imageModel = ...
  )
}

CoilImage

Coil - это почти то же самое, что и Glide, только вместо GlideImage используется CoilImage.

CoilImage(
  imageModel = poster.poster
)

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

CoilImage(
  imageModel = imageUrl,
  // Crop, Fit, Inside, FillHeight, FillWidth, None
  contentScale = ContentScale.Crop,
  // shows an image with a circular revealed animation.
  circularRevealedEnabled = true,
  // shows a placeholder ImageBitmap when loading.
  placeHolder = ImageBitmap.imageResource(R.drawable.placeholder),
  // shows an error ImageBitmap when the request failed.
  error = ImageBitmap.imageResource(R.drawable.error)
)

LocalCoilImageLoader

Также поддерживает CompositionLocal для обеспечения подобия ImageLoader в компонуемой иерархии.

val imageLoader = ImageLoader.Builder(context)
   // customize the ImageLoader as needed
   .build()
CompositionLocalProvider(LocalCoilImageLoader provides imageLoader) {
  // This will automatically use the value of current imageLoader in the hierarchy.
  CoilImage(
    imageModel = ...
  )
}

FrescoImage

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

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

class App : Application() {

  override fun onCreate() {
    super.onCreate()

    val pipelineConfig =
      OkHttpImagePipelineConfigFactory
        .newBuilder(this, OkHttpClient.Builder().build())
        .setDiskCacheEnabled(true)
        .setDownsampleEnabled(true)
        .setResizeAndRotateEnabledForNetwork(true)
        .build()

    Fresco.initialize(this, pipelineConfig)
  }
}

Также и другие вещи почти не отличаются от вышеупомянутых библиотек.

FrescoImage(
  imageUrl = stringImageUrl,
  // Crop, Fit, Inside, FillHeight, FillWidth, None
  contentScale = ContentScale.Crop,
  // shows an image with a circular revealed animation.
  circularRevealedEnabled = true,
  // shows a placeholder ImageBitmap when loading.
  placeHolder = ImageBitmap.imageResource(R.drawable.placeholder),
  // shows an error ImageBitmap when the request failed.
  error = ImageBitmap.imageResource(R.drawable.error)
)

Эффект мерцания

Landscapist поддерживает эффект мерцания при загрузке изображений из сети, и мы можем реализовать его с помощью ShimmerParams. В примере ниже используется CoilImage, но ShimmerParams также можно применить для GlideImage и FrescoImage.

 CoilImage(
 imageModel = poster.poster,
 modifier = modifier,
 // shows a shimmering effect when loading an image.
 shimmerParams = ShimmerParams(
        baseColor = MaterialTheme.colors.background,
        highlightColor = shimmerHighLight,
        durationMillis = 350,
        dropOff = 0.65f,
        tilt = 20f
      ),
 // shows an error text message when request failed.
 failure = {
   Text(text = "image request failed.")
 })

Можно настроить все детали ShimmerParam, используя baseColor, highlightColor, durationMillis, dropOff и tilt.

Анимация кругового раскрытия

Landscapist поддерживает анимацию кругового раскрытия при показе изображений. Это можно реализовать очень просто, задав для атрибута circularRevealedEnabled значение true.

FrescoImage(
  imageUrl = stringImageUrl,
  // Crop, Fit, Inside, FillHeight, FillWidth, None
  contentScale = ContentScale.Crop,
  // shows an image with a circular revealed animation.
  circularRevealedEnabled = true,
  // shows a placeholder ImageBitmap when loading.
  placeHolder = ImageBitmap.imageResource(R.drawable.placeholder),
  // shows an error ImageBitmap when the request failed.
  error = ImageBitmap.imageResource(R.drawable.error)
)

CircularRevealedImage

Мы можем использовать составное изображение CircularRevealedImage независимо от библиотек загрузки (Glide, Coil, Fresco) и реализовать анимацию кругового раскрытия, используя наше рисованное изображение ресурса.

CircularRevealedImage(
    bitmap = ImageBitmap.imageResource(R.drawable.flower),
    contentDescription = null,
    circularRevealedEnabled = true,
    circularRevealedDuration = 350
  )

Мы должны установить параметр circularRevealedEnabled в true, если хотим применить анимацию кругового раскрытия, также можно изменить продолжительность анимации с помощью атрибута circularRevealedDuration.

Палитра

Landscapist поддерживает API палитры для извлечения цветовых профилей из изображений. В основном, необходимо использовать BitmapPalette для извлечения основных цветов из изображений, как показано ниже. И можно ссылаться на типы цветов здесь.

var palette by remember { mutableStateOf<Palette?>(null) }

GlideImage( // CoilImage, FrescoImage also can be used.
  imageModel = poster?.poster!!,
  bitmapPalette = BitmapPalette {
    palette = it
  }
)

Crossfade(
  targetState = palette,
  modifier = Modifier
    .padding(horizontal = 8.dp)
    .size(45.dp)
) {
  Box(
    modifier = Modifier
      .background(color = Color(it?.lightVibrantSwatch?.rgb ?: 0))
      .fillMaxSize()
  )
}

Мы можем настроить больше опций, используя interceptor и paletteLoadListener.

 var palette by remember { mutableStateOf<Palette?>(null) }

  GlideImage(
    imageModel = poster?.poster!!,
    modifier = Modifier
      .aspectRatio(0.8f),
    bitmapPalette = BitmapPalette(
      imageModel = poster.poster,
      useCache = true,
      interceptor = {
        it.addFilter { rgb, hsl ->
          // here edit to add the filter colors.
          false
        }
      },
      paletteLoadedListener = {
        palette = it
      }
    )
  )

Заключение

В этом посте мы рассмотрели, как загружать и рисовать изображения для Jetpack Compose с помощью Landscapist. К большому удовольствию, эта библиотека используется многими мировыми компаниями, включая Twitter. Ее путь начался с Jetpack Compose 1.0.0-alpha, и на данный момент она уже была выпущена более 30 раз, пока не достигла Jetpack Compose 1.0 stable. К счастью, я получил массу вдохновения и помощи от accompanist (спасибо, Крис Бейнс! спасибо за все Google Compose, Android Team!); и это долгое путешествие, кажется, только начинается.


Материал подготовлен в рамках курса «Android Developer. Professional». Всех желающих приглашаем на двухдневный онлайн-интенсив «Android Lint». На нем мы:
- изучим Android Lint API;
- научимся писать кастомные Lint детекторы и тесты на них;
- разберемся, как правильно подсвечивать контекст и реализовывать LintFix’ы;
- напишем несколько проверок на частые ошибки использования популярных библиотек.
>> РЕГИСТРАЦИЯ

Комментарии (0)