Note: You can find the full source code for
TextDrawable
and the sample project on GitHub.
The Drawable framework in Android is a neat and really flexible way to create
portions of your UI. Many times have I been able to simplify the view hierarchy
or required resources just by getting creating with what a Drawable can do.
Recently, I had a need to place text into a Drawable so it could be inserted in
places where the framework only allows Drawables to go. So I created TextDrawable
and thought I'd share it.
If you would like to take a look at the TextDrawable
source directly, head over
to the GitHub link above. This post deals mostly with the example application that
wraps the implementation.
The Basics
Basically TextDrawable
is exactly that, a Drawable that displays a CharSequence
.
It supports most all of the functionality you would find on TextView
for setting
and formatting text display. It supports multi-line strings (line breaks, etc.)
and is configured to have intrinsic bounds equal to the size required to draw the
text contents as formatted. This means that in most cases, you don't have to explicitly
call setBounds()
on the object, it knows how big it wants to be...similar to working
with Bitmaps.
With this class, text can now be part of the Drawable world, meaning it can not
only be set alone in places where you would normally put an image, it can also be
placed together with other Drawables in containers like StateListDrawable
or
animated with the likes of TransitionDrawable
and ClipDrawable
.
In many cases, we can use this to do a job that would otherwise require multiple
views or compound controls just to achieve a given visual effect; thus it can reduce
overhead in your view hierarchy.
Java Only
One of the major drawbacks of creating your own Drawable implementations is they cannot be inflated using XML (the Android team claims this is a security concern since this code is used by the core platform, so I wouldn't expect it to change anytime soon). This can make them more difficult to work with if you want to include a custom implementation inside a larger composite container (such as a state list or layer list). Note that it is still possible, it just means you must do all your Drawable construction in Java.
Usage Examples
A simple example of using TextDrawable
looks something like this:
ImageView mImageOne;
TextDrawable d = new TextDrawable(this);
d.setText("SAMPLE TEXT\nLINE TWO");
d.setTextAlign(Layout.Alignment.ALIGN_CENTER);
mImageOne.setImageDrawable(d);
This simply display the two-line string supplied, center-aligned, using the default text appearance settings from the application theme.
Path Drawing
TextDrawable
does have one additional feature, in that you can also pass a
Path
object to it if you want the text to be drawn in a particular custom way
(e.g. in a circle or along a curve). In this case, the text measurement code
cannot properly determine the size required, so TextDrawable
will report no
intrinsic size and you will need to call setBounds()
with a size that appropriately
matches the Path you have applied.
ImageView mImageThree;
TextDrawable d = new TextDrawable(this);
d.setText("TEXT DRAWN IN A CIRCLE");
d.setTextColor(Color.BLUE);
d.setTextSize(12);
Path p = new Path();
int origin = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 40, getResources().getDisplayMetrics());
int radius = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30, getResources().getDisplayMetrics());
int bound = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 80, getResources().getDisplayMetrics());
p.addCircle(origin, origin, radius, Path.Direction.CW);
d.setTextPath(p);
//Must call setBounds() since we are using a Path
d.setBounds(0, 0, bound, bound);
mImageThree.setImageDrawable(d);
In this example we apply a circular path to the TextDrawable
on which to draw
the text content. Because we are using a custom path, and TextDrawable
cannot
measure its proper size, our application must also call setBounds()
to the
Drawable
before handing it over to the ImageView
.
Compound Drawable
Another very useful case for this is to place a small piece of static text in the
top, bottom, left, right drawable locations on a TextView
widget.
The following example inserts a short, non-editable, text prefix before whatever
the user types into an EditText
:
EditText mEditText;
d = new TextDrawable(this);
d.setText("SKU#");
mEditText.setCompoundDrawablesWithIntrinsicBounds(d, null, null, null);
If you want to use the same text in multiple locations, you can even set the same
instance in more than one place. Notice how we used the version of this method
that expects intrinsic bounds. If we had used a Path, we would need to explicitly
call setBounds()
and use setCompoundDrawables()
instead.
Note: Since
Button
is also aTextView
, you can use this technique there as well.
Dynamic Drawable
The following example wraps TextDrawable
in a ClipDrawable
for the purposes
of animating the revealing of the supplied text in a more granular fashion rather
than just going letter-by-letter (i.e. typewriter style):
ImageView mImageFive;
d = new TextDrawable(this);
d.setText("SHOW ME TEXT");
ClipDrawable clip = new ClipDrawable(d, Gravity.LEFT, ClipDrawable.HORIZONTAL);
mImageFive.setImageDrawable(clip);
You can then animate this by calling setImageLevel()
repeatedly on the
enclosing container.
These are only some of the examples of what you might be able to simplify in
your application UI by using text as a Drawable
.
Framework Bug
In developing the examples for this, I came across a problem on ICS and Jelly Bean
devices (and probably Honeycomb) having to do with hardware acceleration.
We're all familiar by now with the fact that hardware acceleration is a great
addition to the platform starting in Android 3.0 but that not all drawing primitives
and features are quite yet properly supported there. In many cases Views, Activities,
or entire applications need to disable hardware acceleration to keep compatibility
with their graphics code...this is one of those such cases. It seems that drawText()
or one of its variants is not yet fully supported in hardware.
You may notice if you look at the example code posted with the TextDrawable
implementation that some of the ImageView
instances have a separate scaling mode
applied to illustrate how TextDrawable
responds to being matrix scaled.
The framework matrix scales images by transforming the Canvas on which they are drawn,
and on older devices or with hardware acceleration disabled, this works great.
However, enabling hardware acceleration will cause the text in the scaled cases
to get terribly pixelated. I imagine in the future this will be corrected,
but it's behavior at least on these versions of the platform will forever be this way.
As is common in these cases, the solution is to add hardwareAcceleration="false"
to your manifest or use setLayerType(View.LAYER_TYPE_SOFTWARE, null)
on the view
where you may be doing these transformations.
Tip: Resizing Issues
One thing to be aware of when using Drawables in a dynamic manner (i.e. changing
their contents after they are set on a View) is that most widgets in the framework
do not allow for a drawable to notify the host that it has been resized.
Most widgets like TextView
and ImageView
act as a callback for the Drawable
so it can request, through invalidation, that it be redrawn. However, in most cases
this does not trigger the host View to re-check the Drawable bounds. The initial
bounds it calculated are those you are stuck with. In many cases where we display
Bitmaps, this is not a concern, however if you are expecting to dynamically change
the text of a TextDrawable
after it is attached, you will notice that text will
simply be redrawn inside of the initial bounding box.
There are a few hacks to get this to work if that is your plan.
For example, ImageView
does remeasure its Drawable content whenever setSelected()
is called, so it is possible to force ImageView
to resize the drawable with each
change by calling setSelected(false)
each time as well (assuming this doesn't
affect other code you may have in place). The best solution, however, is to use
the container Drawables in the framework whenever possible to create the dynamic
effects you need.
So there you have it! Now the next time you think to yourself "if I could just put this text into a Drawable..."; now you can!