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.

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.

What you should already know

You should be able to:

What you'll learn

What you'll do

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

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.

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.

  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.

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.

  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.

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.

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.

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.

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:

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.

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.

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 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:

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.

Answer these questions

Question 1

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

Question 2

What are some properties of drawables?

Question 3

Which of the following statements are true?

Question 4

What is clipping?

Submit your app for grading

Guidance for graders

Check that the app has the following features:

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