Tags

, , , , , , , , , , , , ,

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).

A TextView that fades if there is more text to show.

A TextView that fades if there is more text to show.

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! 😦

Advertisement