Let's start by the simplest drawing operations: drawColor(int color), drawARGB(int a, int r, int g, int b), drawRGB(int r, int g, int b), and drawPaint(Paint paint). These will fill the entire canvas, taking into account the clipping area.
Let's move forward to drawRect() and drawRoundRect(). These two methods are quite simple too, drawRect() will draw a rectangle and drawRoundRect() will draw a rectangle with rounded borders.
We can use both methods directly, specifying the coordinates or using Rect. Let's create a simple example that will draw a new random rounded rectangle every time the view is drawn or it’s onDraw() method is called.
To start, lets define two ArrayLists; one will hold the coordinates and the other will hold the color information of that rectangle:
private Paint paint; private ArrayList<Float> rects; private ArrayList<Integer> colors;
We also declared a Paint object that we'll use to draw all the rounded rectangles. Let's now initialize them:
public PrimitiveDrawer(Context context, AttributeSet attributeSet) { super(context, attributeSet); rects = new ArrayList<>(); colors = new ArrayList<>(); paint = new Paint(); paint.setStyle(Paint.Style.FILL); paint.setAntiAlias(true); }
We've set the paint object style to Paint.Style.FILL and set the AntiAlias flag, but we haven't set the color. We'll do so before drawing each rectangle.
Let's now implement our onDraw() method. To start, we'll add four new random coordinates. As Math.random() returns a value from 0 to 1, we multiply it by the current view width and height to get a proper view coordinate. We also generate a new random color with full opacity:
@Override protected void onDraw(Canvas canvas) { canvas.drawColor(BACKGROUND_COLOR); int width = getWidth(); int height = getHeight(); for (int i = 0; i < 2; i++) { rects.add((float) Math.random() * width); rects.add((float) Math.random() * height); } colors.add(0xff000000 | (int) (0xffffff * Math.random())); for (int i = 0; i < rects.size() / 4; i++) { paint.setColor(colors.get(i)); canvas.drawRoundRect( rects.get(i * 4 ), rects.get(i * 4 + 1), rects.get(i * 4 + 2), rects.get(i * 4 + 3), 40, 40, paint); } if (rects.size() < 400) postInvalidateDelayed(20); }
Then, we'll loop with all the random points we added and take the 4 of them at the time, assuming the first two will be the starting X and Y and the latter two will be the ending X and Y coordinates of the rectangle. We hardcoded 40 as the angle of the rounded edges. We can play with this value to change the amount of roundness.
We've introduced bitwise operations on colors. We know we can store a color in a 32-bit integer value, and usually, in ARGB format. That gives us 8 bits for each component. Doing bitwise operations, we can easily manipulate colors. For more information on bitwise operations, please refer to:
https://en.wikipedia.org/wiki/Bitwise_operation.
To finish, if we have less than 100 rectangles or 400 coordinates in our array, we post an Invalidate event delayed by 20 milliseconds. It is only for demonstration purposes and to show that it is adding and drawing more rectangles. The drawRoundRect() method can easily be changed by drawRect() by just removing the two hardcoded 40s as the angle of the rounded edges.
Let's see the result:
![](assets/bd7c0838-689e-4951-b557-940f215def1a.png)
For the full source code, check the Example14-Primitives-Rect folder in the GitHub repository.
Let's continue with other primitives, for example, drawPoints. The drawPoints(float[] points, Paint paint) method will simply draw a list of points. It will use the stroke width and the stroke Cap of the paint object. For instance, a quick example that draws few random lines and also draws a point both at the beginning and at the end of each line:
@Override protected void onDraw(Canvas canvas) { canvas.drawColor(BACKGROUND_COLOR); if (points == null) { points = new float[POINTS * 2]; for(int i = 0; i < POINTS; i++) { points[i * 2 ] = (float) Math.random() * getWidth(); points[i * 2 + 1] = (float) Math.random() * getHeight(); } } paint.setColor(0xffa0a0a0); paint.setStrokeWidth(4.f); paint.setStrokeCap(Paint.Cap.BUTT); canvas.drawLines(points, paint); paint.setColor(0xffffffff); paint.setStrokeWidth(10.f); paint.setStrokeCap(Paint.Cap.ROUND); canvas.drawPoints(points, paint); }
Let's see the result:
![](assets/c54012c6-98f1-4eff-b43d-ce100ae7acf6.png)
We're creating the points array here on the onDraw() method, but it's done only once.
Check the full source code of this example in the Example15-Primitives-Points folder, in the GitHub repository.
Building on top of the previous example, we can easily introduce the drawCircle primitive. Let's change the code a bit though; instead of generating only pairs of random values, let's generate three random values. The first two will be the X and Y coordinate of the circle and the third the circle's radius. In addition, let's remove the lines for the sake of clarity:
@Override protected void onDraw(Canvas canvas) { canvas.drawColor(BACKGROUND_COLOR); if (points == null) { points = new float[POINTS * 3]; for(int i = 0; i < POINTS; i++) { points[i * 3 ] = (float) Math.random() * getWidth(); points[i * 3 + 1] = (float) Math.random() * getHeight(); points[i * 3 + 2] = (float) Math.random() * (getWidth()/4); } } for (int i = 0; i < points.length / 3; i++) { canvas.drawCircle( points[i * 3 ], points[i * 3 + 1], points[i * 3 + 2], paint); } }
We've also initialized our paint object on our class constructor:
paint = new Paint(); paint.setStyle(Paint.Style.FILL); paint.setAntiAlias(true); paint.setColor(0xffffffff);
Let's see the result:
![](assets/cb2b47d2-cd97-4573-b4aa-3a21886b8c4e.png)
Check the full source code of this example on the Example16-Primitives-Circles folder, in the GitHub repository.
To find out about all the primitives, modes, and methods to draw on a Canvas, check the Android documentation.
Paths can be considered as containers of primitives, lines, curves, and other geometric shapes that, as we already seen, can be used as clipping regions, drawn, or used to draw text on it.
To begin, let's modify our previous example and convert all the circles to a Path:
@Override protected void onDraw(Canvas canvas) { if (path == null) { float[] points = new float[POINTS * 3]; for(int i = 0; i < POINTS; i++) { points[i * 3 ] = (float) Math.random() * getWidth(); points[i * 3 + 1] = (float) Math.random() * getHeight(); points[i * 3 + 2] = (float) Math.random() * (getWidth()/4); } path = new Path(); for (int i = 0; i < points.length / 3; i++) { path.addCircle( points[i * 3 ], points[i * 3 + 1], points[i * 3 + 2], Path.Direction.CW); } path.close(); }
We don't need to store the points, so we declared it as a local variable. We created a Path object instead. Now that we have this Path with all the circles in it, we can draw it by calling the drawPath(Path path, Paint paint) method or use it as a clipping mask.
We added an image to our project and we'll draw it as a background image, but we'll apply a clipping mask defined by our Path to make things interesting:
canvas.save(); if (!touching) canvas.clipPath(path); if(background != null) { backgroundTranformation.reset(); float scale = ((float) getWidth()) / background.getWidth(); backgroundTranformation.postScale(scale, scale); canvas.drawBitmap(background, backgroundTranformation, null); } canvas.restore(); }
Let's see the result:
![](assets/3d2481dd-8a7f-49c3-b050-5f7b4929fa05.png)
To see the full source code of this example, check the Example17-Paths folder on the GitHub repository.
Checking the Android documentation about Paths, we can see that there are a lot of methods to add primitives to a Path, for example:
- addCircle()
- addRect()
- addRoundRect()
- addPath()
However, we're not limited to these methods, we can also add lines or displace where our path will start our next element using the lineTo or moveTo methods, respectively. In the case we want to use relative coordinates, the Path class provides us with the methods rLineTo and rMoveTo that assumes that the given coordinates are relative from the last point of the Path.
For additional information about Path and its methods, check the Android documentation website. We can do so, using the methods cubicTo and quadTo. A Bezier curve consists of control points that control the shape of the smooth curve. Let's build a quick example by adding control points each time the user taps on the screen.
First, let's define two Paint objects, one for the Bezier lines and another to draw the control points for reference:
pathPaint = new Paint(); pathPaint.setStyle(Paint.Style.STROKE); pathPaint.setAntiAlias(true); pathPaint.setColor(0xffffffff); pathPaint.setStrokeWidth(5.f); pointsPaint = new Paint(); pointsPaint.setStyle(Paint.Style.STROKE); pointsPaint.setAntiAlias(true); pointsPaint.setColor(0xffff0000); pointsPaint.setStrokeCap(Paint.Cap.ROUND); pointsPaint.setStrokeWidth(40.f);
Control points will be drawn as round red dots, while the Bezier lines will be drawn as thinner white lines. As we're initializing our objects, let's also define an empty Path and a float array to store the points:
points = new ArrayList<>(); path = new Path();
Now, let's override onTouchEvent() to add the point where the user tapped the screen and trigger a redraw of our custom view by calling the invalidate method.
@Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { points.add(event.getX()); points.add(event.getY()); invalidate(); } return super.onTouchEvent(event); }
On our onDraw() method, let's first check if we have already three points. If that is the case, let's add a cubic Bezier to our Path:
while(points.size() - currentIndex >= 6) { float x1 = points.get(currentIndex); float y1 = points.get(currentIndex + 1); float x2 = points.get(currentIndex + 2); float y2 = points.get(currentIndex + 3); float x3 = points.get(currentIndex + 4); float y3 = points.get(currentIndex + 5); if (currentIndex == 0) path.moveTo(x1, y1); path.cubicTo(x1, y1, x2, y2, x3, y3); currentIndex += 6; }
The currentIndex maintains the last index of the point array that has been inserted into the Path.
Now, let's draw the Path and the points:
canvas.drawColor(BACKGROUND_COLOR); canvas.drawPath(path, pathPaint); for (int i = 0; i < points.size() / 2; i++) { float x = points.get(i * 2 ); float y = points.get(i * 2 + 1); canvas.drawPoint(x, y, pointsPaint); }
Let's see the result:
![](assets/7f6bcce8-2242-4eba-887b-629a65ee7777.png)
See the full source code of this example on the Example18-Paths folder, in the GitHub repository.