Skip to content

Commit

Permalink
Add RenderEffect sample (#17)
Browse files Browse the repository at this point in the history
* Create RenderEffect sample app

* Implement Blur effect

* Implement color filter effect

* Create main screen that lists the different effects the app showcases

* Implement offset effect

* Update Blue effect example to add chips dynamically

* Add sample demos

* Resize the sample demo gifs

* Create README.md
  • Loading branch information
husaynhakeem authored Mar 18, 2021
1 parent d5f6f06 commit 6f24287
Show file tree
Hide file tree
Showing 32 changed files with 1,149 additions and 0 deletions.
15 changes: 15 additions & 0 deletions RenderEffectSample/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
18 changes: 18 additions & 0 deletions RenderEffectSample/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Render effects

Android sample app to learn about the [`RenderEffect`](https://developer.android.com/reference/android/graphics/RenderEffect) API, which was recently introduced in Android 12.

A `RenderEffect` corresponds to a visual effect that can be applied to render a part of the UI, a [`RenderNode`](https://developer.android.com/reference/android/graphics/RenderNode). It's a structure used to record draw operations, and can store and apply render properties when drawn. `RenderNode`s are useful to divide up the rendering of a scene into multiple smaller pieces. This allows updating them individually, which is more optimal/cheaper.

`View`s in Android internally use `RenderNode`s, this allows for hardware accelerated rendering, meaning that UI hierarchies are rendered using the GPU, instead ofthe CPU. A `RenderEffect` can be applied to a View by calling [`View.setRenderEffect(RenderEffect)`](https://developer.android.com/reference/android/view/View#setRenderEffect(android.graphics.RenderEffect)).

![blur-effect](https://github.com/husaynhakeem/android-playground/blob/master/RenderEffectSample/art/blur-effect.gif)
![color-filter-effect](https://github.com/husaynhakeem/android-playground/blob/master/RenderEffectSample/art/color-filter-effect.gif)
![offset-effect](https://github.com/husaynhakeem/android-playground/blob/master/RenderEffectSample/art/offset-effect.gif)

The app mainly showcases:
- Appliying blur effects on a View.
- Applying color filter effects on a View.
- Applying offset effects on a View.

The sample should be run on a device/an emulator running Android 12, since the `RenderEffect` API was only introduced in Android S.
1 change: 1 addition & 0 deletions RenderEffectSample/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
31 changes: 31 additions & 0 deletions RenderEffectSample/app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
}

android {
compileSdkVersion "android-S"
defaultConfig {
applicationId "com.husaynhakeem.rendereffectsample"
minSdkVersion "S"
targetSdkVersion "S"
versionCode 1
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures.viewBinding = true
}

dependencies {
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.4'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.4'
}
23 changes: 23 additions & 0 deletions RenderEffectSample/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.husaynhakeem.rendereffectsample">

<application
android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.RenderEffectSample"
tools:ignore="AllowBackup,MissingApplicationIcon">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.husaynhakeem.rendereffectsample

import android.graphics.RenderEffect
import android.graphics.Shader
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.google.android.material.chip.Chip
import com.google.android.material.slider.Slider
import com.husaynhakeem.rendereffectsample.databinding.FragmentBlurEffectBinding

class BlurEffectFragment : Fragment() {

private lateinit var binding: FragmentBlurEffectBinding
private var radiusX: Float = 0F
private var radiusY: Float = 0F
private var tileMode: Shader.TileMode = Shader.TileMode.MIRROR

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentBlurEffectBinding.inflate(layoutInflater)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

// Set initial values
binding.radiusXSlider.value = radiusX
binding.radiusYSlider.value = radiusY

binding.radiusXSlider.addOnChangeListener(Slider.OnChangeListener { _, value, _ ->
this.radiusX = value
updateEffect()
})

binding.radiusYSlider.addOnChangeListener(Slider.OnChangeListener { _, value, _ ->
this.radiusY = value
updateEffect()
})

// Add all tile modes as chips in the ChipGroup
Shader.TileMode.values()
.forEach { tileMode ->
binding.tileModes.addView(tileMode.toChip(this.tileMode == tileMode))
}
}

private fun Shader.TileMode.toChip(isChecked: Boolean): Chip {
val chip = layoutInflater.inflate(R.layout.choice_chip, null, false) as Chip
chip.text = this.name
chip.gravity = Gravity.CENTER
chip.isChecked = isChecked
chip.setOnClickListener {
this@BlurEffectFragment.tileMode = this
updateEffect()
}
return chip
}

private fun updateEffect() {
val effect = RenderEffect.createBlurEffect(radiusX, radiusY, tileMode)
binding.imageView.setRenderEffect(effect)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.husaynhakeem.rendereffectsample

import android.graphics.BlendMode
import android.graphics.BlendModeColorFilter
import android.graphics.Color
import android.graphics.RenderEffect
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.google.android.material.chip.Chip
import com.google.android.material.slider.Slider
import com.husaynhakeem.rendereffectsample.databinding.FragmentColorFilterEffectBinding

class ColorFilterEffectFragment : Fragment() {

private lateinit var binding: FragmentColorFilterEffectBinding
private var red: Int = 127
private var green: Int = 127
private var blue: Int = 127
private var blendMode: BlendMode = BlendMode.COLOR

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentColorFilterEffectBinding.inflate(layoutInflater)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

// Set sliders initial values
binding.redSlider.value = red.toFloat()
binding.greenSlider.value = green.toFloat()
binding.blueSlider.value = blue.toFloat()
updateRenderEffect()

// Set up listener on color sliders
binding.redSlider.addOnChangeListener(Slider.OnChangeListener { _, value, _ ->
this.red = value.toInt()
updateRenderEffect()
})
binding.greenSlider.addOnChangeListener(Slider.OnChangeListener { _, value, _ ->
this.green = value.toInt()
updateRenderEffect()
})
binding.blueSlider.addOnChangeListener(Slider.OnChangeListener { _, value, _ ->
this.blue = value.toInt()
updateRenderEffect()
})

// Add all blend modes as chips in the ChipGroup
BlendMode.values()
.reversed()
.forEach { blendMode ->
binding.blendModes.addView(blendMode.toChip(this.blendMode == blendMode))
}
}

private fun BlendMode.toChip(isChecked: Boolean): Chip {
val chip = layoutInflater.inflate(R.layout.choice_chip, null, false) as Chip
chip.text = this.name
chip.gravity = Gravity.CENTER
chip.isChecked = isChecked
chip.setOnClickListener {
this@ColorFilterEffectFragment.blendMode = this
updateRenderEffect()
}
return chip
}

private fun updateRenderEffect() {
val color = Color.rgb(red, green, blue)
binding.color.setBackgroundColor(color)

val colorFilter = BlendModeColorFilter(color, blendMode)
val effect = RenderEffect.createColorFilterEffect(colorFilter)
binding.imageView.setRenderEffect(effect)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.husaynhakeem.rendereffectsample

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.husaynhakeem.rendereffectsample

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.husaynhakeem.rendereffectsample.databinding.FragmentMainBinding

class MainFragment : Fragment() {

private lateinit var binding: FragmentMainBinding

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentMainBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

binding.blurEffectButton.setOnClickListener {
findNavController().navigate(R.id.action_mainFragment_to_blurEffectFragment)
}
binding.colorFilterEffectButton.setOnClickListener {
findNavController().navigate(R.id.action_mainFragment_to_colorFilterEffectFragment)
}
binding.offsetEffectButton.setOnClickListener {
findNavController().navigate(R.id.action_mainFragment_to_offsetEffectFragment)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.husaynhakeem.rendereffectsample

import android.graphics.RenderEffect
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import androidx.fragment.app.Fragment
import com.google.android.material.slider.Slider
import com.husaynhakeem.rendereffectsample.databinding.FragmentOffsetEffectBinding

class OffsetEffectFragment : Fragment() {

private lateinit var binding: FragmentOffsetEffectBinding
private var offsetX: Float = 0F
private var offsetY: Float = 0F

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentOffsetEffectBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

// Set up sliders with initial values
binding.offsetXSlider.value = offsetX
binding.offsetYSlider.value = offsetY

// Set up to values for the sliders
binding.imageView
.viewTreeObserver
.addOnGlobalLayoutListener(object :
ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
binding.offsetXSlider.valueFrom = -binding.imageView.width.toFloat()
binding.offsetXSlider.valueTo = binding.imageView.width.toFloat()
binding.offsetYSlider.valueFrom = -binding.imageView.height.toFloat()
binding.offsetYSlider.valueTo = binding.imageView.height.toFloat()
binding.imageView.viewTreeObserver.removeOnGlobalLayoutListener(this)
}
})

// Set up listeners on sliders
binding.offsetXSlider.addOnChangeListener(Slider.OnChangeListener { _, value, _ ->
offsetX = value
updateEffect()
})
binding.offsetYSlider.addOnChangeListener(Slider.OnChangeListener { _, value, _ ->
offsetY = value
updateEffect()
})
}

private fun updateEffect() {
val effect = RenderEffect.createOffsetEffect(offsetX, offsetY)
binding.imageView.setRenderEffect(effect)
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions RenderEffectSample/app/src/main/res/layout/choice_chip.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.chip.Chip xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/Widget.MaterialComponents.Chip.Choice"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
Loading

0 comments on commit 6f24287

Please sign in to comment.