360andev huyen tue dao share fb

Measure, Layout, Draw, Repeat: Custom Views and ViewGroups

Sometimes, the Android platform layouts and widgets are all you need. And, sometimes, you need more control over design and interaction, or some performance help. Custom Views and ViewGroups are powerful tools, but with great power comes great complexity. In this 360AnDev talk, to help you get started, we will first build a simple custom View and then add layout, drawing, and interaction. Along the way, we will discuss when to and when not to go custom and talk about best practices.


Introduction (00:00)

My name is Huyen Tue Dao. I am an Android developer and have been between six and seven years. I am currently on the Android team at Trello, and I love custom views. I think in the modern parlance, I am a custom view fan-girl. They are one of my favorite parts of Android. It’s weird because the platform comes with all these great widgets and views e.g. button, and text view, and view-pager, and layouts, and linear layouts, and straight layout now, and… Why would you ever want to write your own custom view? There are a couple of good reasons why.

Custom Views. Why and why not? (00:37)

Here are a few reasons why you might consider using custom views:

  • Say you are building an application and a component does not work quite the way you want.
  • You want to build a new, innovative and cool interaction or animation or UI, and you cannot make it work with the current Android components.
  • To make your code a little more reusable, if you have some logic that is cool and you want to use in different parts of your application.
  • To make your app more modular; you have a complex UI, and you want to have good software engineering, good software design, and separate things out.
  • Performance: If you have nested views, or if you have many views period, that can drag your performance down - custom views can help you.

There are also a few reasons why you might not want to use custom views. They are time-consuming, difficult. The great thing about using Android’s platform widgets and views is that you have so much in them: features, accessibility, different interactions, animations, styling, and all the wonderful things that the Android provides us. If you do a custom view, you have to do much of that on your own. That is a disadvantage and custom views will not always be the solution for you.

How Android Draws Views (03:24)

You are going to start off with your XML layouts. Those XML layouts have to be instantiated; they are going to be inflated, and each one of those views that you specify in that XML layout is going to be allocated and come to life. Eventually, you will have your view hierarchy with your views, and everything else.

Once that view hierarchy comes into existence, three things have to happen before it ends up on the screen: measure, layout, draw. Each of these is a traversal of that view hierarchy, and each of them is a depth-first traversal from parent to children to children’s children to children children children, all the way down.

There are three phases to how Android draws views and you, as a developer, have three entry points into this process. For the “measure” phase, there is onMeasure(), for the “layout”, there is onLayout(), and for the “draw”, there is onDraw():

  1. What happens with measure is that starting at the root, and again, going down from parents to children, the parent will figure out some constraints for each of its children on how big they can be. That child will take those constraints and figure out, “given what the parents told me about how big I can be, here is how big I want to be.” Then it keeps that to itself until the parent is ready later on to read that information. In the process of calculating it is own width and height, that child might actually measure any children that it has a well. Again, it is a recursive process.
  2. After measure comes layout. Layout is when a parent positions a child; it will find it a final position and size on the screen.
  3. Finally, draw. Once everything is nicely measured and nicely positioned, they will start drawing. And drawing starts with the parent, the parent draws itself and then basically requests each of its children to do the same. Something interesting to note is that, whoever draws first, ends up lower on the screen. A parent will draw itself, and then when its children draw, they will draw on top of the parent.

Different Ways To Go Custom (07:32)

Now that we now the three different phases of how Android draws views, how can we take that and translate it to custom views?

There are a couple of different ways to approach this. Lucas Rocha did a great article, Custom Layouts on Android, where he explored these different methods, and he gave them different names (I added one, it is not quite as good a name as Lucas’).

Two general ways of creating custom views: extend the existing class, or extend the base view class/group class.

View Custom Extended: The easiest way is extending an existing view class. Arguably, it is the easiest way to get some custom logic or custom behavior out of a view.

View Flat Custom: If you are getting really adventurous, you can do what Lucas calls a flat custom view. Instead of extending a higher level widget, you extend the base U-class. Usually, you will do this when you want to do totally custom drawing logic if you have cool behavior that you want to do all on your own. That is when you use flat custom groups. Flat custom groups are also good for performance. If you have a layout that has much stuff in it, rather than using a bunch of different views to implement that UI or that behavior, you can use a single view to do the same thing. By that, you reduce the number of levels in your view tree, and you reduce the number of views that you are using.

On the ViewGroup side, Lucas talks about a couple of different strategies.

ViewGroup Composite: I have a set of components, buttons, and widgets, and I just want to do something with them. For example, separate them out to make my UI a little more modular; maybe I just have some specific code or business logic that I want to apply to these, maybe I have a set of controls. You can do what is called a “composite view group”: you take your set of widgets and views, and you place them inside a platform layout (e.g. a linear layout or a relative layout), and that is your custom view group, that is where you focus your logic on.

ViewGroup Custom Composite (if you are feeling adventurous!): Rather than taking your set of views and sticking them in a platform’s view group, you are going to create your own view group. That is great if you want to do your own custom layout logic (e.g. do a view group where you lined out all the views in a circle). Custom composite views can also be good for performance. You might have heard that relative layout sometimes is not good for performance because it does measure layout twice. If you are running into problems with that and you say, “I do not want to handle this double layout path, measure layout path thing,” you can create a custom composite view group: you lay out everything specifically and efficiently and help out your performance that way.

Get more development news like this

In this talk, we are going to be adventurous and talk about flat customs or flat custom views, and custom composite views.

What Do You Need To Implement? (10:58)

Measure, layout, and draw: these are your entry points into the view traversals. The secret is that you do not have to implement all three to do custom views and custom view groups. Depending on what you are doing and what you need, you will only have to implement a subset of them.

I am going to do a simple Custom View Example. If you are doing a flat custom view, the only thing you need is onDraw. You need to render that view on the screen, and that is all you need. You do not need “layout” because flat custom views will not have children. You do not technically need onMeasure, but I think it is a good idea: while there is default measure logic in the base U-class, it is not always the best case. If you want to create a cool custom view that is usable, that you can share with your teammates or the world, implementing onMeasure will make that view more reusable and render better in layouts.

So, what I am going to build for you today? A tally counter! (see slide 11). It is so fabulous, a wonderful square with numbers in it, this is exactly what I need custom views for. Because I over-think things (I am an engineer), I have this interface, so these are the methods that I’ll be referring to:


package randomlytyping.widget

/**
  * An interface for a view implementing a tally counter.
  */
public interface TallyCounter {

    /**
      * Reset the counter.
      */
    void reset();

    /**
      * Increment the counter.
      */
    void increment();

    /**
      * @return The current count of the counter.
      */
    int getCount();

    /**
      * Set the counter value.
      */
    void setCount(int count);

}

View Constructors (12:40)

When you are building custom views, the first thing you are going to need is constructors. You have to bring your custom view to life, and override the constructors in the base U-class. There are four of them, but you only need to worry about the first two:

  1. Create new from code View(Context context)

  2. Create from XML View(Context context, AttributeSet attrs)

  3. Create from XML *with a style from theme attribute View(Context context, AttributeSet attrs, int defStyleAttr)

  4. Create from XML *with a style from theme attribute or style resource View(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)

These two constructors are used in different situations:

  1. View (Context context) will take one parameter, a Context - the current context in which the view lives, and it is how that view will access resources, themes, etc. You call this constructor when you are building your view using the new operator.
  2. View(Context context, AttributeSet attrs). Similar to the first one, but it has one extra parameter: it has that same context, but it also has an AttributeSet. That constructor is used when your view is inflated from XML, and that attributes that contains all of those cool layout attribute sets, or attributes that you can specify in XML (e.g. padding and text color and background) all of those end up in the attribute set, and that is how your view can access those.

If you are interested, my coworker Dan wrote a blog called “A Deep Dive Into Android View Constructors.”

Example 1: TallyCounterView (14:00)

Here is my TallyCounterView and the two constructors that I implemented:


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

public TallyCounterView(Context context, AttributeSet attrs) {
    super(context, attrs);
    
    // Set up points for canvas drawing.
    backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    backgroundPaint.setColor(ContextCompat.getColor(context, R.color.colorPrimary));
    linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    linePaint.setColor(ContextCompat.getColor(context, R.color.colorAccent));
    linePaint.setStrokeWidth(1f);
    numberPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
    numberPaint.setColor(ContextCompat.getColor(context, android.R.color.white));
    // Set the number text size to be 64sp.
    // Translate 64sp
    numberPaint.setTextSize(Math.round(64f * getResources().getDisplayMetrics().scaledDensity));
    
    // Allocate objects needed for canvas drawing here.
    backgroundRect = new RectF();
    
    // Initialize drawing measurements.
    cornerRadius = Math.round(2f * getResources().getDisplayMetrics().density);
    
    // Do initial count setup.
    setCount(0);
}

When you are doing custom drawing, you will need a lot of objects. When you do drawing in a view, you are drawing on a canvas, and you are going to be drawing with paint. There is a paint object, and a text paint object: these objects hold properties and styles that you can apply to your drawing operations (e.g. stroke width, fill color, fill mode). We are going to need a couple of paint objects to do our drawing.

TallyCounterView(Context context)

In my constructor, I am instantiating them, setting fill color, and I have my number paint. My numberPaint is a TextPaint object (which I will use to draw my sexy little counter numbers) and, apart from setting a fill color, I am also going to set text size.

Remember that everything is relative to pixels (any drawing operation you do is in pixels). When doing custom views, think in terms of Dp and Sp for flexibility and adaptability of your layouts. When you are doing custom view drawing, translate from Dp’s and Sp’s to pixels - you do that by taking your value in sp (here, I want my text to be 64 sp). I am going to take 64 and multiply it by the scaled density of the display. I am going to access that through the resources, get the display metrics, and get out that skill density (like the ratio). Then, I need to multiply my 64 sp by it to get it to the actual pixel size it needs to be on the screen.

The majority of the drawing operation that you will do takes geometric objects, point, and rectangle, which you should probably do in your constructor.

Finally, any other property that you need. For example, I am going to draw a rounded rectangle, and I have a corner radius. Working with pixels but thinking in Dps, if I want my corner radius to be 2 Dps, I am going to pull out the display metrics, get the density value, and multiply that by my value in Dps. Then I call my set count on my counter to get it all initialized and ready for clicking.

onDraw(Canvas canvas)

The meat of it is the onDraw. I am drawing some light text on a pinkish background with a line under it, and the text is centered in the canvas:


@Override
protected void onDraw(Canvas canvas) {
    // Grab canvas dimensions.
    final int canvasWidth = canvas.getWidth();
    final int canvasHeight = canvas.getHeight();
    
    // Calculate horizontal center.
    final float centerX = canvasWidth * 0.5f;
    
    // Draw the background.
    backgroundRect.set(0f, 0f, canvasWidth, canvasHeight);
    canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, backgroundPaint);
    
    // Draw baseline.
    final float baselineY = Math.round(canvasHeight * 0.6f);
    canvas.drawLine(0, baselineY, canvasWidth, baselineY, linePaint);
    
    // Draw text.
    
    // Measure the width of text to display.
    final float textWidth = numberPaint.measureText(displayedCount);
    // Figure out an x-coordinate that will center the text in the canvas.
    final float textX = Math.round(centerX - textWidth * 0.5f);
    // Draw.
    canvas.drawText(displayedCount, textX, baselineY, numberPaint);
}

First (for convenience), I am pulling out the canvas width and canvas height. That canvas that gets passed to you in onDraw is what you are going to do your drawing operations on.

To get the center of my text, I am going to calculate the center of my canvas. To start, I am going to draw the background. I am going to call drawRoundRect (a method you can call on that canvas object). I am going to calculate the bounds of my rectangle (the bounds of that canvas) and pass into drawRoundRect this rectangle that defines the bounds, my corner radiuses, and that paint that I instantiated earlier. That should draw my background.

Similarly, for the line I am drawing, I’m using the drawLine method, passing in a coordinate that I calculated along with that line paint, and finally, drawing the text. Drawing the text is more interesting: for me to center that text on my canvas, I have to know how wide that text has to be. I am taking advantage of the TextPaint measure text method, which will take a string and, based on the properties of that text paint, will let you know how many pixels wide that text is going to be. Then I can calculate where I need to position my text to get it centered, and finally, I draw text.

Reflecting State Changes (18:42)

That is all I needed to do to draw this absolutely gorgeous tally counter (see slides 16). That is how you draw a view and get it to appear on the screen. I have a click button, and that click button should increment my counter… but it does not. Whenever that button is clicked, I am calling set count on my counter. What is wrong?

  • When I set the counter, I am changing the state. When I changed the state, I did not draw the counter again to reflect that state (something that you have to remember when you are doing custom views). Anytime the state of that object/view changes, you have to be able to reflect that in the drawing. To do that, you are going to use the invalidate method. That invalidate method says to Android, “This view is dirty,” to note that drawing is no longer valid and no longer correctly reflects the state of that view. It will let Android know that this needs to be drawn.
  • Android will draw it again at “some point in the future”. When you mark a view as dirty, needing to be drawn, any views that intersect that view on the screen will also get re-drawn. It is great, and this is how you ask for your views to be redrawn.
  • It is also interesting to look at an alternate invalidate() method, which takes four integer values: left, right, top, bottom, which allows you to specify a specific region that is dirty. If you were looking at my counter, anything that only ever changes is that text in the middle. If I wanted to be more precise, I could figure out a bounding box for that text and then pass that to invalidate.

In order to get my counter to be a counter, I had to add this invalidate call to my setCount method:


//
// TallyCounter interface
//

@Override
public void reset() { setCount(0); }

@Override
public void increment() { setCount(count + 1); }

@Override
public void setCount(int count) {
    count = Math.min(count, MAX_COUNT);
    this.count = count;
    this.displayedCount = String.format(Locale.getDefault(), "%04d", count);
    invalidate()
}

@Override
public int getCount() { return count; }

//
// View overrides
//

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

Then, I click my button: it is counting!

Things To Remember When Drawing (20:29)

Some things to remember, stuff that I did wrong that I would love to share with you, so you don’t do it:

  • Do not allocate objects in onDraw. Ever, ever ever, or onDraw will get called a lot. It gets called every time your view needs to be drawn. If you are allocating objects in that, you can imagine how much memory you are going to use if you keep allocating objects in onDraw.
  • Invalidate() only when needed. Drawing and the invalidation process is expensive. If you can save other views of being drawn by using the invalidate method with the four coordinates that do not intersect other views (and other views can chill on their own and be cool), then you can save processing and draw time.
  • Text is positioned at the baseline. The baseline of text is where that text sits, but there are pieces of text sometimes that go under that line. If you draw a W, the W sits on the baseline, but the little stem sticks out. When I first started drawing all my text always seemed to be higher than it meant to be - that is because, when you position text, you position text at (0, 0). It is not the bottom of the text that is at (0, 0) - it is that baseline.

Again: you draw in pixels, but think in dp and sp, to make sure that your custom views are nice and adaptable to different layouts and sizes.

The secret in this layout is that there is a button below my counter, and you cannot see it. Because it does not implement onMeasure, the counter does not know how big it should be.

That is why I tend to implement onMeasure. Sticking this view in this layout (see slides 21) means that that poor button is all lost in the land of nowhere. It is always good to implement onMeasure - you can make your views more flexible and “layout-able”.

How Measurements Are Made (23:00)

Child defines LayoutParams in XML or Java

It is similar to a negotiation between that view and its parent. The child and the parent communicate to each other what they want in different ways. The child communicates with the parent view through LayoutParams() (how wide/high does it want to be) and we use layout parameters by calling layout_width and layout_height in your XML.

You can do it in Java code, but that is how the child tells the parent how it wants to be laid out. Whether your layout is being inflated from XML, or whether you are doing everything by hand in Java, at some point you are going to call setLayoutParams on that child. At some point later, that parent is going to call getLayoutParams to retrieve those layout parameters.

Parent calculates MeasureSpecs and passes to child.measure()

Some time later, in the parents onMeasure, that parent will look at that child’s layout parameters and calculate how big that child should be.

If there are other children, that parent needs to figure out how big that child gets to be, based on the available space and the layout parameters that that child requested. In the parent’s onMeasure, it is going to figure all this out, and it encodes these constraints, how big each view can be through MeasureSpec values. MeasureSpec value is an integer, and it encodes two things, a mode, and a size. And the combination of these two can tell a child: “You can be at most 300 dp,” or “you can be exactly 500 dp,” or “your size is unspecified, be as big as you want.”

These MeasureSpecs are calculated by the parent, one for width and one for height, and they get passed when the parent calls measure on that child.

Child calculates width/height, setMeasuredDimension()

When the child onMeasure executes, it gets those two MeasureSpecs. Then, it is up to the child to look at these constraints, these MeasureSpecs, and then look at its content and figure out how big it wants to be (or it can be), but still conforming to those parent constraints. Next, it is going to call setMeasuredDimension(), which is storing that measured width and height, that desired width and height, in that view. Remember that setMeasuredDimension() is mandatory when you are implementing onMeasure. If you do not call it at some point, you are going to get a Runtime Exception (and you will be sad).

Parent calls child.layout(), final child size/position

Once that child has figured out it is measured width and height, that parent will start laying out children. The final phase in measuring that child is setting a final size and position. The parent does that when it calls layout on the child.

A child, in a way, has two sets of dimensions: its measured dimension, its measured height, or its measured width and measured height. You call getMeasuredWidth and getMeasuredHeight to get those values, final width, and height on the screen, and you call getWidth and getHeight on the child to get those values.

View overrides (26:11)

I am going to focus on what the child needs to do when it gets those constraints from the parent.

My counter view. Let’s think about this at a high-level at first. I wanted the counter to be big enough to fit that text, so I am going to be measuring text. I also wanted to have padding. I wanted people to use those padding attributes and have it add a little more space to the top left and right of that view.


...

//
// View overrides
//

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    final Paint.FontMetrics fontMetrics = numberPaint.getFontMetrics();
    
    // Measure maximum possible width of text.
    final float maxTextWidth = numberPaint.measureText(MAX_COUNT_STRING);
    // Estimate maximum possible height of text.
    final float maxTextHeight = -fontMetrics.top + fontMetrics.bottom;
    
    // Add padding to maximum width calculation.
    final int desiredWidth = Math.round(maxTextWidth + getPaddingLeft() + getPaddingRight());
    
    // Add padding to maximum height calculation.
    final int desiredHeight = Math.round(maxTextHeight * 2f + getPaddingTop()  + getPaddingBottom());
    
    // Reconcile size that this view wants to be with the size the parent will let it be.
    final int measureWidth = reconcileSize(desiredWidth, widthMeasureSpec);
    final int measuredHeight = reconcileSize(desiredHeight, heightMeasureSpec);
    
    // Store the final measured dimensions.
    setMeasuredDimension(measuredWidth, measuredHeight);
}

To start with, I am going to measure the text, and, if you are working with text and drawing text a lot, you are going to get friendly with the font metrics. The font metrics is an object that lives in many different places, but each text paint object will be able to give you a font metrics object. Font metrics has all properties related to the text; ascent, decent, top, bottom, these are all notions about how high the text may be above the baseline and how low it might be below the baseline.

I am going to measure the text. When you are working with onMeasure, and you are trying to calculate the content, or the size of your content, it is really good to think about edge cases of upper bounds. In this case, rather than just measuring the text that I have right I am going to measure the maximum text that I could render, the maximum value I can render (e.g. here - 9,999) and I created a string out of that. Rather than measuring the count again as it is now, I am going to measure that to make sure that I always am fitting the biggest value. I am going to measure that, and get a text width out of it again, using that measure text method on the text paint, and I am not going to measure the height.

Measuring text height in Android is a little funky, and that is understating it. Sometimes, it does not work; many times you have to use your best guess and estimates. Here, instead of measuring the text, I am going to use some fontMetrics, some font properties. I am going to use the top, which is the maximum distance above the base line that any glyph in that font can be, and I am going to use the bottom, which is the opposite value. I am going to combine those and say, “that is probably the biggest that text is ever going to be.

Next, to those two values, I am going to add some padding. I am going to take those two text measurements that I calculated; I am going to add in the padding. Then, I am going to call get padding left, right, get padding top and bottom, and add those into that measured text width and height.

Once I get that, I need to think for a minute because the parent said that I can only be a certain width and certain height, but I want to be this width and height. How do we reconcile that? There is a method in the view class itself called resolve size, and resolve size will take a content size, how big of you wants to be, and it will take a MeasureSpec, how big that parent told the child it could be, and it will reconcile them. It takes all that work out of you, and it is something that you should definitely use if you are doing your onMeasure implementation.

Reconcile size (29:33)

I cheated: I actually re-implemented it (because the resolve size is a bit complicated). For the purposes of this talk, I re-implemented and called it reconcileSize:


/**
 * Reconcile a desired size for the view contents with a {@link android.view.View.MeasureSpec}
 * constraint passed by the parent.
 *
 * This is a simplified version of {@link View#resolveSize(int, int)}
 *
 * @param contentSize Size of the view's contents.
 * @param measureSpec A {@link android.view.View.MeasureSpec} passed by the parent.
 * @return A size that best fits {@code contentSize} while respecting the parent's constraints.
 */
private int reconcileSize(int contentSize, int measureSpec) {
    final int mode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    switch(mode) {
        case MeasureSpec.EXACTLY:
            return specSize;
        case MeasureSpec.AT_MOST:
            if (contentSize < specSize) {
                return contentSize;
            } else {
                return specSize;
            }
        case MeasureSpec.UNSPECIFIED:
        default:
            return contentSize;
    }
}

...

It looks at that mode that the parent gave you, and if that mode is EXACTLY, then the method says, “you are going to be exactly the size that the parent wants to be”. If it was AT_MOST, then it checks the size that the view wanted to be versus the size the parent says “you can be and take whatever is smaller.” If that mode was UNSPECIFIED, then the method will say, “you can be whatever size that is, that you want it to be, that content size.”

This is my measured counter, and it looks better (see slide 29). It fits the text nicely, it is not hogging all the screen, and there is a button that was underneath that has appeared now. I am sure that button is glad that we implemented onMeasure, and you will be as well. The view has now a better notion of how big it can be; I can probably use this in different places without it taking over the whole layout.

What Do You Need To Implement? (30:36)

Let’s talk about Custom ViewGroups. View groups are views as well, but when you are implementing a custom view group, it works differently: a view group has children, whereas the flat custom view could be selfish and only worry about itself.

A custom view group has to worry about its children - that is the whole point of the view group: grouping and placement. The methods that you have to implement to get your custom view group off the ground are different:

  • You have to implement onMeasure() because you have to be able to call measure on each of your children.
  • You have to implement onLayout(), because you do have to call layout on each child to get it positioned on the screen. If you do not call layout on the child, they become zero width, zero height, and zero zero land: they disappear.
  • You do not have to implement onDraw(). In fact, the default behavior in view group is not to draw. There is a flag called setWillNotDraw, and it is true by default. I am not sure why you would ever really need to draw on top of view group because the point of the view group is grouping and placement. If you do, make sure that you set WillNotDraw to false.

Example #2: Custom List (33:09)

I am going to switch gears: I have a list with some items (see slide 31), and this list item structure is familiar in Android. Image view, text, and more text. You could probably do this with a relative layout, but maybe you are having some performance problems, or maybe you just want to play around with custom views and view groups.

As an aside, when you take the dive into custom views and view groups and you are intimidated, it is good to start with straightforward structures/layouts. If you have a layout that has static content, or if you have a layout that you can describe very simply. Here I have an image, some text to the left, some text below: that is a very straightforward layout, and it is a good way to practice and get into using and implementing onMeasure and onLayout.

Here is the XML layout for my custom list items, and its class:


<randomlytyping.widget.SimpleListItem>
...
android:paddingStart="0dp"
android:paddingEnd="@dimen/activity_horizontal_margin"
android:background="?attr/selectableItemBackground"
android:clickable="true">

<ImageView
    android:id="@+id/icon"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="@dimen/list_item_icon_margin_start"
    android:layout_marginEnd="@dimen/list_item_icon_margin_end"
    android:contentDescription="@null" />
    
<TextView
    android:id="@+id/title"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    style="@style/TextAppearance.AppCompat.Subhead" />
    
<TextView
    android:id="@+id/subtitle"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    style="@style/TextAppearance.AppCompat.Body1"
    android:textColor="?android:attr/textColorSecondary" />

</randomlytyping.widget.SimpleListItem>


/**
 * Custom {@link ViewGroup} for displaying the example list items from the launch screen but
 * without the extra measure/layout pass from the {@link android.widget.RelativeLayout}.
 */
public class SimpleListItem extends ViewGroup {
    //
    // Fields
    //
    
    @BindView(R.id.icon)
    ImageView icon;
    
    @BindView(R.id.title)
    TextView titleView;
    
    @BindView(R.id.subtitle)
    TextView subtitleView;
    
    //
    // Constructors
    //
    
    /**
     * Constructor.
     *
     * @param context The current context.
     * @param attrs The attributes of the XML tag that is inflating the view.
     */
    public SimpleListItem(Context context, AttributeSet attrs) { super(context, attrs); }
    ...
}

I have the image view, the text view, and another text view. It looks like you might do with a relative layout, except that the route tag is SimpleListItem and that is the name of my custom ViewGroup that extends. That is a composite view group. I have very specific views in the layout that I am bringing into the class.

Custom Layout Parameters (33:35)

The onMeasure of parents, of view groups, are a little bit different. They are more complicated because they have to worry about children, along with themselves. There are a couple of different things that I want to talk to you about that you should probably look into when you are doing onMeasure for view groups. This is not required, but it is really good to do: layout parameters. There is a ViewGroup.LayoutParams class and it encodes a width and height (that is the default layout parameters that you use with a straight up view group). It is not linear layout - a linear layout has width and height, but it also has some cool other things (e.g. weight and gravity) that are not covered by the view group LayoutParams class. If you are creating a custom view group because you want to do something cool with a layout (e.g. you want to do a really interesting algorithm to lay out your views), consider creating a custom LayoutParams class - you can encode different modes and properties that your users can set.

What is interesting is margins. We use margins all the time to get everything positioned nicely. Margins are not in the base LayoutParams class; if you use a base view group and just use it straight up, margins will not work. But there is a MarginLayoutParams subclass inside of ViewGroup to use margins within your custom views. You have to start letting the view group know that this the layout params that you are going to use when you are instantiating.

There are four methods to get margin LayoutParams or your own custom LayoutParams to be used by your custom ViewGroup:

  1. checkLayoutParams(), a validation method, it checks whether a particular ViewGroup.LayoutParams distance is valid to use with that ViewGroup. If I stuck a relative layout, specific layout params, in a linear layout, checkLayoutParams() would say, “that is not what we use around here.” There are three methods that generate layout params.
  2. generateDefaultLayoutParams() is called when ViewGroup gets a view that has no layout parameters on it, it can have some default layout parameters.
  3. generateDefaultLayoutParams(ViewGroup.LayoutParams p) - takes ViewGroup.LayoutParams as a parameter. This is used when a view gets a bad layout parameter (e.g. pass some relative layout parameters into a linear layout). This method will give you a chance to take that layout parameter (e.g. that invalid layout parameter object), and pull out stuff that you can use. You can pull out the width the height and any other properties that you can use in your specific layout parameter’s object, and you create a new, valid version using some of those properties from the bad one.
  4. generateDefaultLayoutParams(AttributeSet attrs), a generate layout params that takes an attribute set. AttributeSet is our friend from XML inflation, and it is generating a set of layout params based on attributes specified in XML.

If you want to use MarginLayoutParams in your custom view groups, or if you want to create your own cool layout params, you need to implement these four methods.

Useful Measurement Methods (37:05)

Implementing a parent’s onMeasure can be difficult: you have all these children, they want to be different sizes/be laid out different ways, you only have so much space/padding and you need to make sure that everybody’s layout parameter is satisfied to the best of your ability, while still respecting that parent’s own width MeasureSpec and height MeasureSpec. That sounds complicated, but there are methods that will do it for you inside of view group.

1) Measure Spec + Padding There is two methods that will take a width MeasureSpec from the parent, take a height MeasureSpec from the parent, and take a view, read its layout parameters, and figure out, based on that view group, that view group’s own constraints, and take that into account, but still give that child those layout parameters to the best of its ability, and it is called measureChild(...). There is measureChildren(...), which takes measure child and calls it on all of the children in that view group. BUT, If you like margins, these first two methods do not take margins into account.

2) Measure Spec + Padding + Margins Much of that has to do with the fact that margins are not handled by default. There is this method called measureChildWithMargins(...), it has met a child, but in that calculation and reconciling of what the parent can be versus what the children want to be, it will take the margins into account. It will pull out the margins from that child and add them into the calculations.

3) Measure Spec + Child Layout Parameters You might not even need to use this method, but is handy in case you want to do your own stuff: getChildMeasureSpec(...). All that complicated logic about reconciling how big the parent can be versus how big the children want to be, that is inside of getChildMeasureSpec(...). It does the hard work of figuring out a MeasureSpec to give those children, based on what the view group can be and what the children want to be. That is called within measure child and measure child with params, but if you want to end up doing your own, you can call on that method.

Example 2

Back to my list item: here is my limitation of the layout params methods. I want to use margins on my simplest item - this is what I have to do to get it. It is simple, not too much code, being able to generate that margin layout param’s object in these four different situations:


/**
 * Validates if a set of layout parameters is valid for a child this ViewGroup.
 */
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    return p instanceof MarginLayoutParams;
}

/**
 * @return A set of default layout parameters when given a child with no layout parameters.
 */
@Override
protected LayoutParams generateDefaultLayoutParams() {
    return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}

/**
 * @return A set of layout parameters created from attributes passed in XML.
 */
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new MarginLayoutParams(getContext(), attrs);
}

/**
 * Called when {@link #checkLayoutParams(LayoutParams)} fails.
 *
 * @return A set of valid layout parameters for this ViewGroup that copies appropriate/valid
 * attributes from the supplied, not-so-good-parameters.
 */
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
    return generateDefaultLayoutParams();
}

The meat of it is onMeasure. I have an icon, a title, a subtitle: I am going to measure them, and use measure child with margins that I get those margins wrapped in the calculation. As you can see, it is not too bad:


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    // Measure icon.
    measureChildWithMargins(icon, widthMeasureSpec, 0, heightMeasureSpec, 0);
    
    // Figure out how much width the icon used.
    MarginLayoutParams lp = (MarginLayoutParams) icon.getLayoutParams();
    int widthUsed = icon.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
    
    int heightUsed = 0;
    
    // Measure title
    measureChildWithMargins(
        titleView,
        // Pass width constraints and width already used.
        widthMeasureSpec, widthUsed,
        // Pass height constraints and height already used.
        heightMeasureSpec, heightUsed
    );
    
    // Measure the Subtitle.
    measureChildWithMargin(
        subtitleView,
        // Pass width constraints and width already used.
        widthMeasureSpec, widthUsed,
        // Pass height constraints and height already used.
        heightMeasureSpec, titleView.getMeasuredHeight());

    // Calculate this view's measured width and height.

...

The first thing I want to do is, because the icon, my image view, is sitting here and the title and the subtitle sit to the side of it, that icon has number one priority in terms of measurement. I am going to measure that icon first. When you call measureChild, or you call measure child with margins, you pass in, along with the view and the parent’s MeasureSpecs, other integer values. One integer value is called widthUsed, and the other one is called heightUsed. As you measure your children, you will start taking up space, that parent will have less and less space to doll out and the width used and height used is a way for you to track and provide it information to the measure child and measure child with margins method that it can take those into account for you.

The icon goes first; it gets free reign at all the space; it passes in (0, 0) for width use and height use and passes in the parent’s width and height MeasureSpec. Once we measure that icon, we want to figure out how much width it used. We are going to call the getMeasuredWidth, get that icon’s desired width and add to it, the margins. The measured width takes into account the content and the padding. Now I am adding in the left and the right margin to see how much space that icon needs within that layout, including itself and its margins.

Now I have to measure the title. The title sits to the right of the icon; it has a little bit less horizontal space; I am going to pass in the width use by the icon. But the title gets free reign of the height; I am going to pass in (0, 0) for the height. I am going to call measureChildWithMargins, I can take the margins of the title into account and I am going to have (once that is done), the getMeasuredWidth and getMeasuredHeight of the title. Finally, subtitle. The poor subtitle, it goes last, it just gets whatever’s left over. I am going to have to calculate the width used by the icon, the height used by the title, and then I feed those into the call for measureChildWithMargins for the subtitle and then, after that is all done, I finally have the getMeasuredWidth and height for the child, or for the subtitle.

The children know, or the children of this view group know how big they want to be. The parent still has to calculate that, the parent’s still the view, so it still has to figure out what its measuredWidth and height’s going to be. All these lines of code are taking everything I did up top and adding it all together, the measured width of the icon and the measured width of the title, figuring out whether the title’s bigger than the subtitle, what the width of that has to be, and putting all that together into a width and height.


@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    MarginLayoutParams layoutParams = (MarginLayoutParams) icon.getLayoutParams();
    
    // Figure out the x-coordinate and y-coordinate of the icon.
    int x = getPaddingLeft() + layoutParams.leftMargin;
    int y = getPaddingTop() + layoutParams.topMargin;
    
    // Layout the icon.
    icon.layout(x, y, x + icon.getMeasuredWidth(), y + icon.getMeasuredHeight());
    
    // Calculate the x-coordinate of the title: icon's right coordinate +
    // the icon's right margin.
    x += icon.getMeasuredWidth() + layoutParams.rightMargin;
    
    // Add in the title's left margin.
    layoutParams = (MarginLayoutParams) titleView.getLayoutParams();
    x += layoutParams.leftMargin;
    
    // Calculate the y-coordinate of the title: this ViewGroup's top padding +
    // the title's top margin
    y = getPaddingTop() + layoutParams.topMargin;
    
    // Layout the title.
    titleView.layout(x, y, x + titleView.getMeasuredWidth(), y + titleView.getMeasuredHeight());
    
    // The subtitle has the same x-coordinate as the title. So no more calculating there.
    
    // Calculate the y-coordinate of the subtitle: the title's bottom coordinate +
    // the title's bottom margin.

...

Finally, I am going to call that resolve size, the actual resolve size on the platform, to take that measured width, all that added up width and height of the icons and the title and subtitle. I am going to pass all that to resolve size, pass in the parent’s actual width and height MeasureSpec and then it will spit those out, and I can pass that to set measure dimension. Now I know how big that parent wants to be.

This is the result (see slide 39). In some lines of code, I get a nice looking, simple list item. I could have done it faster if I used relative layout, but it was not too bad. Measuring and laying out children in a view group can be verbose, it can be long, much code, but it is straightforward. Much of it is being able to articulate how things need to be laid out and turning that into code; turning that into reusable view groups that are performant and that can do cool things.

Where To Go From Here (47:11)

A veritable expert on accessibility is Kelly Schuster. Something else that is cool to do with custom views is custom drawables and drawable states. Ryan Harter did a great post on custom drawables, and Marcos Paulo Damasceno gave a great talk. If you are interested in doing custom graphic work, custom drawables, and making things that are cool and reusable in different places, check those out.

I also recommend exploring all the different drawing operations. You can do much with canvases. Romain Guy knows a lot about how the canvases and how the drawing/architecture, works. I read his old blog posts constantly. Also, Chuki gave a great presentation on using filters and shaders - these are different ways of adding different flavor and coloring when you are drawing.

If you are worried about performance, you can start looking to hardware acceleration and learn about hardware layers and display lists.

Eugenio Marletti knows a lot about custom views and display lists and making things efficient. He works on the team at Clue, an app that does custom view work (great app that if you are trying to get into heavy custom view work).

I highly encourage you to start playing with custom views. It will teach you about the Android platform, how views & layouts work, and it will give you this opportunity to do cool and interesting things with the platform. I probably do them in more situations than I need to, but I encourage you to give it a shot because it is a cool way to explore the possibilities with Android.

Next Up: Realm Everywhere: The Realm Platform Reaches v2.0

General link arrow white

About the content

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

Huyen Tue Dao

Huyen Tue Dao is an Android developer and Google Developer Expert, currently working on the Trello Android app at Atlassian. She is also co-creator of the “Android Dialogs” YouTube channel. Huyen lives in Denver, CO though is often found in the DC Metro area. When she is not up late programming, she is often found up late gaming (video, board, card, anything).

4 design patterns for a RESTless mobile integration »

close