Google is committed to advancing racial equity for Black communities. See how.

Advanced Android 11.1 Part C: Apply clipping to Canvas objects

This codelab is part of the Advanced Android Development training course, developed by the Google Developers Training team. You will get the most value out of this course if you work through the codelabs in sequence.

For complete details about the course, see the Advanced Android Development overview.

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. 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, it is usually more efficient to only draw the visible portions. "Usually," because clipping operations also have a cost.

When you display a stack of cards, you only need to draw the visible portions

You do this by specifying 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. The clipping region is commonly a rectangle, but it can be any shape or combination of shapes. You can also specify whether you want the region inside the clipping region included or excluded. The screenshot below shows an example. When a clipping rectangle is applied to an image, only the portion inside that rectangle is displayed.

When a clipping rectangle is applied to an image, only the portion inside that rectangle is displayed.

What you should already know

You should be able to:

  • Create apps with Android Studio and run them on a physical or virtual mobile device.
  • Add event handlers to views.
  • Create a custom View.
  • Create and draw on a Canvas.
  • Create a Bitmap and associate it with a View; create a Canvas for a Bitmap; create and customize a Paint object for styling; draw on the canvas and refresh the display.
  • Create a custom View, override onDraw() and onSizeChanged().

What you'll learn

  • How to apply different kinds of clipping to a canvas.
  • How to save and restore drawing states of a canvas.

What you'll do

  • Create an app that draws clipped shapes on the screen.

The ClippingExample app demonstrates how you can use and combine shapes to specify which portions of a canvas are displayed in a view.

Screenshot for the ClippingExample app

1.1 Create the ClippingExample project

  1. Create the ClippingExample app with the Empty Activity template. Uncheck Generate layout file as you don't need it.
  2. In the MainActivity class, in the onCreate() method, set the content view to a new instance of ClippedView.
@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(new ClippedView(this));
}
  1. Create a new class for a custom view called ClippedView which extends View. The rest of the work will all be inside ClippedView.
private static class ClippedView extends View {...}

1.2 Add convenience variables for the ClippedView class

  1. Define member variables mPaint and mPath in the ClippedView class.
private Paint mPaint;
private Path mPath;
  1. For the app to look correct on smaller screens, define dimensions for the smaller screen in the default dimens.xml file.
<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>
  1. Create a values-sw480dp folder and define values for the larger screens in dimens.xml in the values-sw480dp folder. (Note: If the empty folder does not show up in Android Studio, manually add a resource file to the ClippingExample/app/src/main/res/values-sw480dp directory. This makes the folder show in your Project pane.)
<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>
  1. In ClippedView, add convenience member variables for dimensions, so that you only have to fetch the resources once.
private int mClipRectRight =
       (int) getResources().getDimension(R.dimen.clipRectRight);
private int mClipRectBottom =
       (int) getResources().getDimension(R.dimen.clipRectBottom);
private int mClipRectTop =
       (int) getResources().getDimension(R.dimen.clipRectTop);
private int mClipRectLeft =
       (int) getResources().getDimension(R.dimen.clipRectLeft);
private int mRectInset =
       (int) getResources().getDimension(R.dimen.rectInset);
private int mSmallRectOffset =
       (int) getResources().getDimension(R.dimen.smallRectOffset);

private int mCircleRadius =
       (int) getResources().getDimension(R.dimen.circleRadius);

private int mTextOffset =
       (int) getResources().getDimension(R.dimen.textOffset);
private int mTextSize =
       (int) getResources().getDimension(R.dimen.textSize);
  1. In ClippedView, add convenience member variables for row and column coordinates so that you only have to calculate them once.
private int mColumnOne = mRectInset;
private int mColumnnTwo = mColumnOne + mRectInset + mClipRectRight;

private int mRowOne = mRectInset;
private int mRowTwo = mRowOne + mRectInset + mClipRectBottom;
private int mRowThree = mRowTwo + mRectInset + mClipRectBottom;
private int mRowFour = mRowThree + mRectInset + mClipRectBottom;
private int mTextRow = mRowFour + (int)(1.5 * mClipRectBottom);
  1. In ClippedView, add a private final member variable for a rectangle of type RectF:
private final RectF mRectF;

1.3 Add constructors for the ClippedView class

  1. Add a constructor that initializes the Paint and Path objects for the canvas.

Note that the Paint.Align property specifies which side of the text to align to the origin (not which side of the origin the text goes, or where in the region it is aligned!). Aligning the right side of the text to the origin places it on the left of the origin.

public ClippedView(Context context) {
   this(context,null);
}

public ClippedView(Context context, AttributeSet attributeSet) {
   super(context, attributeSet);
   setFocusable(true);
   mPaint = new Paint();
   // Smooth out edges of what is drawn without affecting shape.
   mPaint.setAntiAlias(true);
   mPaint.setStrokeWidth(
           (int) getResources().getDimension(R.dimen.strokeWidth));
   mPaint.setTextSize((int) getResources().getDimension(R.dimen.textSize));
   mPath = new Path();

   mRectF = new RectF(new Rect(mRectInset, mRectInset,
           mClipRectRight-mRectInset, mClipRectBottom-mRectInset));
}
  1. Run your app to make sure the code is correct. You should see the name of the app and a white screen.

1.4 Understand the drawing algorithm

In onDraw(), you define seven different clipped rectangles as shown in the app screenshot below. The rectangles are all drawn the same way; the only difference is their defined clipping regions.

Screenshot for the ClippingExample app

The algorithm used to draw the rectangles works as shown in the screenshot and explanation below. In summary, drawing a series of rectangles by moving the origin of the Canvas. (1) Translate Canvas. (2) Draw rectangle. (3) Restore Canvas and Origin.

Drawing a series of rectangles by moving the origin of the Canvas. (1) Translate Canvas. (2) Draw rectangle. (3) Restore Canvas and Origin.

  1. Fill the Canvas with the gray background color.
  2. Save the current state of the Canvas so you can reset to that initial state.
  3. Translate the Origin of the canvas to the location where you want to draw the next rectangle. 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, and then draw the shapes at the same location in the translated coordinate system. This is simpler and slightly more efficient.
  4. Apply clipping shapes and paths.
  5. Draw the rectangle.
  6. Restore the state of the Canvas.
  7. GO BACK TO Step 2 and repeat until all rectangles are drawn.

1.5 Add a helper method to draw clipped rectangles

The app draws the rectangle below seven times, first with no clipping, then six times with various clipping paths applied.

Clipping rectangle for one square

The drawClippedRectangle() method factors out the code for drawing one rectangle.

  1. Create a drawClippedRectangle() method that takes a Canvas canvas argument.
private void drawClippedRectangle(Canvas canvas) {...}
  1. Apply a clipping rectangle that constraints to drawing only the square to the canvas.
canvas.clipRect(mClipRectLeft, mClipRectTop,
                mClipRectRight, mClipRectBottom);

The Canvas.clipRect(left, top, right, bottom) method reduces the region of the screen that future draw operations can write to. It sets the clipping boundaries (clipBounds) to be the spatial intersection of the current clipping rectangle and the rectangle specified. There are lot 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. Because of the clipping rectangle, only the region defined by the clipping rectangle is filled, creating a white rectangle.
canvas.drawColor(Color.WHITE);
  1. Draw the red line, green circle, and text, as shown in the completed method below.
  2. After you paste the code, create a string resource "Clipping" to get rid of the error for R.string.clipping in the last line.
private void drawClippedRectangle(Canvas canvas) {
   // Set the boundaries of the clipping rectangle for whole picture.
   canvas.clipRect(mClipRectLeft, mClipRectTop,
           mClipRectRight, mClipRectBottom);

   // Fill the canvas with white.
   // With the clipped rectangle, this only draws
   // inside the clipping rectangle.
   // The rest of the surface remains gray.
   canvas.drawColor(Color.WHITE);

   // Change the color to red and
   // draw a line inside the clipping rectangle.
   mPaint.setColor(Color.RED);
   canvas.drawLine(mClipRectLeft, mClipRectTop,
           mClipRectRight, mClipRectBottom, mPaint);

   // Set the color to green and
   // draw a circle inside the clipping rectangle.
   mPaint.setColor(Color.GREEN);
   canvas.drawCircle(mCircleRadius, mClipRectBottom - mCircleRadius,
           mCircleRadius, mPaint);

   // Set the color to blue and draw text aligned with the right edge
   // of the clipping rectangle.
   mPaint.setColor(Color.BLUE);
   // Align the RIGHT side of the text with the origin.
   mPaint.setTextAlign(Paint.Align.RIGHT);
   canvas.drawText(getContext().getString(R.string.clipping),
           mClipRectRight, mTextOffset, mPaint);
}
  1. If you run your app, you still only see the white screen, because you have not overridden onDraw() and thus are not drawing anything yet.

1.6 Override the onDraw() method

In the onDraw() method 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.

When you use View classes provided by the Android system, the system clips views for you to minimize overdraw. When you use custom View classes and override the onDraw() method, clipping what you draw becomes your responsibility.

  1. Create the onDraw() method, if it is not already present as a code stub.
@Override protected void onDraw(Canvas canvas) { ... }

Next, add code to draw the first rectangle, which has no additional clipping.

IMAGEINFO]: icon_first_unclipped_rectangle.png

  1. In onDraw(), fill the canvas with gray color.
canvas.drawColor(Color.GRAY);
  1. Save the drawing state of the canvas.

Context maintains a stack of drawing states. Each state includes the currently applied transformations and clipping regions. Undoing a transformation by reversing it is error-prone, as well as chaining too many transformations relative to each other. Translation is straightforward to reverse, but if you also stretch, rotate, or custom deform, it gets complex quickly. Instead, you save the state of the canvas, apply your transformations, draw, and then restore the previous state.

canvas.save();
  1. Translate the origin of the canvas to the top-left corner of the first rectangle.
canvas.translate(mColumnOne, mRowOne);
  1. Call the drawClippedRectangle() method to draw the first rectangle.
drawClippedRectangle(canvas);
  1. Restore the previous state of the canvas.
canvas.restore();
  1. Run your app. You should now see the first rectangle drawn on a gray background.

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

IMAGEINFO]: icon_second_frame_rectangle.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 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.
  3. Run your app.
// Draw a rectangle that uses the difference between two 
// clipping rectangles to create a picture frame effect.
canvas.save();
// Move the origin to the right for the next rectangle.
canvas.translate(mColumnnTwo, mRowOne);
// Use the subtraction of two clipping rectangles to create a frame.
canvas.clipRect(2 * mRectInset, 2 * mRectInset,
       mClipRectRight-2 * mRectInset, mClipRectBottom-2 * mRectInset);
// 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(android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
    canvas.clipRect(4*mRectInset, 4*mRectInset,
               mClipRectRight-4*mRectInset, mClipRectBottom-4*mRectInset,
                Region.Op.DIFFERENCE);
else{
    canvas.clipOutRect(4*mRectInset, 4*mRectInset,
                         mClipRectRight-4*mRectInset,
                            mClipRectBottom-4*mRectInset);
     }

drawClippedRectangle(canvas);
canvas.restore();

Next, add code to draw the third rectangle, which uses a circular clipping region created from a circular path.

IMAGEINFO]: icon_third_circle_rectangle.png

Here is the code:

// Draw a rectangle that uses a circular clipping region 
// created from a circular path.
canvas.save();
canvas.translate(mColumnOne, mRowTwo);
// Clears any lines and curves from the path but unlike reset(),
// keeps the internal data structure for faster reuse.
mPath.rewind();
mPath.addCircle(mCircleRadius, mClipRectBottom-mCircleRadius,
       mCircleRadius, 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 (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
   canvas.clipPath(mPath, Region.Op.DIFFERENCE);
} else {
   canvas.clipOutPath(mPath);
}

drawClippedRectangle(canvas);
canvas.restore();

Next, add code to draw the intersection of two clipping rectangles.

IMAGEINFO]: icon_fourth_intersect_rectangle.png

Here is the code:

// Use the intersection of two rectangles as the clipping region.
canvas.save();
canvas.translate(mColumnnTwo, mRowTwo);
canvas.clipRect(mClipRectLeft, mClipRectTop,
       mClipRectRight-mSmallRectOffset,
       mClipRectBottom-mSmallRectOffset);
// 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 (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
   canvas.clipRect(mClipRectLeft + mSmallRectOffset,
                   mClipRectTop + mSmallRectOffset, mClipRectRight,
                   mClipRectBottom, Region.Op.INTERSECT);
} else {
   canvas.clipRect(mClipRectLeft + mSmallRectOffset,
                   mClipRectTop + mSmallRectOffset, mClipRectRight,
                   mClipRectBottom);
}

drawClippedRectangle(canvas);
canvas.restore();

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

IMAGEINFO]: icon_fifth_combo_rectangle.png

Here is the code:

// You can combine shapes and draw any path to define a clipping region.
canvas.save();
canvas.translate(mColumnOne, mRowThree);
mPath.rewind();
mPath.addCircle(mClipRectLeft+mRectInset+mCircleRadius,
       mClipRectTop+mCircleRadius+mRectInset,
       mCircleRadius, Path.Direction.CCW);
mPath.addRect(mClipRectRight/2-mCircleRadius,
       mClipRectTop+mCircleRadius+mRectInset,
       mClipRectRight/2+mCircleRadius,
       mClipRectBottom-mRectInset,Path.Direction.CCW);
canvas.clipPath(mPath);
drawClippedRectangle(canvas);
canvas.restore();

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

IMAGEINFO]: icon_sixth_rounded_rectangle.png

Here is the code:

// Use a rounded rectangle. Use mClipRectRight/4 to draw a circle.
canvas.save();
canvas.translate(mColumnnTwo, mRowThree);
mPath.rewind();
mPath.addRoundRect(mRectF, (float)mClipRectRight/4,
       (float)mClipRectRight/4, Path.Direction.CCW);
canvas.clipPath(mPath);
drawClippedRectangle(canvas);
canvas.restore();

Next, clip the outside around the rectangle.

IMAGEINFO]: icon_seventh_try_rectangle.png

Here is the code:

// Clip the outside around the rectangle.
canvas.save();
// Move the origin to the right for the next rectangle.
canvas.translate(mColumnOne, mRowFour);
canvas.clipRect(2 * mRectInset, 2 * mRectInset,
       mClipRectRight-2*mRectInset,
       mClipRectBottom-2*mRectInset);
drawClippedRectangle(canvas);
canvas.restore();

Finally, draw and transform text.

IMAGEINFO]: transformed_text.png

In the previous steps you used the translate transform to move the origin of the canvas. You can apply transformations to any shape, including text, before you draw it, as shown in the following example.

// Draw text with a translate transformation applied.
canvas.save();
mPaint.setColor(Color.CYAN);
// Align the RIGHT side of the text with the origin.
mPaint.setTextAlign(Paint.Align.LEFT);
// Apply transformation to canvas.
canvas.translate(mColumnnTwo, mTextRow);
// Draw text.
canvas.drawText(
        getContext().getString(R.string.translated), 0, 0, mPaint);
canvas.restore();

// Draw text with a translate and skew transformations applied.
canvas.save();
mPaint.setTextSize(mTextSize);
mPaint.setTextAlign(Paint.Align.RIGHT);
// Position text.
   canvas.translate(mColumnnTwo, mTextRow);
   // Apply skew transformation.
   canvas.skew(0.2f, 0.3f);
   canvas.drawText(
           getContext().getString(R.string.skewed), 0, 0, mPaint);
   canvas.restore();
} // End of onDraw()

Android Studio project: ClippingExample

  • 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.

The related concept documentation is in 11.1 The Canvas class.

Android developer documentation:

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.

Build and run an app

Create a MemoryGame app that hides and reveals "cards" as the user taps on the screen. Use clipping to implement the hide/reveal effect.

  • You can use simple colored squares or shapes for the "cards."
  • If the user reveals two matching cards, show a congratulatory toast. If the user reveals two cards that don't match, show an encouraging message telling them to tap to continue.
  • Click handling: On the first tap, show the first card. On the second tap, show the second card and display a message. On the next tap, restart.

Answer these questions

Question 1

To display something to the screen, which one of the following draw and animation classes is always required?

  • View
  • Drawable
  • Canvas
  • Bitmap

Question 2

What are some properties of drawables?

  • Drawables are drawn into a view and the system handles drawing.
  • Drawables are best for simple graphics that do not change dynamically.
  • Drawables offer the best performance for game animations.
  • You can use drawables for frame-by-frame animations.

Question 3

Which of the following statements are true?

  • You use a Canvas object when elements in your app are redrawn regularly.
  • To draw on a Canvas, you must override the onDraw() method of a custom view.
  • Every view has a Canvas that you can access.
  • A Paint object holds style and color information about how to draw geometries, text, and bitmaps.

Question 4

What is clipping?

  • A technique for defining regions on a Canvas that will not be drawn to the screen.
  • A technique for making the Canvas smaller so the Canvas uses less memory.
  • A way of telling the system which portions of a Canvas do not need to be redrawn.
  • A technique to consider when you're trying to speed up drawing.
  • A way to create interesting graphical effects.

Submit your app for grading

Guidance for graders

Check that the app has the following features:

  • When the user taps, the app reveals a "card."
  • When the user taps again, the app reveals a second "card" and shows a toast congratulating or encouraging the user.
  • On the third tap, the game restarts.
  • The code uses a Canvas object and clipping methods to achieve the hide/reveal effects of playing the game.

To see all the codelabs in the Advanced Android Development training course, visit the Advanced Android Development codelabs landing page.