Tags
android, canvas, compound, compound drawable, custom-ui, drawable, getCompoundPaddingLeft, getCompoundPaddingRight, layout, mozilla, onDraw, onMeasure, optimization, optimize, paint, performance, text, text layout, textview, view hierarchy
The pretty basic unit for developing UI in Android is a View
. But if we look closely, View is a UI widget that provides user interaction. It comprises of Drawables
and text Layouts
. We see drawables everywhere — right from the background of a View. TextView
has compound drawables too. However, TextView
has only one layout. Is it possible to have more than one text layout in a View
/TextView
?
Let’s take an example. We have a simple ListView
with each row having an image, text and some sub-text. Since TextView shows only one text Layout by default, we would need a LinearLayout with 2 or 3 views (2 TextViews in them) to achieve this layout. What if TextView can hold one more text layout? It’s just a private variable that can be created and drawn on the Even if it can hold and draw it, how would we be able to let TextView’s original layout account for this layout?
If we look at TextView’s onMeasure()
closely, the available width for the layout accounts for the space occupied by the compound drawables. If we make TextView account for a larger compound drawable space on the right, the layout will constrain itself more. Now that the space is carved out, we can draw the layout in that space.
private Layout mSubTextLayout; @Override public int getCompoundPaddingRight() { // Assumption: the layout has only one line. return super.getCompoundPaddingRight() + mSubTextLayout.getLineWidth(0); }
Now we need to create a layout for the sub-text and draw. Ideally it’s not good to create new objects inside onMeasure()
. But if we take care of when and how we create the layouts, we don’t have to worry about this restriction. And what different kind of Layouts can we create? TextView allows creating a BoringLayout, a StaticLayout or a DynamicLayout. BoringLayout can be used if the text is only single line. StaticLayout is for multi-line layouts that cannot be changed after creation. DynamicLayout is for editable text, like in an EditText.
@Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.getSize(widthMeasureSpec); // Create a layout for sub-text. mSubTextLayout = new StaticLayout( mSubText, mPaint, width, Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true); // TextView doesn't know about mSubTextLayout. // It calculates the space using compound drawables' sizes. super.onMeasure(widthMeasureSpec, heightMeasureSpec); }
The mPaint
used here has all the attributes for the sub-text — like text color, shadow, text-size, etc. This is what determines the size used for a text layout.
@Override public void onDraw(Canvas canvas) { // Do the default draw. super.onDraw(canvas); // Calculate the place to show the sub-text // using the padding, available width, height and // the sub-text width and height. // Note: The 'right' padding to use here is 'super.getCompoundPaddingRight()' // as we have faked the actual value. // Draw the sub-text. mLayout.draw(canvas); }
But hey, can’t we just use a Spannable text? Well… what if the name is really long and runs into multiple lines or need to be ellipsized?
By this, we use the same TextView to draw two layouts. And that has helped us remove 2 Views! Happy hacking! 😉
P.S: The icons are from: http://www.tutorial9.net/downloads/108-mono-icons-huge-set-of-minimal-icons/
Clever use of the compound padding to provide a space for the extra layout without additional layout of the core layout element! One issue though, onMeasure() is a bad place to re-create the StaticLayout because this method gets call A LOT even when the metrics of the view don’t necessarily change. A better choice would be a callback like onSizeChanged() that only triggers when the view metrics are actually updated, and re-create the extra layout at that stage.
It’s true that you shouldn’t “keep creating” objects in onMeasure(), onLayout(), onDraw(), onTouch() etc. But if you look at TextView’s code, they create a new layout inside onMeasure() — because the layout determines the size of the TextView in few cases. That why I had mentioned, “if we take care of when and how we create the layouts, we don’t have to worry about this restriction”. Basically if we track few things like “is the text changed?”, “is the view measured with a new MeasureSpec”?, “was any layout created already?” etc, we can optimize creating new layout/objects inside onMeasure().
It cannot go under onSizeChanged() too. Say you add more things like the sub-text can have a background. If the background’s padding makes the layout bigger the actual text layout’s height, then the TextView depends on the height of the sub-text’s layout. That means, sub-text’s layout should be available during onMeasure().
Like you said, with some extra code it’s possible to make sure the object allocation doesn’t occur too frequently by checking if the text has changed, the view is being measured with the same measure spec, etc.
However, is it in any way possible to reuse the same Layout object instead of repeatedly creating new ones in onMeasure()? Or are all Layout instances immutable once created and not able to be reused again? Figured I would ask, since obviously reusing the same instance is usually the way to avoid object allocations in onMeasure(), onLayout(), etc.
Pingback: Centered Buttons | Sriram Ramani
Hey Sriram, thanks for all of these great articles. Just wanted to point out that you seem to have a typo in the second paragraph: “It’s just a private variable that can be created and drawn on the Even if it can hold and draw it, how would we be able to let TextView’s original layout account for this layout?”