Tags
android, attributes, custom attributes, custom-ui, fade, faded text view, gradient, layout, linear gradient, mozilla, paint, shader, text layout, textview
In the dub-dub-dub browser world, there’s a lot of need for the dot-dot-dot. The pages titles are exponentially long, to make sure Google indexes them, and they don’t fit properly on a small screen. For e.g., CNN shows a very long title that cannot be shown comfortably in a ListView
‘s row. Firefox used to show ellipsis and truncate the text in such cases. However, there are a couple of problems: An ellipsis in the middle cuts the text off in such a way that the context is lost; an end ellipsis is not good from a design point of view. To mitigate this problem, we wanted a TextView
that fades in the end. (Design philosophy: Show a small path, and leave the imagination to the viewer).
To show a faded TextView
, we need to know when, how and how much to fade. The amount of fading needed can be exposed as an attribute, say fadeWidth
for the custom view. When to fade part becomes easier if we can constrain the TextView
to a single line and see if the text runs longer than available width. The XML for such a view would look like:
<com.sriramramani.widget.FadedTextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:singleLine="true" android:ellipsize="none" sirius:fadeWidth="30dp"/>
Every TextView
is backed by a Layout
, that holds the information on how to draw the text on the screen. This includes the number of lines, text in each line, the line ascent, baseline, etc. Unfortunately, the getWidth()
on the layout returns a very huge number. Fortunately, we know that the TextView
will have only one line. Using getLineWidth(0)
, we can find the width of the first line — the entire text.
// ... somewhere in the custom text view. Layout layout = getLayout(); if (layout.getLineWidth(0) > getMeasuredWidth()) { // add fading. } else { // don't add fading. }
That was simple! How do we add a fading effect to the end? The LinearGradient
can take a range of colors and stops, and can be added as a Shader
to a Paint
. Voila!
// ... somewhere in the custom text view. float start = 0.0f; float end = getMeasuredWidth(); // mFadeWidth is obtained from the attribute. float stop = ((float) (end - mFadeWidth) / (float) end); // Setup the linear gradient. LinearGradient gradient = new LinearGradient(0, 0, end, 0, new int[] { color, color, Color.TRANSPARENT }, new float[] { 0, stop, 1.0f } Shader.TileMode.CLAMP);
Let’s build the entire custom view now.
public class FadedTextView extends TextView { // Width of the fade effect from end of the view. private int mFadeWidth; // Add other constructors too. public FadedTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FadedTextView); mFadeWidth = a.getDimensionPixelSize(R.styleable.FadedTextView_fadeWidth, 0); a.recycle(); } @Override public void onDraw(Canvas canvas) { int width = getMeasuredWidth(); // Layout doesn't return a proper width for getWidth(). // Instead check the width of the first line, as we've restricted to just one line. if (getLayout().getLineWidth(0) > width) { // Always get the current text color before setting the gradient. int color = getCurrentTextColor(); // [0..stop] will be current text color, [stop..1] will be the actual gradient float stop = ((float) (width - mFadeWidth) / (float) width); // Set up a linear gradient. LinearGradient gradient = new LinearGradient(0, 0, width, 0, new int[] { color, color, Color.TRANSPARENT }, new float[] { 0, stop, 1.0f }, Shader.TileMode.CLAMP); getPaint().setShader(gradient); } else { getPaint().setShader(null); } // Do a default draw. super.onDraw(canvas); } }
This doesn’t take care of compound drawables and their padding. But that’s pretty straightforward as there are methods to access their properties. And that’s how we kill the ellip…sis.
Update: Thanks to Romain Guy! This can be achieved by using android:fadingEdgeLength
and android:requiresFadingEdge="horizontal"
(in ICS). On pre-ICS phones, it’s enabled using android:fadingEdge
. Sigh! So much work for something existing! 😦
What if the last characters are, say, small punctuation and a space or two? Will it be possible for the user to discern there is any further text without careful inspection?
why didn’t you use the TextViex android:ellipsize=”marquee” property?
It wont actually marquee the text when the view doesn’t have the “selected” state, and it gives you the same result without having to implement your own view.
There was a lot of hatred against marqueueue. I went ahead with implementing my own. 🙂
You could also simply use View’s fading edges. It’s optimized to avoid creating new gradient objects like you do on every draw.
Aah! I didn’t know this. It works like a charm. But what sort of optimizations are done here?
I tried changing everything to “fadingEdge”. The lists started stuttering. When I turned profiling on, the results went alarming. Here’s the screenshot of profiling with fadingEdge: http://cl.ly/image/1X2j3d0r261C and here’s the one with my approach: http://cl.ly/image/002L1s1f342R
Did you finally manage to do the Android “default” method to apply fading edge with a good performance? I was having the same problem as you mentioned on the last comment using the Android approach… I tried out yours and the difference in performance is huge. My lists works now like a charm and before they were so so so stuterring.
Thanks a lot 🙂