Joshualamson customdrawingwithcanvascopy cover

Custom Drawing with Canvas

If you enjoy talks from 360 AnDev, please support the conference via Patreon!

When you’re creating a new view to display complex data, it’s easy for the number of view objects you’re using to get out of control. Between elaborate shapes, shaders, colors, and text in specific places and orientations, using only Android Views and layouts can turn your view into a dense, complicated mess. Luckily, there’s a better way to add all your customizations. What you need is a blank Canvas! (Literally, android.graphics.Canvas). In this session, we’ll delve into the details of using Android Canvas to display anything you need in a truly custom way.


Introduction

My name is Joshua Lamson. I love custom drawing with Canvas.

If you have not used the outline views developer feature, I highly recommend it. If you go into Settings > Developer Options, one of the choices is to outline every single view. You can see every view object that’s being drawn on screen. I’ll have days where I have this feature on. One day I was checking my email, and noticed Gmail, one email is one box, each individual word is its own thing. They’re doing this with custom views.

Why Custom Drawing?

There are many reasons to use custom drawing in your app. First, we use it to reduce our view hierarchy load. With the outline view setting, you can see the number of views being drawn on screen. For email apps, rather than having one view for the user avatar, one for the title of emails, one for the content, there’s often at least eight views per email. You don’t need that many views.

Custom drawing makes things easier for custom graphics like charts and graphs. Typically, if you want to roll a custom chart and you download a third party library, the library is massive because it’s not just the chart you need; it’s also fifteen other types.

Views in the Android framework are designed to work with everything and anything, so that you can build your ideas with the tools they’ve built. There’s aspects of their tools that you aren’t using, and you don’t need to support it at all. Rather than using their versions, you can use custom drawing to do exactly what you need.

Another big part of the appeal of custom drawing is that magic quality of truly custom effects that only your app has.

Last, you can reuse your code. Once you build one custom view for this email item, you can use it over and over in your app.

Where do we start?

If you want to start custom drawing with Canvas, you need a blank Canvas. Everything we’re going to be doing today is with android.graphics.canvas.

This is an interface that Android provides for drawing whatever you want into a bitmap. To draw things on Android, you need four major components:

  1. A bitmap to hold your pixels: That is what Android is going to use to make what you want show up on screen.
  2. A Canvas: This is how you take whatever you want to draw and dictate what pixels you want to show up in your bitmap.
  3. Commands to draw: You can draw shapes, rectangles, circles, paths for complex shapes, text, you can draw other bitmaps into your bitmap. The possibilities are endless.
  4. A paint object to describe how you want those things to look: For example, a word is a word, but to describe how it looks you need a typeface, font size, etc.

How do you get a Canvas?

Since Canvas is a wrapper around a bitmap, you can create a new Canvas and give it an explicit bitmap. I’ve found this is a rare scenario. The only time I’ve used it is when we were interfacing with a printer and we needed to send it a full bitmap with whatever we needed. But more often than not, one will be provided by Android by the framework (whenever it’s relevant and you need it).

Out of the many popular options to get a canvas, the one that we’ll focus on is view.ondraw. When you override that method, Android will give you a Canvas object for that view. You can put whatever you want in it. ViewGroup also has an onDraw() method since it is a view, Drawable has its own draw() method. We’ll go into detail about overriding that and gaining access to that method.

There are the big three but there’s also extra, e.g. item decorations for RecyclerView, when you make a custom one you get onDraw() with the Canvas for the whole RecyclerView. You can draw whatever you want on it and it’ll also give you some information about what’s currently being shown and what your RecyclerView contains.

How do you get to Painting?

Once you have your Canvas, in order to dictate how everything will look, you’ll need to create your own Paint objects. Those will not be provided for you - every single Paint is going to end up having a color; everything is going to have that or nothing. You can have more stuff. On most Paints, I put this anti-alias flag. It ensures that everything you’re drawing is smooth.

Let’s say you’re drawing a circle. When you draw it, if you don’t have this flag, it’ll look pixelated around the edges because we’re trying to draw a curve with little boxes. This will do it based on what you’re drawing and where you’re drawing it. It’ll interpolate those colors so you get one nice smooth curve.

Also, you can give it anything that defines how you want your text to look. For example: the typeface, text size, or text style. When we’re drawing shapes, you can give it a behavior for how you want to draw that shape (e.g. whether you want it filled in or an outline). You can also give it custom shaders.

We have our Canvas. We have our paints. How do we navigate around?

When you have a Canvas, the top left is always going to be (0,0). A Canvas has its own coordinate system. Regardless of where on screen that Canvas will be drawn, when you’re drawing on a Canvas, (0,0) is the top left of that Canvas, not necessarily where you are on the entire screen; what is providing you the Canvas doesn’t matter. That’s something that the overall view hierarchy uses for Android as well.

There’s other scenarios where (rather than using an (X,Y) coordinate) you’ll be using rotational coordinates mostly when you’re drawing arcs or circles. Zero degrees is at three o’clock. Once you declare the number of degrees that you want to draw your arc, you must declare a radius from center to dictate each individual point in that arc. Also, arcs are always drawn clockwise. You declare a starting angle, a total number of degrees to draw the arc, and it will always be drawn clockwise from your starting point.

Example: Bar Chart

Imagine a bar chart. This is a great example because it can fit into any size and reflects different types of data. When tasked with drawing a custom view, break the task down into discrete chunks.

For a simple bar chat, we have three chunks. First, we’re going to want to draw the dark black line on the bottom and the left for our axes. Next, we have 10 guidelines going up (10%, 20%, etc.). Last, we have our bars, i.e. the actual data that we’re trying to represent.

Get more development news like this

If you are working with a designer, they’re probably going to have ideas on how dimensions are defined. Let’s say that we need to draw the graph in many different sizes. When the width changes, we’re going to define how the graph changes by defining some padding around the edge of the graph and we are also going to define the spacing in between each of the bars. That means if we change our width, the bars will fill the rest of the width after we define the spacing between them.

When you override view inside the constructor, initialize everything you’re going to need. Our goal is to keep onDraw() as light as possible so the more you can do up front, the better.

mBarPaint = new Paint();
mBarPaint.setStyle(Paint.Style.FILL);
mBarPaint.setColor(barColor);

We’re going to look at mBarPaint for drawing each individual bar in our graph. This constructor is simple. We want it to fill. We don’t want an outline for our bar. We want the whole thing to be filled with color. Then we’re going to set our color to whatever color we want our bars to be.

mGridPaint = new Paint();
mGridPaint.setStyle(Paint.Style.STROKE);
mGridPaint.setColor(gridColor);
mGridPaint.setStrokeWidth(gridThicknessInPx);

mGuidelinePaint = new Paint();
mGuidelinePaint.setStyle(Paint.Style.STROKE);
mGuidelinePaint.setColor(guidelineColor);
mGuidelinePaint.setStrokeWidth(guidelineThicknessInPx);

Next, we’re going to define our paint for the grid. We’re going to make a new paint. Instead of having it fill some large area, we want to draw lines so we’re going to set it to STROKE. We’re going to set a color. Since we set it to STROKE, we need to set the stroke width. When you’re drawing a line, you’re not necessarily filling an area but that line itself can have variable widths. You can make it thick, if you’d rather draw lines instead of rectangles.

mGuidelinePaint = new Paint();
mGuidelinePaint.setStyle(Paint.Style.STROKE);
mGuidelinePaint.setColor(guidelineColor);
mGuidelinePaint.setStrokeWidth(guidelineThicknessInPx);

We’re going to do the exact same thing for the guidelines going up and everything’s going to look the same here with its own thickness and a different color.

@Override
protected void onDraw(Canvas canvas) {
    final int height = getHeight();
    final int width = getWidth();
    final float gridLeft = mPadding;
    final float gridBottom = height - mPadding;
    final float gridTop = mPadding;
    final float gridRight = width - mPadding;

    ...
}


@Override
protected void onDraw(Canvas canvas) {
    ...

    // Draw Grid Lines
    canvas.drawLine(gridLeft, gridBottom, gridLeft, gridTop,
        mGridPaint);
    canvas.drawLine(gridLeft, gridBottom, gridRight, gridBottom,
        mGridPaint);

    ...
}

Once we have everything initialized up-front we can override onDraw(), which is a straightforward method. We’re going to be given a Canvas directly and this is where we’re going to do all of our drawing.

First, once we start drawing, we need our width and height. Since we broke it down before by how big each individual component is, the space between them, we need to know how much space we have.

Next, we want to start building the dimensions of how we want to draw our graph. We’re defining some padding around the outside. View groups have a padding that you can give it intrinsically. If you want to support padding for your custom view you need to account for it here (unlike margins, which are accounted for by the larger view group).

In this case, we’re going to set up our left, our padding distance from the left side, our bottom will be the total height minus some padding. Remember, our bottom is going to be bigger numbers, our top is zero, our top we’ll have some padding from the top, and the right will have our width minus that padding.

Now that we have the area that we want to draw within, we can start drawing our individual components. Every time you draw something on Canvas, it will be on top of everything you’ve previously drawn.

We’re going to start off by drawing the axes. We need a start X value and a start Y value, and an end X and end Y. We want our line to start on the bottom left and go to the top left; this will be the left side of our grid. We’ll also give it a paint so it knows how the line should look. We already set that up before because you never want to initialize objects in onDraw().

Next, we’re going to do the same thing but we’re going to start from the bottom left. Then we’re going to go to the bottom right. This will be the bottom line of our grid. Now we have our axes.

Next, we’re going to draw our guidelines, which is similar to setting up the area that we want to draw our grid within.

@Override
protected void onDraw(Canvas canvas) {
    ...

    // Draw guide lines
    float guideLineSpacing = (gridBottom - gridTop) / 10f;
    float y;
    for (int i = 0; i < 10; i++) {
        y = gridTop + i * guideLineSpacing;
        canvas.drawLine(gridLeft, y, gridRight, y,
            mGuidelinePaint);
    }

    ...
}

Here we calculate how we’re going to place these lines. We know that we want each line to be on every 10th percent; if we take the total height of our grid (which is going to be our bottom minus our top) and divide it by 10 that’s going to be the spacing between each individual line.

Now that we know that we can start at the top and iterate through, we know we want 10 lines, so we’re going to iterate through this loop 10 times. For our Y position of each guideline, we’re going to start at the top which is either zero or the padding (if we’ve decided we want that). Then we’re going to add some number of spacings. For the first one, it’ll be zero; it’ll be drawn directly on the top of the grid and then we’ll move down until we reach the bottom.

After we have that Y value calculated, we’re going to call drawLine(). We’re going to start on the left with that Y value and then go to the right with that same Y value. This will give us one big horizontal line across the graph. Finally, we give it a paint.

@Override
protected void onDraw(Canvas canvas) {
    ...

    // Draw Bars
    float totalColumnSpacing = spacing * (dataCount + 1);
    float columnWidth = (gridRight - gridLeft - totalColumnSpacing) / dataCount;
    float columnLeft = gridLeft + spacing;
    float columnRight = columnLeft + columnWidth;
    for (float percentage : data) {

        // Calculate top of column based on percentage.
        float top = gridTop + gridHeight * (1f - percentage);
        canvas.drawRect(columnLeft, top, columnRight, gridBottom, mBarPaint);

        // Shift over left/right column bounds
        columnLeft = columnRight + mColumnSpacing;
        columnRight = columnLeft + columnWidth;
    }
}

Lastly, on top of our grid, we’re going to draw our individual bars. First, we’re going to figure out how wide each bar needs to be. We calculate the total amount spacing throughout the graph. We know that each bar is going to having spacing on the left and right so the total spacing is going to be the number of bars we want to draw plus one times our spacing. Now we can get the individual column width by taking our right side minus the left side which is the total width of the area we want to draw the graph in, we subtract the total spacing and then we’ll divide it by the number of column.

Now we have the width of each individual column that we want to draw. The way we’re going to draw this is we’re going to start with the leftmost one and then move it over and over and over (like we did with the guidelines). We know we’re always going to draw from left to right and we’re adjusting the Y.

We’re always going to draw a rectangle from the bottom to some top with a left and right. We’re going to start our left with the left of the grid plus some spacing. The right of that column will be the left plus the width of the column that we expect. Now we can start iterating over and drawing each individual bar.

I have something named data (a step that you’ll need to add when you’re creating this custom view is whatever data you’re trying to display). I find this useful: instead of trying to do all your data transformations within the view, it should all be done by the time you’re drawing. Our data is a bunch of floats (but if you’re not dealing with that, boiling your data down to a simple number that you can do math with is helpful).

The variable here for each individual column determines how tall it will be. We need to calculate what Y value the top of our bar will be. We’re going to start with the top of the grid because that’s the tallest it can be. We’re going to add our height of the grid times one minus our percentage.

If we’re showing 100%, we want the bar to be as tall as the entire graph. One minus 100% is going to be zero - our top will be the top of the grid. If we want to show an empty bar, our percentage will be zero - the top of the grid minus the height of the grid, which means our rectangle has no height. Every value in between is accounted for.

We draw the bars on the graph and our chart is complete. It is important to reiterate: onDraw() is going to get called every single frame that Android needs to render, you need onDraw() to be quick enough so that you can do it in that 16 milliseconds that it takes to calculate a single frame. You want to do as much work upfront as possible.

Extra Credit: Animation

Since we have defined a custom graphic, we can start doing some fun stuff with it.

Let’s say we we want the chart to look empty when you first come into the view, and we want to take those bars from empty and then animate them to their final value. We create a ValueAnimator(), we give it a duration, an interpolator (however you want your animation to look).

You need to add an update listener to your animation. Once we need to animate the data, whether it’s when you first get data or on a button click, we’ll tell our animator we want to animate from zero to one and we’ll start the animation.

Animating from a float of zero to a float of one is powerful. For any animation where you’re modifying some values that you’ve already determined, if you can dictate your beginning state and your end state you know when you have a value of zero you want it to look one way, when you have a value of one, you want it to look another. Then you end up multiplying something by that float for every in between case.

The last bit is this animation update. When we set our update listener, we need to update the fraction for how tall these need to be. We’ll call get animated fraction, and then you have to call and validate. This is the method that will tell your view, something changed, I need to be redrawn, otherwise onDraw() won’t get called again because it doesn’t need to be.

If we go back to our onDraw() when we’re dictating how tall we want our bar to be, we now have another number. Instead of doing one minus whatever percentage height we’re trying to draw, we can multiply that percentage by that float that’s going from zero to one. When the animation starts, all of our percentages will now be zero. At the end, we’ll be multiplying by one so it’s the actual data that we want to show.

View Groups are Views

The next place that you can override onDraw() and start drawing your custom stuff is view groups. View groups are views. Everything a view can do, a view group can do. It also can hold children.

There is an additional step that you need to be able to draw those views however. This is my least favorite method of all time because the name is unnecessarily confusing. You need to call setWillNotDraw(false). If it is false that you won’t draw, it means you will. You have to call this in order to tell whatever view group you’re overriding, I want access to the Canvas, and I’ll be drawing some things on top of it.

Similar to view we have onDraw(). For a view group, it will draw before the children are drawn. Anything you do to the Canvas will happen behind our children.

There are other methods that view group will provide to get access to that Canvas. One of the most powerful ones is dispatchDraw(). This method is how a view group will tell all of its children to draw. If you override this method you get access to Canvas in the same way that you would in onDraw() but you can also control when you call super.dispatchDraw() which will tell your view group to tell all the children to draw. If you want to draw underneath, you can draw a bunch of stuff, then call super to draw the children. After calling super, draw more stuff on the Canva. In that case you’ll start to draw on top of all of the children.

In API 23 Android has an additional onDraw() method; in this case it’s onDrawForeground(). This is useful because it’s used by view groups that have additional stuff to draw on top of the children. For example, scroll view. When you have the little scroll bar on the left that is where your scrollbar is being drawn. If you are implementing a custom scroll view or you want some additional UI based on the state of the view group, you can use this to draw on top of everything.

Drawables

The next object that we can start drawing with a Canvas is in drawables. There’s many ways in Android that you can gain access to these. For example, you can bake in assets into your project, you can create custom XML drawables and you can access all of these either with R.drawable in code or @drawable/ in XML. You’ve more than likely dealt with drawables before.

Drawable is an abstraction for something that can be drawn. A drawable has its own draw() method, similar to onDraw(), dispatchDraw(), etc. It’s a method where you can gain access to a Canvas to do whatever you want with it.

Drawable Methods

One caveat with drawables. The draw() method must have is bounds. When you’re drawing a drawable onto some other Canvas, you need to have bounds to dictate where that drawable will show up.

If we want to override a drawable class there’s a couple methods that you’ll need to define. setBounds() you don’t need to override but it’s useful for figuring out how big your Canvas is. It replaces getWidth() and getHeight() that you have from the view. You can override that or onBoundsChange(), to get access to how big your drawable is and where it’s being drawn on some other Canvas.

You can override getIntrinsicWidth/Height() these methods to dictate the default size for your drawables. For example, if you’re making a notification badge for a number of unread messages, you can dictate how big you want it to be by default. If someone calls setBounds() on your drawable, it will ignore the intrinsic width and bounds. If they don’t, the intrinsic width and height will be the default.

There’s also a couple methods that you have to override that drawable dictates you have to.

The first two are setAlpha() and setColorFilter(). These are called by the system and any paints that you initialize in your drawable class. Both of these methods, you can call directly on your paint objects, so more often than not you can delegate it there. Whatever you’re drawing will reflect the values set here.

The next one is getOpacity(); this is something that the Android system will look at to dictate what visibility you have behind your drawable. You’re going to end up returning one of these three PixelFormat variables. The first one is opaque. This implies you’re using the entire space of your drawable and nothing is visible behind it. Transparent is the opposite: you’re drawing nothing and everything is visible behind it. More often you’re probably going to return translucent, which is somewhere in between: you’re drawing over some of your bounds but not necessarily all of it.

Finaly, the most important method, draw() to which you should be familar.

Example: Letter Drawable

Let’s look at extending a drawable. We’re going to go back to our Gmail example. If you have a profile picture, you can show that (see slides); if you don’t, you can draw one. We’re going to define it. We’re going to take the same steps that we took with view.

First, we want to break it into discrete chunks. We have a circle with a letter inside of it. We want to ask what dimensions are defined, e.g. the circle is going to be some size dictated by a radius, the text will be some size and we want the text to be centered inside of that circle.

public LetterDrawable(Context context, String letter) {
    mLetter = letter;

    mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mCirclePaint.setColor(iconColor);

    mLetterPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
    mLetterPaint.setColor(Color.WHITE);
    mLetterPaint.setTextSize(letterTextSize);
    mLetterPaint.setTextAlign(Paint.Align.CENTER);

    // Text draws from the baselineAdd some top padding to center
    vertically.
    Rect textMathRect = new Rect();
    mLetterPaint.getTextBounds(mLetter, 0, 1, textMathRect);
    mLetterTop = textMathRect.height() / 2f;
}

@Override
public void draw(@NonNull Canvas canvas) {
    canvas.drawCircle(
        bounds.exactCenterX(), bounds.exactCenterY(),
        bounds.width() / 2f, mCirclePaint);
    canvas.drawText(mLetter,
        bounds.exactCenterX(), mLetterTop + bounds.exactCenterY(),
        mLetterPaint);
}

Inside our constructor for our drawable (we’ve overridden the drawable class), we need some letter to draw. We’re going to save that first. Next, we’re going to paint some stuff, so we’re going to create some paints. We’re not drawing lines and rectangles that have no problem being drawn on a square pixel screen. Instead, we’re drawing things with curves, so we’ll want to use this anti-alias flag. All of our curves will be smooth. Also, our circle will have some color.

After that, we’re going to create our letter paint. We haven’t used a text paint yet, most of what you can do with the text paint is very similar to a normal paint. We’ll set the anti-alias flag again, so everything is drawn normally, and smoothly, we’re going to set our color to white.

The text size is exactly what it sounds like. Then we have the text align, e.g. a horizontal alignment (whether you’re going to be normal, which will respect your current locale, either left to right or right to left). We want it to be centered. This text alignment will affect what coordinates you need to use when you’re dictating where to draw your text. If you’re using normal coordinates (e.g. left to right), zero is going to be the left, your X coordinate is going to be the leftmost part of your text. But we’re using center so any X coordinates that we use to dictate where that text is drawn, that will be the center of the text that we’re drawing.

The text alignment is going to affect the X coordinate, but the Y coordinate for drawing text is always going to be based on the baseline. In order to center our text vertically we’ll need to figure out how big our letter is. A text paint has a useful method for this, we have getTextBounds(). Based on your paint, you’re going to give it the string that you care about, a beginning and end index for that string and then a new rect. What it will do is based on the text size and any attributes set on your text paint, it will tell you the total space you need to draw that string with that paint. We’ll use that to get the total dimensions of our letter.

Then we’re going to create a little top padding; rather than drawing the baseline at the top so you can’t see the text we’re going to nudge it down a bit, so it’ll be centered vertical. We defined everything upfront; now when we override our draw() method, there’s not too much to do.

First off we know that we’re going to draw a circle. In this case, there’s not too much to define. We know we need an X coordinate and our Y coordinate for the center of our circle, and then that third thing is our radius. Since we know we’re trying to fit within some bounds, we’re going to go ahead and assume we’re fitting in a square area. Our radius is going to be half of our total width. Last, we’ll give it a paint, so it knows what it needs to look like. After we’ve drawn that circle, we’re going to go ahead and draw our text.

There’s a couple things we need to give it: we need to give it the string that we want to draw, we’ll give it an X and Y coordinate and then our paint. We discussed the coordinates. We want to draw, for our X coordinate we want to give it the center of our space (because we’ve set that center alignment so that’s what dictates where we draw in the X direction). For our Y we’re going to take that top padding which is half of the height of our font and then we’re going to add center Y to it because we’re drawing on the baseline. We want the bottom of our text to be half the height of our text below the center. Then we’ll give it our letter paint so it knows what font to use, what text size, etc.

This is not so bad if you’re drawing a small piece of text, e.g. a label. This can get confusing when you want to draw large pieces of text. It’s possible to draw any text with this tool, but it doesn’t give us the fine-tuned controls of drawing large pieces of text. If we’re drawing a paragraph, we don’t have control over linebreaks or ellipsize in the text if it’s too long in some area, maximum number of lines, etc.

In order to solve that problem we have two great tools we can use, first is android.text.layout and the second is android.text.spannable.

StaticLayout + SpannableString

There’s two types: a static layout and a dynamic layout. The biggest difference is whether your text is going to change. If you’re drawing static text that won’t change, you want to use static layout. If you’re using text that will constantly be changing (e.g. in an edit text) you want to use dynamic.

SpannableString will allow us to dictate the style of the string itself that you want to draw. Not necessarily how it’s drawn in some area, but how each individual letter looks. You can make it bold, italics, you can set style, give it color, highlighting, you can make clickable links–it’s very powerful.

You can use it with text view. It’s useful when we’re drawing our own texts but there are so many applications to SpannableString.

Opposed to controlling the text itself that we want to draw, StaticLayout would control where the entire text is drawn. This will control things like break strategies, ellipsizing, and how much area you have to draw your text within. It also provides a couple convenience methods like the getTextSize() on the text paint that will give you some information about how your text will draw once you pass in a Canvas.

Do we have a Canvas.drawStaticLayout()? This does not exist. Instead, it has its own draw() method. Similar to drawable rather than you overriding it and implementing onDraw() yourself, StaticLayout is going to do that work for you. It’s going to dictate where the text is going to draw, how big it needs to be and will draw itself on the Canvas.

Example: Simple Email

This is my simplified version of Gmail. Let’s define each individual component. We want to draw an icon on the left, a circle with an individual letter in it (this was the previous example so we’ve already made a drawable for this). We know we’re going to have at most one line for the people we’re sending the email to and from. We’ll have a subject and a preview (and it looks like we’re concatting those so if the subject is two lines we’ll append it there). Next, what dimensions are defined. First, we know that the icon, the circle to our left, is going to be a set size. There’s going to be some spacing around it. We know the list of people is going to be a single line, and we know the subject and preview will be up to two lines.

public EmailView(Context context, AttributeSet attrs,
        int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    initDimens(); // Padding, Icon Size
    initPaints(); // From TextPaint, Preview TextPaint
}


private void onNewEmail(Email email) {
    mIconDrawable = new LetterDrawable(getContext(),
        email.getPreviewLetter());
    mIconDrawable.setBounds(sPadding, sPadding,
        sPadding + sIconSize, sPadding + sIconSize);

    ...
}

As we’ve done before in our constructor, we want to initialize all of our dimensions, our padding, our icon size (things that dictate how big everything is). Next we want to initialize our paints. This is everything we’re going to use to draw that stuff onto a Canvas. We’ll have one for the people list and another for the email preview. Everything else before has been static. We want to draw up front. We’ll need to recreate things every time we get a new email.

First, we’ll want to create a new drawable, you could also update this so you could use one drawable and validate it and update the letter that you’re drawing. We’ll create an icon drawable based on the whatever preview letter we want to use in that circle. We have to set the bounds in order for this to draw the right place on this Canvas. The top left will be inset by our padding and our bottom and right will be that top left plus the size of the icon.

Next, we’re going to need to create our layouts so once we get to onDraw(), all the work is done. We’ll need to define how much width we have to draw that text in. We’re going to calculate that by taking the total width of our view and subtract the width of everything on the left: two times the padding and the width of the icon (we have the left side of our text), and then from the right side we’ll subtract the padding on the other side. We have the width that our text can fit within.

Next, this is a nice convenience method that I found in text utilities. We’re going to use this to ellipsize our people list down to one line. This is commaEllipsize: you give it a comma separated list, a paint, the width that you can fit within. These last two strings dictate how you want to ellipsize it. If you have a giant list and there’s plus one, it’ll be plus one. You can give it a format for if - there’s plus eight you can shove the eight in there. That will give you a one line string that fits within that width.

private void onNewEmail(Email email) {
    ...

    final int textWidth = getWidth()
        - 2 * sPadding - sIconSize // left content & padding
        - sPadding; // content and padding on right

    final CharSequence ellipsizedFrom = TextUtils.commaEllipsize(
        email.getCommaSeparatedFromList(),
        sFromPaint, textWidth, "+1", "+%d");
    mFromLayout = new StaticLayout(ellipsizedFrom, sFromPaint,
        textWidth, Layout.Alignment.ALIGN_NORMAL, 1f, 1f, false);
    ...
}

Last, we want to create our static layout. There’s many things you can give static layout. This is the most simple constructor. We’re going to give it the text itself that we want to draw, that ellipsize from text that we made before. We’re going to give it the paint that we want to draw with. This’ll have the size of the text, the text color, etc.. We’ll give it the width that we need it to fit within. This is a vertical alignment.

These next two numbers will dictate line spacing. If you want to draw multiple lines this will dictate how much space is in between each line, one of those numbers is a multiplication (it’ll take whatever value and multiply it by that much). The other one is addition, that addition should be zero. We’re going to take that spacing, multiply it by one (not change it) and add one pixel to it. This last one is whether you want to include your font padding. More often than not you don’t need to worry about this but when we’re drawing the individual letter, this can be useful.

That is our text layout for drawing the people lis. Next, we have our preview text. We’re going to get the entire string. We’re going to take our subject, add the entire body to it, and then that unicode is a long dash. Next up we’re going to create our spannablestring with that text. You can set how you want your text to show up - we’re going to add a span to make the first chunk of this text bold.

private void onNewEmail(Email email) {
    ...

    String previewText = email.subject + " \u2014 " + email.body;
    SpannableString preview = new SpannableString(previewText);
    preview.setSpan(new StyleSpan(Typeface.BOLD),
        0, Math.min(previewText.length(), mEmail.subject.length()),
        Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

    ...
}

We’re going to give it the span itself which dictates what style we want to apply, we’re going to give start and end indices. We want to start at the beginning and either the whole thing (if there’s no body) or the length of the subject. This last integer will dictate how these indices work, whether you want to include your last indice or not. That gives us our string.

We’re going to do a similar thing here for building our layout. We could use that same constructor before, but this is the new way to do it with Android M and above (6.0). Now there’s a nice builder pattern.

We’re going to start off and give it some things we did before, the text we want to draw, a start and end indice and the paint and then the width that we need to fit that text within. We’re going to set our alignment for how we want that text to behave within its area, next, we’re going to give it some behavior on how we want it to ellipsize - we want it to ellipsize at the very end, with the “…” , we want that width is how much area we’re going to fit within. We’re going to give it that width again, and we want to ellipsize after two lines.

On older versions of static layout, the ellipsize option does not work. Find below a link to an example project with all of these in it and that will have some code and some examples on how to handle that ellipsizing for multiple lines before Android M. After we’ve set everything we care about, we’ll go ahead and build our static layout and set it. Now, we have everything we need to draw.

private void onNewEmail(Email email) {
    ...

    mPreviewLayout = StaticLayout.Builder.obtain(preview, 0,
            preview.length(), sPreviewPaint, textWidth)
        .setAlignment(Layout.Alignment.ALIGN_NORMAL)
        .setEllipsize(TextUtils.TruncateAt.END)
        .setEllipsizedWidth(textWidth)
        .setMaxLines(2)
        .build();
}

Next, we’re going to override on draw again.

We have a couple things different: our drawable, rather than telling the Canvas to draw a drawable, the drawable knows how to draw itself. All we need to do is give it that Canvas. The bounds have to be set for this to draw in the right location on that Canvas (which is why we did it ahead of time).

Next, we need to handle how to draw our layout. When we’re drawing individual shapes (e.g. the lines and rectangles we dealt with before and the circle), we can give it coordinates for where it needs to draw on the Canvas. For drawable we give the bounds to accomplish the same thing, text layouts do not have this mechanism. You don’t have a way to tell the layout intrinsically I want you to draw here on any Canvas I give you. Rather than giving it a location to draw on the Canvas, we’re going to shift the Canvas itself.

A good example to think about this is a 3D printer: you have the printer head itself. Instead of moving the output itself, you’re going to move the base underneath it.

First, we’re going to translate our Canvas so the front text will draw in the right place. We’re going to translate it with an X and Y. We know X of the text is going to be two times the padding plus the size of the icon. The Y is just the padding. Once the Canvas has been translated, we can give the Canvas to the layout and it will assume that the Canvas is normal. It’ll draw it on (0, 0). Instead of (0, 0) being (0, 0), (0, 0) is the (X, Y) we just calculated. After we draw the from layout, we haven’t untranslated yet.

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    mIconDrawable.draw(canvas);

    canvas.save();
    canvas.translate(2 * sPadding + sIconSize, sPadding);
    mFromLayout.draw(canvas);
    canvas.translate(0, sFromPaint.getTextSize() + 2);
    mPreviewLayout.draw(canvas);
    canvas.restore();
}

Now we’re going to translate a bit more. We’re not going to move left and right at all, we know the left alignment is going to be same between those two pieces of text. We’re going to shift it down. We’re going to take the text size of the from paint. It’ll be that same height. This is accomplishing the same thing of taking the text bounds and putting it in that rect and using that height. Then we’re going to add some spacing to it. We can give the Canvas to that preview layout and it will draw itself.

We’ve messed with this Canvas, it’s not in the state that we got it before. There’s a very important step that you can’t forget to do. Before and after you mess with your Canvas, we have two methods: save() and restore(). These are like database transactions. The system will remember the initial state of the Canvas when you call save(), and then when you call restore() it will restore the back up you saved. This will let us do whatever we want to the Canvas to draw the text in the right place and then we call restore. Anything else that is drawing on this same Canvas, e.g. a view group, won’t be translated, it can do its own transformations.

Transforming the Canvas

You need to move the Canvas for your static layout to draw in the right spot. We only did translate but you can do a whole mess of stuff to this Canvas. You’re transforming the Canvas by doing matrix calculations. Anything you can do to a matrix, you can do to this Canvas (rotate it, translate it, scale, skew it). It’s fun to mess with a Canvas: draw some text on there and then unmess with it.

You always have to transform your Canvas back, save() and restore() is the way to do this. save() and restore() returns an integer. Every time you call save() that number will increment. There’s another restore() method that you can give it one of those integers. If I want to translate it, draw something, rotate it, draw something else and then skew it and draw something else, I can call save() before each translation and then bounce back to whichever transformation I want to undo.

Something More… Animated

This is the example I did before: you’re moving the base itself instead of the paintbrush. Find the code here. This is more animated - that heart is a complex path object, we’re going to override on touch.

Example: Flying Heart

We take an X and Y coordinate for where to draw this heart and then that wiggle is done by a value animator, similar to the way that we animated those bars in. We’re going to animate from negative 30 to 30. It’s important to break it into chunks, figure out each individual component that you need to draw, and then figure out what you need to define, e.g. the size of the heart and the way that we’re going to animate it.

public TouchTrackingView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setColor(heartColor);
    mHeartPath = HeartPathHelper.getHeartOfSize(Math.round(heartSize));

    mRotationAnimator = new ValueAnimator();
    mRotationAnimator.setDuration(250);
    mRotationAnimator.setIntValues(-30, 0, 30);
    mRotationAnimator.setRepeatCount(ValueAnimator.INFINITE);
    mRotationAnimator.setRepeatMode(ValueAnimator.REVERSE);
    mRotationAnimator.addUpdateListener(this);
}


@Override
public boolean onTouchEvent(MotionEvent event) {
    final int action = event.getAction();
    if (userIsTouchingScreen) {
        // If a finger is down and/or moving, update the location
        mIsFingerDown = true;
        mFingerX = event.getX();
        mFingerY = event.getY();

        // If our Animator isn't running, start it.
        if (mRotationAnimator.isPaused()) {
        mRotationAnimator.resume();
        }

        // Must call invalidate here
        invalidate();

    } else {
        // Stop drawing & animation
    }
}


@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    // If user isn't touching the screen, do nothing.
    if (!mIsFingerDown) {
        return;
    }

    canvas.save();
    canvas.rotate((Integer) mRotationAnimator.getAnimatedValue(),
        mFingerX, mFingerY);
    canvas.translate(mFingerX - mHeartSize / 2f, mFingerY - mHeartSize / 2f);
    canvas.drawPath(mHeartPath, mPaint);
    canvas.restore();

Look at the whole project, it will give you some tips on how to override on touch, when to start and end your animations, when to call and validate. You want to call it only when you’re drawing new stuff; for example, when the user isn’t touching the screen nothing is changing, there’s no reason to invalidate your view and call on draw again because you’re going to draw nothing.

Flying Heart: Reviewing Steps

Same steps here, you’re going to create everything up front. In ondraw you’re going to do as little as possible.

Questions?

Dave Smith’s talk on mastering the Android touch system, it gets complicated once you have views and view groups. Measure, layout, draw, repeat by Huyen Tue Dao also talks about the larger layout structure; instead of overriding some view and drawing on the Canvas, you can also do powerful stuff with the ways your views are laid out, e.g. their position onscreen, relative to other things.

For the static layout and caching I could not find a good talk on the layouts, but I did find this interesting article on taking a recycler view, drawing it in a custom Canvas with that static layout, and start saving those static layouts in an LRU cache taking.

Next Up: Android Architecture Components and Realm

General link arrow white

About the content

This talk was delivered live in July 2017 at 360 AnDev. The video was recorded, produced, and transcribed by Realm, and is published here with the permission of the conference organizers.

Joshua Lamson

Joshua Lamson is a Software Engineer at POSSIBLE Mobile, specializing in Android Development. Joshua is a graduate of the Colorado School of Mines and has an affinity for Rubik’s cubes.

4 design patterns for a RESTless mobile integration »

close