Advanced Android in Kotlin 02.3: Clipping Canvas Objects

1. Welcome

Introduction

For the purpose of this codelab, clipping is a way to define regions of an image, canvas, or bitmap that are selectively drawn or not drawn onto the screen. One purpose of clipping is to reduce overdraw. Overdraw is when a pixel on the screen is drawn more than once to display the final image. When you reduce overdraw, you minimize the number of times a pixel or region of the display is drawn, in order to maximize drawing performance. You can also use clipping to create interesting effects in user interface design and animation.

For example, when you draw a stack of overlapping cards as shown below, instead of fully drawing each card from the bottom up, it is usually more efficient to only draw the visible portions. "Usually", because clipping operations also have a cost, and overall, the Android system does a lot of drawing optimization.

d035f17d8befef1c.png

To only draw the visible portions of the cards, you specify a clipping region for each card. For example in the diagram below, when a clipping rectangle is applied to an image, only the portion inside that rectangle is displayed.

cada0beef5b7d77f.png

The clipping region is commonly a rectangle, but it can be any shape or combination of shapes, even text. You can also specify whether you want the region inside the clipping region included or excluded. For example, you could create a circular clipping region and only display what's outside the circle.

In this codelab, you are going to experiment with various ways of clipping.

What you should already know

You should be familiar with:

  • How to create an app with an Activity and run it using Android Studio.
  • How to create and draw on a Canvas.
  • How to create a custom View, and override onDraw() and onSizeChanged().

What you'll learn

  • How to clip objects to draw on a Canvas.
  • How to save and restore drawing states of a canvas.
  • How to apply transformations to a canvas and to text.

What you'll do

  • Create an app that draws clipped shapes on the screen demonstrating different ways of clipping and its result on the visibility of those shapes.
  • You will also draw some translated and skewed text.

2. App overview

The ClippingExample app demonstrates how you can use and combine shapes to specify which portions of a canvas are displayed in a view. Your final app will look like the screenshot below.

f78afb757ff8aac1.png

3. Task: Project and shapes setup

You are going to build this app from scratch, so you will have to set up a project, define dimensions and strings, and declare some variables.

Step 1: Create the ClippingExample project

  1. Create a Kotlin project called ClippingExample with the Empty Activity template. Use com.example.android for the package name prefix.
  2. Open MainActivity.kt.
  3. In the onCreate() method, replace the default content view and set the content view to a new instance of ClippedView. This will be your custom view for the clipping examples that you will create next.
setContentView(ClippedView(this))
  1. At the same level as MainActivity.kt, create a new Kotlin file and class for a custom view called ClippedView that extends View. Give it the signature shown below. The rest of your work will all be inside this ClippedView. The @JvmOverloads annotation instructs the Kotlin compiler to generate overloads for this function that substitute default parameter values.
class ClippedView @JvmOverloads constructor(
   context: Context,
   attrs: AttributeSet? = null,
   defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
}

Step 2: Add dimensions and string resources

  1. Define the dimensions that you'll be using for the clipped views in a new resources file in res/values/dimens.xml. These default dimensions are hardcoded and sized to fit on a pretty small screen.
<?xml version="1.0" encoding="utf-8"?>
<resources>
   <dimen name="clipRectRight">90dp</dimen>
   <dimen name="clipRectBottom">90dp</dimen>
   <dimen name="clipRectTop">0dp</dimen>
   <dimen name="clipRectLeft">0dp</dimen>

   <dimen name="rectInset">8dp</dimen>
   <dimen name="smallRectOffset">40dp</dimen>

   <dimen name="circleRadius">30dp</dimen>
   <dimen name="textOffset">20dp</dimen>
   <dimen name="strokeWidth">4dp</dimen>

   <dimen name="textSize">18sp</dimen>
</resources>

For the app to look good on a bigger screen (and to more easily see details), you can create a dimens file with bigger values that only applies to bigger screens.

  1. In Android Studio, right-click on the values folder and choose New > Values resource file.
  2. In the New Resource File dialog, call the file dimens. In Available qualifiers, select Smallest Screen Width and click the >> button to add it to the Chosen qualifiers. Enter 480 into the Smallest screen width box and click OK.

62830dac2615b30a.png

  1. The file should show in your values folder as shown below.

3f74d4b27dfc4900.png

  1. If you can't see the file, switch to the Project Files view of the app. The full path of the new file is as shown below: ClippingExample/app/src/main/res/values-sw480dp/dimens.xml.

cb8b77158fb11980.png

  1. Replace the default contents of the values-sw480dp/dimens.xml file with the dimensions below.
<?xml version="1.0" encoding="utf-8"?>
<resources>
   <dimen name="clipRectRight">120dp</dimen>
   <dimen name="clipRectBottom">120dp</dimen>

   <dimen name="rectInset">10dp</dimen>
   <dimen name="smallRectOffset">50dp</dimen>

   <dimen name="circleRadius">40dp</dimen>
   <dimen name="textOffset">25dp</dimen>
   <dimen name="strokeWidth">6dp</dimen>
</resources>
  1. In strings.xml, add the following strings. These will be used to display text on the canvas.
<string name="clipping">Clipping</string>
<string name="translated">translated text</string>
<string name="skewed">"Skewed and "</string>

Step 3: Create and initialize a Paint and a Path object

  1. Switch back to the Android view of your project.
  2. In ClippedView define a Paint variable to draw with. Enable anti-aliasing, and use the stroke width and text size defined in the dimensions, as shown below.
private val paint = Paint().apply {
   // Smooth out edges of what is drawn without affecting shape.
   isAntiAlias = true
   strokeWidth = resources.getDimension(R.dimen.strokeWidth)
   textSize = resources.getDimension(R.dimen.textSize)
}
  1. In ClippedView, create and initialize a Path to store locally the path of what has been drawn. Import android.graphics.Path.
private val path = Path()

Step 4: Set up the shapes

In this app, you are displaying several rows and two columns of shapes clipped in various ways.

They all have in common:

  • A large rectangle (square) that acts as a container
  • A diagonal line across the large rectangle
  • A circle
  • A short string of text

15e11763cdd54e57.png

In this step you set up dimensions for those shapes from resources, so that you only have to get the dimensions once when you use them later.

  1. In ClippedView, below the path, add variables for dimensions for a clipping rectangle around the whole set of shapes.
private val clipRectRight = resources.getDimension(R.dimen.clipRectRight)
private val clipRectBottom = resources.getDimension(R.dimen.clipRectBottom)
private val clipRectTop = resources.getDimension(R.dimen.clipRectTop)
private val clipRectLeft = resources.getDimension(R.dimen.clipRectLeft)
  1. Add variables for the inset of a rectangle and the offset of a small rectangle.
private val rectInset = resources.getDimension(R.dimen.rectInset)
private val smallRectOffset = resources.getDimension(R.dimen.smallRectOffset)
  1. Add a variable for the radius of a circle. This is the radius of the circle drawn inside the rectangle.
private val circleRadius = resources.getDimension(R.dimen.circleRadius)
  1. Add an offset and a text size for text that is drawn inside the rectangle.
private val textOffset = resources.getDimension(R.dimen.textOffset)
private val textSize = resources.getDimension(R.dimen.textSize)

Step 4: Set up row and column locations

The shapes for this app are displayed in two columns and four rows, determined by the values of the dimensions set up above. The math for this is not part of this codelab, but take a look at it as you copy to code given in this step.

  1. Set up the coordinates for two columns.
private val columnOne = rectInset
private val columnTwo = columnOne + rectInset + clipRectRight
  1. Add the coordinates for each row, including the final row for the transformed text.
private val rowOne = rectInset
private val rowTwo = rowOne + rectInset + clipRectBottom
private val rowThree = rowTwo + rectInset + clipRectBottom
private val rowFour = rowThree + rectInset + clipRectBottom
private val textRow = rowFour + (1.5f * clipRectBottom)
  1. Run your app. The app should open with a blank white screen below the name of the app.

8206d53018fee7d4.png

4. Task: Understanding the drawing algorithm

In onDraw(), you call methods to draw seven different clipped rectangles as shown in the app screenshot below. The rectangles are all drawn in the same way; the only difference is their defined clipping regions and location on the screen.

f78afb757ff8aac1.png

The algorithm used to draw the rectangles works as shown in the diagram and explanation below. In summary, you draw a series of rectangles by moving the origin of the Canvas. Conceptually, this consists of the following steps:

ad97c62423885613.png

(1) First, you translate Canvas to where you want the rectangle to be drawn. That is, instead of calculating where the next rectangle and all the other shapes need to be drawn, you move the Canvas origin, that is, its coordinate system.

(2) Then, you draw the rectangle at the new origin of the canvas. That is, you draw the shapes at the same location in the translated coordinate system. This is a lot simpler and slightly more efficient.

(3) Finally, you restore the Canvas to its original Origin.

Here is the algorithm as you will implement it:

  1. In onDraw(), call a function to fill the Canvas with the gray background color and draw the original shapes.
  2. Call a function for each clipped rectangle and the text to draw.

For each rectangle or text:

  1. Save the current state of the Canvas so you can reset to that initial state.
  2. Translate the Origin of the canvas to the location where you want to draw.
  3. Apply clipping shapes and paths.
  4. Draw the rectangle or text.
  5. Restore the state of the Canvas.

Step: Override onDraw()

  1. Override onDraw() as shown in the code below. You call a function for each shape you are drawing, which you'll implement later.
 override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        drawBackAndUnclippedRectangle(canvas)
        drawDifferenceClippingExample(canvas)
        drawCircularClippingExample(canvas)
        drawIntersectionClippingExample(canvas)
        drawCombinedClippingExample(canvas)
        drawRoundedRectangleClippingExample(canvas)
        drawOutsideClippingExample(canvas)
        drawSkewedTextExample(canvas)
        drawTranslatedTextExample(canvas)
        // drawQuickRejectExample(canvas)
    }
  1. Create stubs for each of the drawing functions so that the code will continue to compile. You can copy the code below.
private fun drawBackAndUnclippedRectangle(canvas: Canvas){
}
private fun drawDifferenceClippingExample(canvas: Canvas){
}
private fun drawCircularClippingExample(canvas: Canvas){
}
private fun drawIntersectionClippingExample(canvas: Canvas){
}
private fun drawCombinedClippingExample(canvas: Canvas){
}
private fun drawRoundedRectangleClippingExample(canvas: Canvas){
}
private fun drawOutsideClippingExample(canvas: Canvas){
}
private fun drawTranslatedTextExample(canvas: Canvas){
}
private fun drawSkewedTextExample(canvas: Canvas){
}
private fun drawQuickRejectExample(canvas: Canvas){
}

5. Task: Create a method to draw the shapes

The app draws the same rectangle and shapes seven times, first with no clipping, then six times with various clipping paths applied. The drawClippedRectangle() method factors out the code for drawing one rectangle, as shown below.

15e11763cdd54e57.png

Step 1: Create the drawClippedRectangle() method

  1. Create a drawClippedRectangle() method that takes an argument canvas of type Canvas.
private fun drawClippedRectangle(canvas: Canvas) {
}
  1. Inside the drawClippedRectangle() method, set the boundaries of the clipping rectangle for the whole shape. Apply a clipping rectangle that constrains to drawing only the square.
canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight,clipRectBottom
)

The Canvas.clipRect(...) method reduces the region of the screen that future draw operations can write to. It sets the clipping boundaries to be the spatial intersection of the current clipping rectangle and the rectangle passed into clipRect(). There are lots of variants of the clipRect() method that accept different forms for regions and allow different operations on the clipping rectangle.

  1. Fill the canvas with white color. Yes! The whole canvas, because you are not drawing rectangles, you are clipping! Because of the clipping rectangle, only the region defined by the clipping rectangle is filled, creating a white rectangle. The rest of the surface remains gray.
canvas.drawColor(Color.WHITE)
  1. Change the color to red and draw a diagonal line inside the clipping rectangle.
paint.color = Color.RED
canvas.drawLine(
   clipRectLeft,clipRectTop,
   clipRectRight,clipRectBottom,paint
)
  1. Set the color to green and draw a circle inside the clipping rectangle.
paint.color = Color.GREEN
canvas.drawCircle(
   circleRadius,clipRectBottom - circleRadius,
   circleRadius,paint
)
  1. Set the color to blue and draw text aligned with the right edge of the clipping rectangle. Use canvas.drawText() to draw text.
paint.color = Color.BLUE
// Align the RIGHT side of the text with the origin.
paint.textSize = textSize
paint.textAlign = Paint.Align.RIGHT
canvas.drawText(
   context.getString(R.string.clipping),
   clipRectRight,textOffset,paint
)

Step 2: Implement the drawBackAndUnclippedRectangle() method

  1. To see the drawClippedRectangle() method in action, draw the first unclipped rectangle by implementing the drawBackAndUnclippedRectangle() method as shown below. Save the canvas, translate to the first row and column position, draw by calling drawClippedRectangle(), and then restore the canvas to its previous state.
private fun drawBackAndUnclippedRectangle(canvas: Canvas){
   canvas.drawColor(Color.GRAY)
   canvas.save()
   canvas.translate(columnOne,rowOne)
   drawClippedRectangle(canvas)
   canvas.restore()
}
  1. Run your app. You should see the first white rectangle with its circle, red line, and text on a gray background.

bea74ed67cb08c6d.png

6. Task: Implement the clipping methods

In the following clipping example methods you apply various combinations of clipping regions to achieve graphical effects and learn how you can combine clipping regions to create any shape you need.

Each of these methods follows the same pattern.

  1. Save the current state of the canvas: canvas.save().

The activity context maintains a stack of drawing states. Drawing states consist of the current transformation matrix and the current clipping region. You can save the current state, perform actions that change the drawing state (such as translating or rotating the canvas), and then restore the saved drawing state. (Note: This is like the "stash" command in git!).

When your drawing includes transformations, chaining and undoing transformations by reversing them is error-prone. For example, if you translate, stretch, and then rotate, it gets complex quickly. Instead, save the state of the canvas, apply your transformations, draw, and then restore the previous state.

For example, you could define a clipping region, and save that state. Then translate the canvas, add a clipping region, and rotate. After doing some drawing, you can restore the original clipping state, and you could proceed to do a different translation and skew transformation, as shown in the diagram.

8ac5dc529d60f1f4.png

  1. Translate the origin of the canvas to the row/column coordinates: canvas.translate().

It is much simpler to move the origin of the canvas and draw the same thing in a new coordinate system than to move all the elements to draw. (Tip: You can use the same technique for rotating elements.)

  1. Apply transformations to the path, if any.
  2. Apply clipping: canvas.clipPath(path).
  3. Draw the shapes: drawClippedRectangle() or drawText().
  4. Restore the previous canvas state: canvas.restore().

Step 1: Implement drawDifferenceClippingExample(canvas)

Add code to draw the second rectangle, which uses the difference between two clipping rectangles to create a picture frame effect.

f0a9c0ca61f67b93.png

Use the code below which does the following:

  1. Save the canvas.
  2. Translate the origin of the canvas into open space to the first row, second column, to the right of the first rectangle.
  3. Apply two clipping rectangles. The DIFFERENCE operator subtracts the second rectangle from the first one.
  1. Call the drawClippedRectangle() method to draw the modified canvas.
  2. Restore the canvas state.
private fun drawDifferenceClippingExample(canvas: Canvas) {
   canvas.save()
   // Move the origin to the right for the next rectangle.
   canvas.translate(columnTwo,rowOne)
   // Use the subtraction of two clipping rectangles to create a frame.
   canvas.clipRect(
       2 * rectInset,2 * rectInset,
       clipRectRight - 2 * rectInset,
       clipRectBottom - 2 * rectInset
   )
   // The method clipRect(float, float, float, float, Region.Op
   // .DIFFERENCE) was deprecated in API level 26. The recommended
   // alternative method is clipOutRect(float, float, float, float),
   // which is currently available in API level 26 and higher.
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O){
       canvas.clipRect(
           4 * rectInset,4 * rectInset,
           clipRectRight - 4 * rectInset,
           clipRectBottom - 4 * rectInset,
            Region.Op.DIFFERENCE
       )
   } else {
       canvas.clipOutRect(
           4 * rectInset,4 * rectInset,
           clipRectRight - 4 * rectInset,
           clipRectBottom - 4 * rectInset
       )
   }
   drawClippedRectangle(canvas)
   canvas.restore()
}
  1. Run your app and it should look like this.

cfa32bc7c9fcc65d.png

Step 2: Implement drawCircularClippingExample(canvas)

Next, add code to draw a rectangle that uses a circular clipping region created from a circular path, essentially removing (not drawing) the circle and thus showing the gray background instead.

58a9717ae951640b.png

private fun drawCircularClippingExample(canvas: Canvas) {

   canvas.save()
   canvas.translate(columnOne, rowTwo)
   // Clears any lines and curves from the path but unlike reset(),
   // keeps the internal data structure for faster reuse.
   path.rewind()
   path.addCircle(
       circleRadius,clipRectBottom - circleRadius,
       circleRadius,Path.Direction.CCW
   )
   // The method clipPath(path, Region.Op.DIFFERENCE) was deprecated in
   // API level 26. The recommended alternative method is
   // clipOutPath(Path), which is currently available in
   // API level 26 and higher.
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
       canvas.clipPath(path, Region.Op.DIFFERENCE)
   } else {
       canvas.clipOutPath(path)
   }
   drawClippedRectangle(canvas)
   canvas.restore()
}

Step 3: Implement drawIntersectionClippingExample(canvas)

Next, add code to draw the intersection of two clipping rectangles in the second row and column.

ec28108176a964df.png 29bcdee51e177787.png

Note that depending on your screen resolution, the looks of this region will vary. Experiment with the smallRectOffset dimension to change the size of the visible region. A smaller smallRectOffset results in a larger region on screen.

private fun drawIntersectionClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnTwo,rowTwo)
   canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight - smallRectOffset,
       clipRectBottom - smallRectOffset
   )
   // The method clipRect(float, float, float, float, Region.Op
   // .INTERSECT) was deprecated in API level 26. The recommended
   // alternative method is clipRect(float, float, float, float), which
   // is currently available in API level 26 and higher.
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
       canvas.clipRect(
           clipRectLeft + smallRectOffset,
           clipRectTop + smallRectOffset,
           clipRectRight,clipRectBottom,
           Region.Op.INTERSECT
       )
   } else {
       canvas.clipRect(
           clipRectLeft + smallRectOffset,
           clipRectTop + smallRectOffset,
           clipRectRight,clipRectBottom
       )
   }
   drawClippedRectangle(canvas)
   canvas.restore()
}

69f139122121dc6b.png

Step 4: Implement drawCombinedClippingExample(canvas)

Next, combine shapes, a circle and a rectangle, and draw any path to define a clipping region.

285443ce02c677f6.png

private fun drawCombinedClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnOne, rowThree)
   path.rewind()
   path.addCircle(
       clipRectLeft + rectInset + circleRadius,
       clipRectTop + circleRadius + rectInset,
       circleRadius,Path.Direction.CCW
   )
   path.addRect(
       clipRectRight / 2 - circleRadius,
       clipRectTop + circleRadius + rectInset,
       clipRectRight / 2 + circleRadius,
       clipRectBottom - rectInset,Path.Direction.CCW
   )
   canvas.clipPath(path)
   drawClippedRectangle(canvas)
   canvas.restore()
}

3051208654995360.png

Step 5: Implement drawRoundedRectangleClippingExample(canvas)

Next, add a rounded rectangle which is a commonly used clipping shape.

786afee7d8b7f611.png

  1. At the top level, create and initialize a rectangle variable. RectF is a class that holds rectangle coordinates in floating point.
private var rectF = RectF(
   rectInset,
   rectInset,
   clipRectRight - rectInset,
   clipRectBottom - rectInset
)
  1. Implement the function drawRoundedRectangleClippingExample(). The addRoundRect() function takes a rectangle, values for the x and y values of the corner radius, and the direction to wind the round-rectangle's contour. Path.Direction specifies how closed shapes (e.g. rects, ovals) are oriented when they are added to a path. CCW stands for counter-clockwise.
private fun drawRoundedRectangleClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnTwo,rowThree)
   path.rewind()
   path.addRoundRect(
       rectF,clipRectRight / 4,
       clipRectRight / 4, Path.Direction.CCW
   )
   canvas.clipPath(path)
   drawClippedRectangle(canvas)
   canvas.restore()
}

5afd44132d0804f8.png

Step 6: Implement drawOutsideClippingExample(canvas)

Clip the outside around the rectangle by doubling the insets of the clipping rectangle.

345189d86581ec0f.png

private fun drawOutsideClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnOne,rowFour)
   canvas.clipRect(2 * rectInset,2 * rectInset,
       clipRectRight - 2 * rectInset,
       clipRectBottom - 2 * rectInset)
   drawClippedRectangle(canvas)
   canvas.restore()
}

689dfbbdd6c5c9fb.png

Step 7: Implement drawTranslatedTextExample(canvas)

Drawing text is not really different from any other shapes, and you can apply transformations to text. For example, you can translate text by translating the canvas and drawing the text.

2c4aa44ac47870a7.png

  1. Implement the function below.
private fun drawTranslatedTextExample(canvas: Canvas) {
   canvas.save()
   paint.color = Color.GREEN
   // Align the RIGHT side of the text with the origin.
   paint.textAlign = Paint.Align.LEFT
   // Apply transformation to canvas.
   canvas.translate(columnTwo,textRow)
   // Draw text.
   canvas.drawText(context.getString(R.string.translated),
       clipRectLeft,clipRectTop,paint)
   canvas.restore()
}
  1. Run your app to see the translated text.

f832673db567ce9a.png

Step 8: Implement drawSkewedTextExample(canvas)

You can also skew text. That is, distort it in various ways.

83e4442d6cb52fc.png

  1. Create the function below in ClippedView.
private fun drawSkewedTextExample(canvas: Canvas) {
   canvas.save()
   paint.color = Color.YELLOW
   paint.textAlign = Paint.Align.RIGHT
   // Position text.
   canvas.translate(columnTwo, textRow)
   // Apply skew transformation.
   canvas.skew(0.2f, 0.3f)
   canvas.drawText(context.getString(R.string.skewed),
       clipRectLeft, clipRectTop, paint)
   canvas.restore()
}
  1. Run your app to see the skewed text drawn before the translated text.

578d870e80d7110e.png

7. Task: quickReject()

The quickReject() Canvas method allows you to check whether a specified rectangle or path would lie completely outside the currently visible regions, after all transformations have been applied.

The quickReject() method is incredibly useful when you are constructing more complex drawings and need to do so as fast as possible. With quickReject(), you can decide efficiently which objects you do not have to draw at all, and there is no need to write your own intersection logic.

  • The quickReject() method returns true if the rectangle or path would not be visible at all on the screen. For partial overlaps, you still have to do your own checking.
  • The EdgeType is either AA ( Antialiased: Treat edges by rounding-out, because they may be antialiased) or BW (Black-White: Treat edges by just rounding to the nearest pixel boundary) for just rounding to the nearest pixel.

There are several versions of quickReject(), and you can also find them in the documentation.

boolean

quickReject(float left, float top, float right, float bottom, Canvas.EdgeType type)

boolean

quickReject(RectF rect, Canvas.EdgeType type)

boolean

quickReject(Path path, Canvas.EdgeType type)

In this exercise, you are going to draw in a new row, below the text, and inside the clipRect, as before.

  • You first call quickReject() with a rectangle inClipRectangle, that overlaps with clipRect. So quickReject() returns false, clipRect is filled with BLACK, and the inClipRectangle rectangle is drawn.

32a4f3d6e0136ac3.png

  • Then change the code and call quickReject(), with notInClipRectangle. quickReject() now returns true, and clipRect is filled with WHITE, and notInClipRectangle is not drawn.

1d3efd8d1322560.png

When you have complex drawings, this can quickly tell you, which shapes are completely outside the clipping region, and for which you may have to do additional calculations, and drawing, because they are partially or fully inside the clipping region.

Step: Experiment with quickReject()

  1. At the top-level, create a variable for the y coordinates of an additional row.
   private val rejectRow = rowFour + rectInset + 2*clipRectBottom
  1. Add the following drawQuickRejectExample() function to ClippedView. Read the code, as it contains everything you need to know to use quickReject().
private fun drawQuickRejectExample(canvas: Canvas) {
   val inClipRectangle = RectF(clipRectRight / 2,
       clipRectBottom / 2,
       clipRectRight * 2,
       clipRectBottom * 2)

   val notInClipRectangle = RectF(RectF(clipRectRight+1,
       clipRectBottom+1,
       clipRectRight * 2,
       clipRectBottom * 2))

   canvas.save()
   canvas.translate(columnOne, rejectRow)
   canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight,clipRectBottom
   )
   if (canvas.quickReject(
           inClipRectangle, Canvas.EdgeType.AA)) {
       canvas.drawColor(Color.WHITE)
   }
   else {
       canvas.drawColor(Color.BLACK)
       canvas.drawRect(inClipRectangle, paint
       )
   }
       canvas.restore()
}
  1. In onDraw(), uncomment the invocation of drawQuickRejectExample().
  2. Run your app, and you will see a black rectangle, which is the filled clipping region, and parts of the inClipRectangle, because the two rectangles overlap, so quickReject() returns false and inClipRectangle is drawn.

32a4f3d6e0136ac3.png

  1. In drawQuickRejectExample(), change the code to run quickReject() against notInClipRectangle.Now quickReject() returns true and the clipping region is filled with white.

1d3efd8d1322560.png

8. Solution code

Download the code for the finished codelab:

$  git clone https://github.com/googlecodelabs/android-kotlin-drawing-clipping

Alternatively you can download the repository as a Zip file, unzip it, and open it in Android Studio.

9. Summary

  • The Context of an activity maintains a state that preserves transformations and clipping regions for the Canvas.
  • Use canvas.save() and canvas.restore() to draw and return to the original state of your canvas.
  • To draw multiple shapes on a canvas, you can either calculate their location, or you can move (translate) the origin of your drawing surface. The latter can make it easier to create utility methods for repeated draw sequences.
  • Clipping regions can be any shape, combination of shapes or path.
  • You can add, subtract, and intersect clipping regions to get exactly the region you need.
  • You can apply transformations to text by transforming the canvas.
  • The quickReject() Canvas method allows you to check whether a specified rectangle or path would lie completely outside the currently visible regions.

10. Learn more

Udacity course:

Android developer documentation:

Also see the Graphics Architecture series of articles for an in-depth explanation of how the Android framework draws to the screen.

11. Homework

This section lists possible homework assignments for students who are working through this codelab as part of a course led by an instructor. It's up to the instructor to do the following:

  • Assign homework if required.
  • Communicate to students how to submit homework assignments.
  • Grade the homework assignments.

Instructors can use these suggestions as little or as much as they want, and should feel free to assign any other homework they feel is appropriate.

If you're working through this codelab on your own, feel free to use these homework assignments to test your knowledge.

Answer these questions

Question 1

What method do you call to efficiently exclude shapes from being drawn?

excludeFromDrawing()

quickReject()

onDraw()

clipRect()

Question 2

Canvas.save() and Canvas.restore() save and restore which information?

▢ Color, line width, etc.

▢ Current transformations only

▢ Current transformations and clipping region

▢ Current clipping region only

Question 3

Paint.Align specifies:

▢ How to align the following drawing shapes

▢ Which side of the origin the text is drawn from

▢ Where in the clipping region it is aligned

▢ Which side of the text to align to the origin

12. Next codelab

For links to other codelabs in this course, see the Advanced Android in Kotlin codelabs landing page.