UIKit Apprentice, Second Edition – Now Updated!

Learn iOS and Swift from scratch. Build four powerful apps—with support for iPad and Dark Mode. Publish apps to the App Store.

Home Android & Kotlin Tutorials

RenderEffect in Android 12

Learn how to use the new RenderEffect API in Android 12 to efficiently add custom styles to your views like blurs, saturation, offset, and more.

5/5 2 Ratings

Version

  • Android Studio 4.2

With so many social media and photo-sharing apps, it’s quite common nowadays to apply filters to images before sharing them. Having the ability to do that within the Android OS makes things easier and more efficient. Prior to Android 12, the process was much more complicated, as you had to define RenderNodes by interacting with the Canvas and then apply the effects directly with transformations and algorithms.

In Android 12, however, Google introduced the RenderEffect API. This enables developers to effortlessly apply graphic effects such as blurs, color filters and more to Views.

In this tutorial, you’ll create an app that lets a user apply different graphic effects to a photo. During the process, you’ll:

  • Learn about the RenderEffect API.
  • Explore key concepts like RenderNodes, Display List and Canvas.
  • Learn how to use RenderEffect to implement simple graphic effects like blur, color filters and offset.
  • Use multiple effects to create chain effects.
  • Implement a full-screen blur.
Note: This tutorial assumes you know the basics of Android development. You’ll need the latest stable version of Android Studio or a minimum version of 4.2 to run this project.

If you’re new to Kotlin, check out our Kotlin introduction tutorial. If you’re completely new to Android development, familiarize yourself with our Beginning Android Development tutorials first.

Since Android 12 is still in beta at the time of writing, you can read on how to Set up an Android emulator with an Android 12 system image.

Getting Started

Download the project materials by clicking Download Materials at the top or bottom of this tutorial. Open the starter project in Android Studio.

The app, called Instafilter, allows you to apply graphic effects to a photo of an adorable dog. Right now, it can’t do much, but you’ll add functionality to perform several types of effects soon.

Build and run. You’ll see the following:

Instafilter app with dog photo and various checkboxes and slider bars

The main files in the project are:

  • MainActivity: The entry point of your app. It’s where you’ll write all your code.
  • activity_main.xml: Contains all of the app’s user interface (UI) design.

Adding Effects Before RenderEffect

Adding graphic effects to Views was possible before the introduction of the RenderEffect API. The two main ways to achieve this were:

  • Using third-party libraries like Glide and Blurry.
  • Using the RenderScript API, which involves writing your own algorithm.

Two issues you may have to deal with if using these methods are:

  • Potential random bugs caused by external libraries.
  • Poor performance on certain devices caused by CPU, GPU and memory limitations.

Luckily, RenderEffect solves these issues at the GPU level since the Android OS itself handles the rendering.

Understanding RenderEffect

RenderEffect is an intermediary used to render drawing commands with corresponding visual effects. It allows users to apply effects such as blurs, color filters, shader effects and more to Views and rendering hierarchies. This means the effects are added to Views when they’re drawn on the screen at the GPU level.

It does this by leveraging the existing rendering pipeline to minimize excess calculation, resulting in better performance.

Android uses RenderNodes to render all its Views on the GPU to improve performance. RenderEffect hooks into this implementation to apply effects on the hardware-accelerated rendering pipeline.

Rendering in Android

Android renders views on the GPU, and it uses RenderNodes to build hardware-accelerated rendering hierarchies to ensure better performance. Each RenderNode contains display lists as well as a set of properties that affect the rendering of the display list.

The display list is produced on the main thread and then synced with the RenderThread. The RenderThread issues the display list operations to the GPU, which then renders the view.

Note: The RenderThread only talks to the GPU.

A typical GPU Rendering graph of most apps prior to this API will show lots of inefficiencies:

A visual representation of GPU rendering on Android

To learn more about rendering in Android, check out this video from Google I/O.

Understanding RenderNode

Introduced in API 29, Android uses RenderNodes to build hardware-accelerated rendering hierarchies.

RenderNodes break down complex scenes, rendering content into smaller chunks that can individually be updated more cheaply. Instead of redrawing an entire scene, updating a portion of it requires updating the display list or properties of a small number of RenderNodes. Only when the content of a RenderNode has to be modified does its display list need to be rerecorded.

For example, when a user clicks an item in a RecyclerView, only that item’s display list is updated instead of rerecording the entire RecyclerView’s RenderNode. Another example is a TextView that has different paragraphs in it. Only an updated paragraph’s display list will be updated and the changes applied.

Understanding the Display List

A display list is a structure that stores the rendering information. This means that it stores rendering commands for Views. Graphic commands in Canvas — like drawBackground(), drawDrawable() and drawLine() — all end up as operations in a display list. So the display list is a compact way of representing those operations and the parameters to the operations.

Understanding the Canvas

A Canvas is a 2D drawing surface that provides drawing commands for drawing to a bitmap. The Canvas provides graphic commands/operations like drawBackground(), drawDrawable() and drawText() of views that end up as operations in a display list in the rendering pipeline.

Adding Some Awesome Effects

Enough of the boring theory, it’s now time to apply some effects to your cute dog photo. To add any effects using RenderEffect, you need to follow three main steps:

  1. Create an instance of RenderEffect.
  2. Implement RenderEffect’s factory static method.
  3. Then, set the effect to the appropriate view.

You’ll be working through a bunch of TODOs to complete this tutorial.

Adding Blur

To blur a view, you first need to implement the createBlurEffect static factory method, which RenderEffect provides. Do this by replacing the TODO() under //TODO 1: Add blur effect with:

return RenderEffect.createBlurEffect(radiusX, radiusY, shader)

The code above returns a blur RenderEffect object by implementing the createBlurEffect factory method, which takes three arguments. The first two arguments, radiusX and radiusY, specify the horizontal and vertical radii to which the blur effect should be rendered. Then, the third parameter indicates how to render TileMode at the edges.

Now, replace //TODO 4: Add blur effect with:

      //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
      }

In the code above, here’s what’s happening:

  1. Determine if blurCheckBox is checked.
  2. If it’s checked, enable the blur slider.
  3. Create a blur object by passing in default values for the X and Y radii.
  4. Apply the blur effect to the view.
  5. If blurCheckBox isn’t checked, remove the blur from the view. Removing blur is as easy as setting the RenderEffect to null.
  6. Disable blur slider.
Note: In the code above, all checkboxes except Blur Fullscreen are disabled when Blur is checked, and enabled when it’s unchecked. Throughout the rest of the tutorial, this behavior is repeated for each effect enabled via the checked box.

Build and run. Check Blur, and you’ll see something like this:

Instafilter with Blur box checked and dog image blurred

Adjusting the Blur

You can adjust the blur of a view by varying the values of the radii in the code above. To achieve this, you’ll vary the values of radiusX and radiusY. Within your if-else block, below line 4, add the following code:

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

In the code above:

  1. Checks that the slider value isn’t 0. This prevents passing 0 into createBlurEffect, because it doesn’t take 0 for X and Y.
  2. Creates an object and stores it in varyingBlurValue.
  3. Applies blur to the view.

If the condition is true, you pass in the slider value for radiusX and radiusY.

Build and run. Check Blur and adjust its slider. You’ll see something like this:

Animation of Blur box being checked and the slider moving with the dog picture varying in blurriness

Adding Color Filter Effects

RenderEffect also allows you to add different color filter effects. You’ll use the createColorFilterEffect factory method to saturate/desaturate the photo. To do this, replace the TODO() under //TODO 2: Add color effect with:

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

In the code above, you create a ColorMatrix object and adjust the saturation using the setSaturation factory method. You then pass the value returned as an argument to ColorMatrixColorFilter, which is also passed as an argument to createColorFilterEffect.

If you’d like to know more about the the ColorMatrix, check out Android’s documentation on it.

Next, replace //TODO 5: Add color filter effect with the following code to apply this effect to the view:

      //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
      }

Here’s a breakdown of the code above:

  1. Determine if saturationCheckBox is checked.
  2. If so, enable the saturation slider.
  3. Create a variable to hold the saturation object.
  4. Apply saturation to view.
  5. If saturationCheckBox isn’t checked, remove the filter from the view by setting RenderEffect to null.
  6. Disable the saturation slider.

Build and run. Check Saturation, and you’ll see something like this:

Instafilter with Saturation checked and the dog image has much more vivid colors

Adjusting the Color Filter

You can adjust the color effect of a view by varying the saturation value of setSaturation in the code above. In the code you just added, add the following code below line 4 within the if statement.

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

Build and run. Check Saturation and adjust the saturation slider. You’ll see something like this:

Animation of Saturation box being checked and slider moving with color saturation changing in the dog image

Note: Passing a value of 0 to setSaturation maps the color of the view to grayscale, and 1 maps it to the original color. Adjusting the value above 1 increases the saturation.

Using the Offset Effect

Sometimes, all you need is to be able to offset a view. RenderEffect provides the createOffsetEffect static method, which takes in two arguments: X and Y. These offset the drawing content in the horizontal and vertical planes, respectively. To implement an offset, replace the TODO() under //TODO 3: Add offset effect with:

return RenderEffect.createOffsetEffect(offsetX, offsetY)

Next, apply offset when offsetCheckbox is checked by replacing //TODO 6: Add offset effect with:

      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
      }

What you just did is in every way similar to the first two you did earlier.

Build and run. Check Offset, and you’ll see something like this:

Instafilter with dog photo slightly shifted

Adjusting Offset

You can adjust the offset of a view by varying the values of offsetX and offsetY in the code above. To achieve this, pass in the slider value as the user adjusts it. Place the following code below line 3 within the if block:

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

Build and run. Check Offset and adjust the offset effects slider. You’ll see the following:

Animation of Offset being checked and the slider bar moving with the dog photo moving downward and to the right

Applying Chain Effects

The RenderEffect API gives you the ability to combine and apply multiple effects to Views, known as the chain effect. You’ll add a chain effect by applying two effects to the dog image: Blur and Saturation. Replace //TODO 7: Add chain effect with:

    //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)
    }

Here's a breakdown of the code above:

  1. Checks if blur is less than 1. If it is, exit the code.
  2. Creates a blur object.
  3. Creates a saturation object.
  4. Produces a chain effect object by implementing the factory method createChainEffect. This takes in two RenderEffect objects as arguments.
  5. Applies the chain effect to the view.
  6. Removes the chain effect from the view if the checkboxes are unchecked.

Now, to apply chain effect to the view, replace //TODO 8: Add chain effect with the code below:

    //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
    }

Here's a walkthrough of the code above:

  1. Determines if chainEffectCheckbox is checked.
  2. Enables chain effect slider by setting it to true.
  3. Applies chain effect by passing in default values to applyChainEffect.
  4. Removes effect from view by passing null to setRenderEffect.
  5. Disables chain effect slider if Chain Effect is unchecked.

Build and run. Check Chain Effect, and you'll see something like this:

Instafilter with dog image blurred and saturated with color

Note: The order in which arguments are passed into createChainEffect could produce slightly different results on the view. Try changing the order of arguments, and compare the results.

Adjusting the Chain Effect

You can adjust the chain effect of a view by varying the effects passed into createChainEffect. To achieve this, vary the blur and saturation values by moving the slider. Add the following code below line 3 within the if block:

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

Animation of Chain Effect being checked and the slider adjusted with the dog photo getting blurrier and more color saturated

Applying Fullscreen Blur

Finally, you can blur the entire screen by simply applying the render effect to the root view. To achieve this, replace //TODO 9: Add blur to full screen with:

//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)
}

Here's a breakdown of the code above:

  1. Determines if Blur Fullscreen is checked.
  2. Creates a blur effect object.
  3. Applies blur to root view.
  4. Removes blur from root view.

Build and run. Check Fullscreen, and the result should look like this:

Instafilter with dog photo and all controls blurred

Where to Go From Here?

You can download the completed version of the project using Download Materials at the top or bottom of this tutorial.

Congratulations! You now have a better understanding of the RenderEffect API. Just think of all the amazing things you can do with the knowledge you've gained while working through this tutorial. ;]

This is just the tip of the iceberg in terms of what you can achieve with RenderEffect. To learn more, head over to Android's documentation on it.

Also, check out Mark Allison's blog for more content on this topic.

For simplified explanations on rendering and graphics in Android, check out Android Graphics Pipeline: From Button to Framebuffer (Part 1), Android Graphics Pipeline: From Button to Framebuffer (Part 2) and Android rendering mechanism-Display List.

I hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below.

Average Rating

5/5

Add a rating for this content

2 ratings

More like this

Contributors

Comments