1. Welcome
Introduction
In computer graphics, a shader is a type of computer program that was originally used for shading (the production of appropriate levels of light, darkness, and color within an image), but which now performs a variety of specialized functions in various fields of computer graphics special effects.
In Android, Shader
defines the color(s) or the texture with which the Paint
object should draw (other than a bitmap). Android defines several subclasses of Shader
for Paint
to use, such as BitmapShader
, ComposeShader
, LinearGradient
, RadialGradient
, and SweepGradient
.
For example, you can use a BitmapShader
to define a bitmap as a texture to the Paint
object. This allows you to implement custom themes with translucent effects, and custom views with a bitmap as a texture. You can also use masks with transition animations to create impressive visual effects. To draw images in different shapes (rounded rectangle in this example), you can define a BitmapShader
for your Paint
object and use the drawRoundRect()
method to draw a rectangle with rounded corners, as shown below.
In this codelab, you will use a BitmapShader
to set a bitmap as a texture to the Paint
object, instead of a simple color.
What you should already know
You should be familiar with:
- Creating a custom
View
. - Drawing on a
Canvas
. - Adding event handlers to views.
What you'll learn
- How to set a
Shader
for aPaint
and use it to modify what is being drawn.
What you'll do
- Create a simple game with a spotlight to find a hidden Android image.
2. App overview
The FindMe app lets you search for an Android image on a dark phone screen using a "spotlight."
- At app startup, a dialog with instructions on how to play with a play
button is displayed.
- Once the play button has been clicked, a black screen is displayed.
- The player has to touch and hold the screen to reveal the "spotlight" (white circle).
- While the user drags their finger, the white circle follows the touch.
- When the white circle overlaps with the hidden Android image, and the user lifts their finger, the screen lights up to reveal the complete image.
- When the user touches the screen again, the screen turns black and the Android image is hidden in a new random location.
The following screenshots show the FindMe app at startup, when the user is dragging their finger, and after the user has found the Android image by moving around the spotlight.
Additional features:
- Spotlight is not centered under the finger, so that the user can see what's inside the circle.
3. Task: Setting up
In this task, you create the FindMe app from scratch, so you will set up a project and define some strings.
Step: Create the FindMe project
- Open Android Studio.
- Create a new Kotlin project called FindMe that uses the Empty Activity template.
- Open
strings.xml
and add the following strings for the app title and game instructions.
<resources>
<string name="app_name">Find the Android</string>
<string name="instructions_title">
<b>How to play:</b>
</string>
<string name="instructions">
\t \u2022 Find the Android hidden behind the dark surface. \n
\t \u2022 Touch and hold the screen for the spotlight. \n
\t \u2022 Once you find the Android, lift your finger to end the game. \n \n
\t \u2022 To restart the game touch the screen again.
</string>
</resources>
- Get the image of an Android on a skateboard from GitHub and add it to your drawable folder. Alternatively, you can use a small image of your own choice with dimensions close to 120 X 120 pixels, named
android
, and add it to your drawable folder.
- Download the mask image and add it to your drawable folder. You will be using this image for masking the spotlight. You will learn about masking later in this codelab.
4. Task: Creating a custom ImageView
In this task, you create a custom ImageView
, SpotLightImageView
, and declare some helper variables. This class is where game play takes place. SpotLightImageView
- responds to motion events on the screen.
- draws the game screen, with the spotlight at the current position of the user's finger.
- displays the Android image when winning conditions are met.
Step: Create the SpotLightImageView class
- Create a new Kotlin class called
SpotLightImageView
. - Extend the
SpotLightImageView
class fromAppCompatImageView
. Importandroidx.appcompat.widget.AppCompatImageView
when prompted.
class SpotLightImageView : AppCompatImageView {
}
- Click on
AppCompatImageView
, and then click the red bulb. Choose Add Android View constructors using ‘@JvmOverloads'. The@JvmOverloads
annotation instructs the Kotlin compiler to generate one additional overload for every parameter with a default value, which has this parameter and all parameters to the right of it in the parameter list removed.
After Android Studio adds the constructor from the AppCompatImageView
class, the generated code should look like the code below.
class SpotLightImageView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {
}
- In
SpotLightImageView
, declare and define the following class variables.
private var paint = Paint()
private var shouldDrawSpotLight = false
private var gameOver = false
private lateinit var winnerRect: RectF
private var androidBitmapX = 0f
private var androidBitmapY = 0f
- In
SpotLightImageView
, create and initialize variables for the Android image and the mask.
private val bitmapAndroid = BitmapFactory.decodeResource(
resources,
R.drawable.android
)
private val spotlight = BitmapFactory.decodeResource(resources, R.drawable.mask)
|
|
- Open
activity_main.xml
. (In Android Studio 4.0 and later, click the Spliticon in the top-right corner to show both the XML code and the preview pane.)
- Replace the Hello world! text view with the custom view,
SpotLightImageView
, as shown in the code below. Make sure your package name and custom view name match.
<com.example.android.findme.SpotLightImageView
android:id="@+id/spotLightImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
- Run your app. Notice a blank white screen, because you are not drawing anything in the
SpotLightImageView
yet.
5. Concept: Shaders
A Shader
defines the texture for a Paint
object. A subclass of Shader
is installed in a Paint
by calling paint.setShader(shader)
. After that, any object (other than a bitmap) that is drawn with that Paint
will get its color(s) from the shader.
Android provides the following subclasses of Shader
for Paint
to use:
LinearGradient
draws a linear gradient using two or more given colors.
RadialGradient
draws a radial gradient using the given (two or more) colors, the center, and the radius. The colors are distributed between the center and edge of the circle.
SweepGradient
, draws a sweeping gradient around a center point with the specified colors.
ComposeShader
is a composition of two shaders.ComposeShader
and composing modes are beyond the scope of this codelab, please read the ComposeShader documentation for further learning.BitmapShader
draws a bitmap drawable as a texture. The bitmap can be repeated or mirrored by setting the TileMode mode. You will learn more aboutBitmapShader
and TileMode later in this codelab.
Concept: PorterDuff.Mode
The PorterDuff.Mode
class provides several Alpha compositing and blending modes. Alpha compositing is the process of compositing (or combining) a source image with a destination image to create the appearance of partial or full transparency. Transparency is defined by the alpha channel. The alpha channel represents the degree of transparency of a color, that is, for its red, green and blue channels.
To learn about blending modes, see the blending modes documentation.
For example consider the following source and destination images.
|
|
Here is a table defining some of the Alpha compositing modes:
Result of composting | |
|
|
|
|
|
|
|
|
You will use the DST_OUT
PorterDuff Mode to invert the mask asset to be a black rectangle with a spotlight. To learn more about the other modes, refer to the Alpha compositing modes documentation.
6. Task: Creating the BitmapShader
In this task, you create a texture using a mask bitmap for the shader to use.
- Create a bitmap of the same size as the spotlight (mask) bitmap.
- Create a canvas with that bitmap for drawing on the bitmap. Color the background black.
- Create the texture using the
BitmapShader
. - Create the spotlight; composite (combine) the new bitmap with the spotlight (mask) bitmap you defined earlier using the
PorterDuff.Mode
. - Fill the entire screen with the texture. The created texture bitmap is smaller than the screen, so use the CLAMP TileMode to draw the spotlight once and fill in the rest of the screen with black.
Step 1: Create the destination bitmap
- In
SpotLightImageView
, add aninit
block. - Inside the
init
block, create a bitmap of the same size as the spotlight bitmap you created from the mask image, usingcreateBitmap()
.
init {
val bitmap = Bitmap.createBitmap(spotlight.width, spotlight.height, Bitmap.Config.ARGB_8888)
}
- At the end of the
init
block, create and initialize aCanvas
object with the new bitmap. - Below, create and initialize a
Paint
object.
val canvas = Canvas(bitmap)
val shaderPaint = Paint(Paint.ANTI_ALIAS_FLAG)
- Create the bitmap texture and color the bitmap black. In a later step, you will create the spotlight effect by compositing the mask image you downloaded earlier. Draw a black rectangle of the same size as the spotlight bitmap.
// Draw a black rectangle.
shaderPaint.color = Color.BLACK
canvas.drawRect(0.0f, 0.0f, spotlight.width.toFloat(), spotlight.height.toFloat(), shaderPaint)
Destination
Step 2: Mask out the spotlight from the black rectangle
- At the end of the
init
block, use theDST_OUT
compositing mode to mask out the spotlight from the black rectangle.
shaderPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)
canvas.drawBitmap(spotlight, 0.0f, 0.0f, shaderPaint)
Result texture
Step 3: Create the BitmapShader
TileMode
The tiling mode, TileMode
, defined in the Shader
, specifies how the bitmap drawable is repeated or mirrored in the X and Y directions if the bitmap drawable being used for texture is smaller than the screen. Android provides three different ways to repeat (tile) the bitmap drawable (texture):
- REPEAT : Repeats the bitmap shader's image horizontally and vertically.
- CLAMP : The edge colors will be used to fill the extra space outside of the shader's image bounds.
- MIRROR : The shader's image is mirrored horizontally and vertically.
Sample shader image:
Example outputs for the different tilemodes:
|
|
|
- In
SpotLightImageView
, declare aprivate lateinit
class variable of typeShader
.
private var shader: Shader
- At the end of the
init
block, create a bitmap shader using the constructor, BitmapShader(). Pass in the texture bitmap,bitmap
, and the tiling mode asCLAMP
. Clamp tilemode will draw everything outside of the circle with the edge color of the texture, which in this case is black.
shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
The created shader
with the CLAMP
tilemode looks similar to the image below, and the edge color, black, is drawn to fill the remaining space.
- Add the
shader
you created above to thepaint
object. Theshader
contains the texture and instructions (like tiling) on how to apply the texture,.
paint.shader = shader
- The completed
init
block should now look like the code below.
init {
val bitmap = Bitmap.createBitmap(spotlight.width, spotlight.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val shaderPaint = Paint(Paint.ANTI_ALIAS_FLAG)
// Draw a black rectangle.
shaderPaint.color = Color.BLACK
canvas.drawRect(0.0f, 0.0f, spotlight.width.toFloat(), spotlight.height.toFloat(), shaderPaint)
// Use the DST_OUT compositing mode to mask out the spotlight from the black rectangle.
shaderPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)
canvas.drawBitmap(spotlight, 0.0f, 0.0f, shaderPaint)
shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
paint.shader = shader
}
Your bitmap shader is ready to use, and you will learn how to use it in a later task.
7. Task (optional): Draw the BitmapShader texture
In this task you will update the app so you can see the texture you created in the previous task. You will also experiment with tiling modes. This task is optional, and code you add in this task should be reverted or commented out when you are done.
Step 1: Draw the texture
- In
SpotLightImageView
, override theonDraw()
method. - In
onDraw()
, color the background yellow using thecanvas
. - Draw a rectangle of the same size as the texture using the
Paint
object with theShader
. This should draw the texture once.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Color the background yellow.
canvas.drawColor(Color.YELLOW)
canvas.drawRect(0.0f, 0.0f,spotlight.width.toFloat(), spotlight.height.toFloat(), paint)
}
- Run the app. Notice how the bitmap texture you created is drawn in the top left corner of the screen with a yellow background.
Step 2: Experiment with tiling modes
If the size of the object being drawn (like the rectangle in the above step) is larger than the texture, which is usually the case. You can tile the bitmap texture in different ways - CLAMP, REPEAT, and MIRROR. The tiling mode for the shader you created in the previous task is CLAMP, since you only want to draw the spotlight once and fill in the rest with black.
To see the shader you created in action:
- In
SpotLightImageView
, inside theonDraw()
method, update thedrawRect()
method call. Draw the rectangle of the size of the screen.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Color the background Yellow.
canvas.drawColor(Color.YELLOW)
// canvas.drawRect(0.0f, 0.0f,spotlight.width.toFloat(), spotlight.height.toFloat(), paint)
canvas.drawRect(0.0f, 0.0f, width.toFloat(), height.toFloat(), paint)
}
- Run the app. Notice that the spotlight texture is drawn only once, and the rest of the rectangle is filled with the edge color, black.
- Experiment with different tiling modes in X and Y directions.
|
|
|
Step 3: Translate Shader matrix
In this step you learn how to translate the texture (shader) to any location on the screen and draw it.
Matrix translation
When the user touches and holds the screen for the spotlight, instead of calculating where the spotlight needs to be drawn, you move the shader matrix; that is, the texture/shader coordinate system, and then draw the texture (the spotlight) at the same location in the translated coordinate system. The resulting effect will seem as if you are drawing the spotlight texture at a different location, which is the same as the shader matrix translated location. This is simpler and slightly more efficient.
- In
SpotLightImageView
, create a class variable of typeMatrix
and initialize it.
private val shaderMatrix = Matrix()
- Update the
BitmapShader
in theinit
block. Change the tile mode toCLAMP
in the X and Y directions. This will draw the texture only once, which makes observing theShader
matrix translation straightforward.
shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
- Inside the
onDraw()
method, before the call todrawRect()
, translate the shader matrix to random X and Y values. - Set the shader's local matrix to translated
shaderMatrix
.
shaderMatrix.setTranslate(
100f,
550f
)
shader.setLocalMatrix(shaderMatrix)
- Draw some arbitrary shape using the
paint
you defined previously. This code will draw a rectangle of the size of half the screen.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawColor(Color.YELLOW)
shaderMatrix.setTranslate(
100f,
550f
)
shader.setLocalMatrix(shaderMatrix)
canvas.drawRect(0.0f, 0.0f, width.toFloat(), height.toFloat()/2, paint)
}
- Run your app. Try experimenting by translating the shader matrix to different locations, changing the tile modes, and by drawing different shapes and sizes with the
paint
. Below are some effects you might create.
- Revert or comment out the code changes made in this task.
8. Task: Calculate a random location for the Android Image
In this task, you will calculate a random location on the screen for the Android image bitmap that the player has to find. You also need a way to calculate whether the user has found the bitmap.
- In
SpotLightImageView
, add a newprivate
method calledsetupWinnerRect()
. - Set
androidBitmapX
andandroidBitmapY
to random x and y positions that fall inside the screen. Importkotlin.math.floor
andkotlin.random.Random
, when prompted. - At the end of
setupWinnerRect()
, initializewinnerRect
with a rectangular bounding box that contains the Android image.
private fun setupWinnerRect() {
androidBitmapX = floor(Random.nextFloat() * (width - bitmapAndroid.width))
androidBitmapY = floor(Random.nextFloat() * (height - bitmapAndroid.height))
winnerRect = RectF(
(androidBitmapX),
(androidBitmapY),
(androidBitmapX + bitmapAndroid.width),
(androidBitmapY + bitmapAndroid.height)
)
}
9. Task : Use the BitmapShader
In this task, you will override and implement onSizeChanged()
and onDraw()
in SpotLightImageView
.
Step 1: Override onSizeChanged()
- In
SpotLightImageView
, overrideonSizeChanged()
. CallsetupWinnerRect()
from it.
override fun onSizeChanged(
newWidth: Int,
newHeight: Int,
oldWidth: Int,
oldHeight: Int
) {
super.onSizeChanged(newWidth, newHeight, oldWidth, oldHeight)
setupWinnerRect()
}
Step 2: Override onDraw()
In this step you will draw the Android image on a white background, using the
Paint
object with the bitmap shader.
- In
SpotLightImageView
, overrideonDraw()
. - In
onDraw()
, remove the code for showing the texture that you added in a previous task. - In
onDraw()
, color the canvas white and draw the Android image at the random positionsandroidBitmapX
,androidBitmapY
that you calculated inonSizeChanged()
.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawColor(Color.WHITE)
canvas.drawBitmap(bitmapAndroid, androidBitmapX, androidBitmapY, paint)
}
- Run the app. Change the device/emulator screen orientation to restart the activity. Every time the activity restarts, the Android image is displayed in a different random position.
10. Task: Responding to motion events
For the game to work, your app needs to detect and respond to the user's motions on the screen. You will translate the spotlight shader matrix in response to user touch events, so that the spotlight follows the user touch.
Step 1: Override and implement the onTouchEvent() method
- In
SpotLightImageView
, override theonTouchEvent()
method. CreatemotionEventX
andmotionEventY
to store the user's touch coordinates. - You need to return a boolean. Since you are handling the motion event, have the method return
true
.
override fun onTouchEvent(motionEvent: MotionEvent): Boolean {
val motionEventX = motionEvent.x
val motionEventY = motionEvent.y
return true
}
- Just before the
return
statement, add awhen
block onmotionEvent.
action
. Add case blocks forMotionEvent.
ACTION_DOWN
andMotionEvent.
ACTION_UP
.
when (motionEvent.action) {
MotionEvent.ACTION_DOWN -> {
}
MotionEvent.ACTION_UP -> {
}
}
- Inside the
MotionEvent.
ACTION_DOWN -> {}
case, setshouldDrawSpotLight
totrue
. Check ifgameOver
is true, and if so, reset it to false and callsetupWinnerRect()
to restart the game. - Inside the
MotionEvent.
ACTION_UP -> {}
case, setshouldDrawSpotLight
tofalse
. Check whether the spotlight center is inside the winning rectangle.
when (motionEvent.action) {
MotionEvent.ACTION_DOWN -> {
shouldDrawSpotLight = true
if (gameOver) {
gameOver = false
setupWinnerRect()
}
}
MotionEvent.ACTION_UP -> {
shouldDrawSpotLight = false
gameOver = winnerRect.contains(motionEventX, motionEventY)
}
}
Step 2: Translate the shader matrix
Matrix translation (refresher)
When the user touches and holds the screen for the spotlight, instead of calculating where the spotlight needs to be drawn, you move the shader matrix.; that is, the texture/shader coordinate system, and then draw the spotlight texture at the same location in the translated coordinate system. The resulting effect appears as if you are drawing the spotlight texture at a different location, which is same as the shader matrix translated location.
- In
SpotLightImageView
, add a new variable to save the shader matrix. Importandroid.graphics.Matrix
, when prompted.
private val shaderMatrix = Matrix()
- At the end of the
onTouchEvent()
method, before thereturn
statement, translate theshaderMatrix
to the new position based on the user touch event.
shaderMatrix.setTranslate(
motionEventX - spotlight.width / 2.0f,
motionEventY - spotlight.height / 2.0f
)
- Set the shader's local matrix to the new
shaderMatrix
.
shader.setLocalMatrix(shaderMatrix)
- Call
invalidate()
to trigger a call toonDraw()
, which redraws the shader in the new position. - The complete method should look like this:
override fun onTouchEvent(motionEvent: MotionEvent): Boolean {
val motionEventX = motionEvent.x
val motionEventY = motionEvent.y
when (motionEvent.action) {
MotionEvent.ACTION_DOWN -> {
shouldDrawSpotLight = true
if (gameOver) {
// New Game
gameOver = false
setupWinnerRect()
}
}
MotionEvent.ACTION_UP -> {
shouldDrawSpotLight = false
gameOver = winnerRect.contains(motionEventX, motionEventY)
}
}
shaderMatrix.setTranslate(
motionEventX - spotlight.width / 2.0f,
motionEventY - spotlight.height / 2.0f
)
shader.setLocalMatrix(shaderMatrix)
invalidate()
return true
}
- Run your app. Notice the Android image on the white background. Your game app is almost ready. The only implementation missing is the black screen with the spotlight.
- Click on the Android image, to simulate that you found the Android and won and the game.
- Click elsewhere on the screen to restart the game, and now the Android image will be displayed in a different random location.
11. Task: Use the BitmapShader
In this task, you will draw a full-screen dark rectangle with the spotlight using the BitmapShader
with the texture you created.
- At the end of the
onDraw()
method, check ifgameOver
is false. If so, check ifshouldDrawSpotLight
istrue
, and if so, draw the full screen rectangle using the paint object with the updated bitmap shader. - If
shouldDrawSpotLight
isfalse
, color the canvas black. - The complete method should look like this:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawColor(Color.WHITE)
canvas.drawBitmap(bitmapAndroid, androidBitmapX, androidBitmapY, paint)
if (!gameOver) {
if (shouldDrawSpotLight) {
canvas.drawRect(0.0f, 0.0f, width.toFloat(), height.toFloat(), paint)
} else {
canvas.drawColor(Color.BLACK)
}
}
}
- Run your app and GAME ON!
- After you win, tap the screen to play again.
12. Task: Add an instructions dialog
In this task, you will add an alert dialog with instructions on how to play the game.
- In
MainActivity
, add a methodcreateInstructionsDialog()
to create anAlertDialog
. Importandroidx.appcompat.app.AlertDialog
, when prompted.
private fun createInstructionsDialog(): Dialog {
val builder = AlertDialog.Builder(this)
builder.setIcon(R.drawable.android)
.setTitle(R.string.instructions_title)
.setMessage(R.string.instructions)
.setPositiveButtonIcon(ContextCompat.getDrawable(this, android.R.drawable.ic_media_play))
return builder.create()
}
- In
MainActivity
, at the end of theonCreate()
method, display the alert dialog.
val dialog = createInstructionsDialog()
dialog.show()
- Run your app. You should see a dialog with instructions and a play button. Tap the play button and play the game.
13. Solution code
Download the code for the finished codelab.
$ git clone https://github.com/googlecodelabs/android-drawing-shaders
Alternatively you can download the repository as a Zip file, unzip it, and open it in Android Studio.
14. Summary
- In Android,
Shader
defines the color(s) or the texture with which thePaint
object should draw (other than a bitmap). - Android defines several subclasses of
Shader
forPaint
to use, such asBitmapShader
,ComposeShader
,LinearGradient
,RadialGradient
,SweepGradient
. - A
Shader
defines the content for aPaint
object which should be drawn. A subclass ofShader
is installed in aPaint
by callingpaint.setShader(shader)
. - Alpha compositing is the process of compositing (or combining) a source image with a destination image to create the appearance of partial or full transparency. The amount of transparency is defined by the alpha channel.
BitmapShader
draws a bitmap drawable as a texture. The bitmap can be repeated or mirrored by setting the TileMode mode.- The tiling mode,
TileMode
, defined in theShader
, specifies how the bitmap drawable is repeated in the X and Y directiona. Android provides three ways to repeat the bitmap drawable:REPEAT
,CLAMP
,MIRROR
.
15. Learn more
Udacity course:
Android developer documentation:
16. Next codelab
For links to other codelabs in this course, see the Advanced Android in Kotlin codelabs landing page.