Making Nachos: A Material Chips Library for Android
On the Hootsuite Android team, we recently found ourselves looking for a library that implemented material chips. Unfortunately, most of the libraries that existed were extremely tightly coupled to providing chips for contacts, and we didn’t need any of the contact-related features. All we needed were chips that contained text and an optional icon, and a TextView that could provide suggestions for the user as they were typing. Unable to find an existing solution, we decided to write our own library and open-source it: Welcome to Nachos.
The ArchitectureThe Nachos library centres around the NachoTextView, a custom text field that allows the user to create chips in the text that they are entering.
To maximize the ability for customization we broke the functionality down into several components:
The Nachos library includes a default implementation of each of these three components that we will explore in further detail:
The SpanChipTokenizer adds an extra layer of abstraction through the ChipCreator interface to make it easier to customize just the Chip layer without changing any of the other layers. Nachos includes a default implementation of ChipCreator as well:
The ChipThe first thing we had to do was find a way of displaying a “chip”. This had to include being able to draw a coloured background, the text, and a drawable icon.
This is where we discovered the power of “spans”. An EditText by default uses an Editable to hold its text contents and the Editable interface extends Spannable. Spannable declares methods to add and remove “spans” and extends Spanned which additionally declares methods to search for “spans” within a piece of text. A “span” is an object that gets associated with a particular portion of text within a larger piece of text. These “spans” can be incredibly powerful because they can be any object and they can draw themselves in place of the text that they span. Taking advantage of this second ability, we created the ChipSpan class which inherited from ImageSpan and drew itself as a chip. Overriding the draw method, we were able to display a rounded rectangle for a background, the text, and the optional icon:
One of the biggest challenges we ran into with the ChipSpan class was vertically centring the chip within the line of text while keeping the plain text also vertically centred, and maintaining spacing between the lines.
Solving this problem required a combination of manipulating font metrics and padding. Font metrics are a collection of values that describe the size of a font.
First we had to force each line of text to be as tall as the combined value of the chip height and desired vertical spacing:
To do so we manually set the top, bottom, ascent and descent of the FontMetricsInt object that gets passed to the draw method. This effectively tricks the TextView into thinking the text takes up more vertical space than it actually does, forcing the line height of the TextView to be larger:
However, there was one problem with this approach: until a chip was added, there was no way to modify the font metrics object. This resulted in the TextView height changing when the first chip was added or all the chips were removed. The only way to workaround this was to modify the padding on the TextView depending on whether or not there is a chip present. When there is a chip in the TextView we use the padding set by the user (or the default padding). When there is only plain text, we increase the padding to make up for the smaller height of the text. This way, the total TextView height remains constant.
Distinguishing Between Chips and Plain Text
Once we were able to create chips, we had to be able to identify the difference between a chip and plain text. This is implemented in the SpanChipTokenizer class. There, we surround the chips with the “Unit Separator” character. This is an ASCII control character with the code point 31. We chose this character because it is untypable so it would be impossible for a user to interfere with identifying the chips. We also surround the chip with spaces because keyboards that provide autocorrect suggestions would think that the entire text field is one word unless we insert those spaces. We then attach our ChipSpan over this entire sequence of text. The ChipSpan covers up all the underlying text and displays itself instead:
Managing the Creation of Chips
The creation of chips occurs either in a direct connection between the NachoTextView and the ChipTokenizer or through a ChipTerminatorHandler depending on what triggered the creation of a chip. For events such as losing focus, validating the text, tapping on a suggestion etc. the NachoTextView directly calls the ChipTokenizer to create chips. In an event where the user enters text, the NachoTextView allows the ChipTerminatorHandler to handle the event.
The ChipTerminatorHandler works by searching newly entered text for characters that have been set as “chip terminators”. When a “chip terminator” is set, it can be associated with one of three behaviors:
- Chipify All: A chip terminator associated with this behavior will cause all tokens (unchipped text) to be converted into chips when the terminator is encountered
- Chipify Current Token: A chip terminator associated with this behavior will cause the current token to be chipified (all other tokens will be left unchanged)
- Chipify To Terminator: A chip terminator associated with this behavior will cause the text from the beginning of the current token, up to where the terminator was typed, to be chipified (all other text will be left unchanged)
We then attached a TextWatcher to the TextView so that we could respond to the user typing text. One of the benefits of using a TextWatcher is it captures all changes to the text. This includes things like paste events, so we can insert chips when the user pastes text into the field. However, this also includes programmatic changes to the text. We only wanted to listen to user events, so we had to create a boolean flag that would let us know whether or not we were making a programmatic change to the text:
Showing SuggestionsOne of the critical features we needed in our library was the ability to show suggestions in a drop down list as the user types. Luckily, Android comes with a built-in class to support just that: AutoCompleteTextView. However, we needed to show suggestions for just the current token that the user is typing (not the entire contents of the TextView which may contain text that has been already chipped). Once again, Android had a class designed for that: MultiAutoCompleteTextView, so we made NachoTextView extend that class.
To use MultiAutoCompleteTextView, we have to supply it with an implementation of the Tokenizer interface so that it can identify where the current token starts and ends. Since we already have this functionality in our ChipTokenizer interface we simply created a wrapper class to plug it into the MultiAutoCompleteTextView. To supply the actual suggestions you have to provide it with an Adapter. Then, when a suggestion is tapped, the MultiAutoCompleteTextView calls the terminateToken method of Tokenizer (where we create our chip) and automatically inserts the “terminated” text into the TextView.
This worked perfectly; however, we recognized that there is often data associated with a suggestion, that we would also want to associate with a chip. For example if the suggestions were a list of the user’s contacts, and the user tapped on one of these, we would want the tapped contact to be associated with the chip we create. MultiAutoCompleteTextView gives us one opportunity to intercept the text insertion process: the replaceText method. This method gets called after a suggestion is tapped and it gets passed the text of the suggestion. Unfortunately it does not get passed any information relating to the actual suggestion object (the object that was in the adapter). You can, however, obtain data about the suggestion that gets tapped by attaching an OnItemClickListener to the MultiAutoCompleteTextView. So we overrode the replaceText method and instead of replacing the text there, we did nothing. Then in the OnItemClickListener, we performed the text replacement and attached the data to the chip:
ConclusionNachos was designed to be a simple, easy-to-use implementation of material chips on Android. After a lot of research into the inner workings of TextViews, we were able to create a solution that suited our needs in addition to providing the framework for customization and future enhancement. The library is available on Github. Hopefully you will find it useful!
About the Author
Noah Tajwar is a High School Co-op on the Android team, who will be entering his first year of engineering at the University of British Columbia. In his spare time, he enjoys writing Android/Java applications, playing soccer, and playing piano.