RenderEffect в Android 12

16 декабря 2021

При таком большом количестве социальных сетей и приложений для обмена фотографиями в настоящее время довольно распространено применять фильтры к изображениям, прежде чем их публиковать. Возможность делать это в Android OS делает работу более эффективной и простой. До Android 12 процесс был намного сложнее, так как вам приходилось определять RenderNodes, взаимодействуя с Canvas, а затем применять эффекты напрямую с преобразованиями и алгоритмами.

Однако, в Android 12 Google представил API RenderEffect. Это позволяет разработчикам без особых усилий применять к вьюшкам графические эффекты, такие как размытие, цветные фильтры и многое другое.

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

  • Узнаете о RenderEffect API.
  • Изучите ключевые концепции, такие как RenderNodes, Display List и Canvas.
  • Узнаете, как использовать RenderEffect для реализации простых графических эффектов, таких как размытие, цветовые фильтры и смещение.
  • Используете несколько эффектов для создания цепочки эффектов.
  • Реализуете полноэкранное размытие. 

Начало Работы

Загрузите материалы проекта, нажав на Скачать Материалы. Откройте стартовый проект в Android Studio.

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

Соберите и запустите. Вы увидите следующее:

Основные файлы проекта:

  • MainActivity: Точка входа вашего приложения. Здесь вы будете писать весь ваш код.
  • activity_main.xml: Содержит весь дизайн пользовательского интерфейса (UI) приложения.

 

Добавление эффектов до RenderEffect

Добавление графических эффектов во вьюшки было возможно еще до появления API RenderEffect. Добиться этого можно было двумя основными способами:

  • Используя сторонние библиотеки, такие как Glide и Blurry.
  • Используя API RenderScript, что предполагало написание вашего собственного алгоритма.

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

  • Возможные случайные ошибки, вызванные внешними библиотеками.
  • Низкая производительность на определенных устройствах, вызванная ограничениями CPU(ЦП), GPU(графический процессор) и памяти.

К счастью, RenderEffect решает эти проблемы на уровне графического процессора, поскольку операционная система Android сама обрабатывает рендеринг.

 

Понимание RenderEffect

RenderEffect - это посредник, используемый для визуализации команд рисования с соответствующими визуальными эффектами. Он позволяет пользователям применять к вью и иерархиям рендеринга такие эффекты, как размытие, цветовые фильтры, эффекты шейдеров и другие. Это означает, что эффекты добавляются к вью, когда они рисуются на экране на уровне GPU.

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

Android использует RenderNodes для рендеринга всех своих вью на графическом процессоре для повышения производительности. RenderEffect подключается к этой реализации, чтобы применить эффекты к процессу подготовки рендеринга, использующего аппаратное ускорение.

 

Рендеринг в Android

Android рендерит окна вью на графическом процессоре, используя RenderNodes для построения иерархий рендеринга с аппаратным ускорением, обеспечивая лучшую производительность. Каждый RenderNode содержит display lists (списки отображения), точно так же, как и набор свойств, влияющих на рендеринг списка отображения.

Список отображения создается в основном(main) потоке, а затем синхронизируется с RenderThread. RenderThread выдает операции списка отображения графическому процессору, который затем производит рендеринг вью. 

Типичный график GPU Рендеринга большинства приложений до этого API показывает множество недостатков:

Чтобы узнать больше о рендеринге в Android, посмотрите это видео с Google I/O.

 

Понимание RenderNode

Android использует RenderNodes, представленный в API 29, для построения иерархий рендеринга с аппаратным ускорением.

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

Например, когда пользователь щелкает элемент в RecyclerView, обновляется только список отображения этого элемента вместо перезаписи всего RenderNode этого RecyclerView. Другой пример - TextView, в котором имеются различные параграфы. Обновится только список отображения обновленного параграфа, и изменения будут применены.

 

Понимание Display List

Display List (Список отображения) - это структура, в которой хранится информация о рендеринге. Это означает, что он хранит команды рендеринга для всех вью. Графические команды в Canvas, такие как drawBackground(), drawDrawable() и drawLine(), завершаются как операции в списке отображения.

Таким образом, список отображения - это компактный способ представления этих операций и параметров для операций.

 

Понимание Canvas

Canvas (Холст) - это двухмерная поверхность для рисования, которая предоставляет команды для отрисовки в растровое изображение. Canvas предоставляет графические команды/операции вьюшек, такие как drawBackground(), drawDrawable() и drawText(), которые заканчиваются как операции в списке отображения в процессе подготовки рендеринга.

 

Добавление некоторых классных эффектов

Хватит скучной теории, пора применить немного эффектов к фотографии вашей милой собаки. Чтобы добавить какие-либо эффекты с помощью RenderEffect, вам необходимо выполнить три основных шага:

  1. Создать экземпляр RenderEffect.
  2. Реализовать фабричный статический метод RenderEffect.
  3. Далее установить эффект в соответствующий вью.

Чтобы завершить этот туториал, вы будете работать над несколькими TODO.

 

Добавление Размытия

Чтобы размыть вью, сначала вам необходимо реализовать статический фабричный метод createBlurEffect, предоставляемый RenderEffect. Сделайте это, заменив TODO() в // TODO 1: Add blur effect  на:

return RenderEffect.createBlurEffect(radiusX, radiusY, shader)

Приведенный выше код возвращает размытый объект RenderEffect путем реализации фабричного метода createBlurEffect, который принимает три аргумента. Первые два аргумента, radiusX и radiusY, определяют горизонтальный и вертикальный радиусы, для которых должен отображаться эффект размытия. Затем третий параметр указывает, как выводить TileMode по краям.

Теперь замените // TODO 4: Add blur effect на:

//1
      if (isChecked) {
        binding.saturationCheck.isClickable = false
        binding.offsetEffectsCheck.isClickable = false
        binding.chainEffectCheck.isClickable = false 
        //2
        binding.blurSlider.isEnabled = true 
        //3
        val blurEffect = createBlurEffect(DEFAULT_BLUR, DEFAULT_BLUR, Shader.TileMode.MIRROR) 
        //4
        binding.imageView.setRenderEffect(blurEffect)
      } else {
        //5
        binding.imageView.setRenderEffect(null) 
        //6
        binding.blurSlider.isEnabled = false

        binding.saturationCheck.isClickable = true
        binding.offsetEffectsCheck.isClickable = true
        binding.chainEffectCheck.isClickable = true
      }

Вот что происходит в коде выше:

  1. Определяем, установлен ли флажок blurCheckBox.
  2. Если он установлен, включаем слайдер размытия.
  3. Создаем объект размытия, передав значения по умолчанию для радиусов X и Y.
  4. Применяем эффект размытия к вью.
  5. Если параметр blurCheckBox не установлен, удаляем размытие из вью. Для этого передаем  RenderEffect значение null.
  6. Отключаем слайдер размытия.

Заметка

В приведенном выше коде все флажки, кроме «Blur Fullscreen» (Размытие в полноэкранном режиме), отключены, если установлен флажок «Blur» (Размытие), и включены, если он не установлен. В остальной части туториала это поведение повторяется для каждого эффекта, включенного с помощью флажка.

Собираем и запускаем. Отмечаем Blur и видим:

 

 

Регулировка Размытия

Вы можете регулировать размытие вью, меняя значения радиусов, radiusX и radiusY, в коде выше. В блоке if-else, под строкой 4, добавьте следующий код:

 binding.blurSlider.addOnChangeListener { _, value, _ ->
          //1
          if (value != 0f) {
            //2
            val varyingBlurValue = createBlurEffect(value, value, Shader.TileMode.MIRROR)
            //3
            binding.imageView.setRenderEffect(varyingBlurValue)
          }
        }

В приведенном выше коде:

  1. Проверяем, что значение slider не равно 0. Это предотвращает передачу 0 в createBlurEffect, поскольку он не принимает 0 для X и Y.
  2. Создаем объект и сохраняет его в variableBlurValue.
  3. Применяем размытие к вью.

Если условие истинно, вы передаете в слайдер value для radiusX и radiusY.

Собираем и запускаем. Отмечаем Blur и регулируем его слайдер. Вы увидите что-то вроде этого:

 

Добавление Эффектов Цветового Фильтра

RenderEffect также позволяет добавлять различные эффекты цветовых фильтров. Вы будете использовать фабричный метод createColorFilterEffect для насыщения/обесцвечивания фотографии. Для этого замените TODO() в // TODO 2: Add color effect на:

return RenderEffect.createColorFilterEffect(ColorMatrixColorFilter(
    ColorMatrix().apply {
      setSaturation(saturation)
    }
))

В коде выше вы создаете объект ColorMatrix и настраиваете насыщенность с помощью фабричного метода setSaturation. Далее вы передаете значение, полученное в качестве аргумента, в ColorMatrixColorFilter, которое также передается как аргумент в createColorFilterEffect.

Если вы хотите узнать больше о ColorMatrix, ознакомьтесь с документацией Android по нему.

Далее замените //TODO 5: Add color filter effect следующим кодом, чтобы применить этот эффект к вью:

 //1
      if (isChecked) {
        binding.blurCheck.isClickable = false
        binding.offsetEffectsCheck.isClickable = false
        binding.chainEffectCheck.isClickable = false
        //2
        binding.colorFilterSlider.isEnabled = true
        //3
        val saturation = createSaturationEffect(DEFAULT_SATURATION)
        //4
        binding.imageView.setRenderEffect(saturation)

      } else {
        //5
        binding.imageView.setRenderEffect(null)
        //6
        binding.colorFilterSlider.isEnabled = false

        binding.blurCheck.isClickable = true
        binding.offsetEffectsCheck.isClickable = true
        binding.chainEffectCheck.isClickable = true
      }

Разбираем код выше:

  1. Определяем, установлен ли флажок напротив saturationCheckBox.
  2. Если да, включаем слайдер насыщенности.
  3. Создаем переменную для хранения объекта насыщенности.
  4. Применяем насыщенность к вью.
  5. Если saturationCheckBox не установлен, удаляем фильтр из вью, установив для RenderEffect значение null.
  6. Отключаем слайдер насыщенности.

Собираем и запускаем. Отмечаем Saturation и видим:

 

 

Регулировка Цветового Фильтра

Вы можете настраивать цветовой эффект вью, меняя значение насыщенности в setSaturation в приведенном выше коде. Добавьте сюда следующий код под строкой 4 в условии if.

binding.colorFilterSlider.addOnChangeListener { _, value, _ ->
val varyingSaturationValue = createSaturationEffect(value)
binding.imageView.setRenderEffect(varyingSaturationValue)
}

Собираем и запускаем. Отмечаем Saturation и перемещаем слайдер saturation. Вы увидите что-то вроде этого:

Заметка

Передача значения 0 для setSaturation придает вью оттенки серого цвета, а 1 отображает его в исходный цвет. Установка значения выше 1 увеличивает насыщенность.

 

Использование Эффекта Смещения

Иногда все, что вам нужно, - это возможность сместить картинку. RenderEffect предоставляет статический метод createOffsetEffect, который принимает два аргумента: X и Y. Они смещают содержимое рисунка в горизонтальной и вертикальной плоскостях соответственно. Чтобы реализовать смещение, замените TODO() в // TODO 3: Add offset effect на:

return RenderEffect.createOffsetEffect(offsetX, offsetY)

Далее, нам надо, чтобы смещение применялось, когда offsetCheckbox отмечен. Для этого заменяем //TODO 6: Add offset effect на:

 if (isChecked) {
        binding.blurCheck.isClickable = false
        binding.saturationCheck.isClickable = false
        binding.chainEffectCheck.isClickable = false 
        //1        
        binding.offsetSlider.isEnabled = true 
        //2
        val offsetEffect = createOffsetEffect(DEFAULT_OFFSET, DEFAULT_OFFSET) 
        //3
        binding.imageView.setRenderEffect(offsetEffect)

      } else { 
        //4
        binding.imageView.setRenderEffect(null) 
        //5
        binding.offsetSlider.isEnabled = false

        binding.blurCheck.isClickable = true
        binding.saturationCheck.isClickable = true
        binding.chainEffectCheck.isClickable = true
      }

То, что вы только что сделали, полностью похоже на настройку первых двух эффектов, которую вы сделали ранее.

Соберите и запустите. Отметьте Offset и вы увидите что-то вроде этого:

 

Регулировка Смещения

Вы можете настраивать смещение картинки, меняя значения offsetX и offsetY в приведенном выше коде. Для этого передайте значение в слайдер по мере его настройки пользователем. Поместите следующий код под строкой 3 внутри блока if:

binding.offsetSlider.addOnChangeListener { _, value, _ ->
val varyingOffsetValue = createOffsetEffect(value, value)
binding.imageView.setRenderEffect(varyingOffsetValue)
}

Соберите и запустите. Установите флажок напротив Offset и регулируйте слайдер эффектов смещения. Вы увидите следующее:

 

Применение Эффектов Цепочки

API RenderEffect дает вам возможность комбинировать и применять к вью несколько эффектов, известных как chain effect (эффект цепочки). Вы добавите его, применив к изображению собаки два эффекта: Blur(размытие) и Saturation(насыщенность). Замените // TODO 7: Add chain effect на: 

  //1
    if (blur < 1) {
      //2
      return
    } else {
      //3
      val blurry = createBlurEffect(blur, blur, Shader.TileMode.MIRROR)
      //4
      val saturate = createSaturationEffect(saturation)
      //5
      val chainEffect = RenderEffect.createChainEffect(blurry, saturate)
      //6
      binding.imageView.setRenderEffect(chainEffect)
    }

Разберем этот код:

  1. Проверяет:  blur меньше 1
  2. Если да, то выходит из кода.
  3. Создает размытый объект.
  4. Создает объект насыщенности.
  5. Создает объект с эффектом цепочки, реализуя фабричный метод createChainEffect. Он принимает в качестве аргументов два объекта RenderEffect.
  6. Применяет эффект цепочки к вью.
  7. Удаляет эффект цепочки из вью, если флажки не отмечены.

Теперь, чтобы применить эффект цепочки к view, замените // TODO 8: Add chain effect кодом ниже:

//1
    if (isChecked) {
      binding.blurCheck.isClickable = false
      binding.saturationCheck.isClickable = false
      binding.offsetEffectsCheck.isClickable = false
      //2
      binding.chainEffectSlider.isEnabled = true
      //3
      applyChainEffect(DEFAULT_BLUR, DEFAULT_SATURATION)

    } else {
      //4
      binding.imageView.setRenderEffect(null)
      //5
      binding.chainEffectSlider.isEnabled = false

      binding.blurCheck.isClickable = true
      binding.saturationCheck.isClickable = true
      binding.offsetEffectsCheck.isClickable = true
    }

Пройдемся по приведенному выше коду:

  1. Определяет, отмечен ли параметр chainEffectCheckbox.
  2. Включает слайдер эффекта цепочки, передав ему значение true.
  3. Применяет эффект цепочки, передавая значения по умолчанию для applyChainEffect.
  4. Удаляет эффект из вью, передавая null в setRenderEffect.
  5. Отключает слайдер эффекта цепочки, если не установлен флажок Chain Effect.

Соберите и запустите. Отметьте Chain Effect и вы увидите что-то вроде этого:

Заметка

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

 

Настройка Эффекта Цепочки

Вы можете настроить эффект цепочки, изменяя эффекты, передаваемые в createChainEffect. Для этого поменяйте значения blur и saturation, перемещая слайдер. Добавьте следующий код под строкой //3 в блоке if:

binding.chainEffectSlider.addOnChangeListener { _, value, _ ->
applyChainEffect(value, value)
}

 

Применение Полноэкранного Размытия

Наконец, вы можете размыть весь экран, просто применив эффект рендеринга к корневому вью. Для этого замените // TODO 9: Add blur to full screen на:

//1
if (isChecked) {
//2
val blurScreen = createBlurEffect(DEFAULT_BLUR, DEFAULT_BLUR, Shader.TileMode.MIRROR)
//3
binding.root.setRenderEffect(blurScreen)
} else {
//4
binding.root.setRenderEffect(null)
}

Разберем приведенный выше код:

  1. Определяет, установлен ли флажок напротив Blur Fullscreen.
  2. Создает объект с эффектом размытия.
  3. Применяет размытие к корневому view.
  4. Удаляет размытие из корневого view.

Соберите и запустите. Отметьте Fullscreen. Результат должен выглядеть так:

Оригинал статьи

 

Содержание