5

Background

There are plenty of ways to style a part of the text that's shown in a TextView, such as setting its foreground color (here), and others (here).

The problem

I can't find out if there is a way to set a rectangular-dashed/dotted-line-outline on a partial text of a TextView. Something like this:

enter image description here

What I've tried

I tried to look for such a solution, and also I tried to read the documentation of CharacterStyle . Still, I don't see any of the available spans as good candidates for this style.

The question

Is there a built in solution for this, or do I need to use a customized implementation?


I've used a modified version of what was suggested below , and it worked fine on POC, but for some reason, on the real project, the vertical dashed lines on the sides of the text are bold:

enter image description here

Here's the current code:

string to use

<string name="text_to_format">test &#160;&#160;%1$s test</string>

usage code

        final String textToDash="DASHED";
        String formattedStr = getString(R.string.text_to_format, textToDash+ "<bc/>");
        Spanned textToShow = Html.fromHtml(formattedStr, null, new TagHandler() {
            int start;

            @Override
            public void handleTag(final boolean opening, final String tag, Editable output, final XMLReader xmlReader) {
                switch (tag) {
                    case "bc":
                        if (!opening)
                            start = output.length() - textToDash.length();
                        break;
                    case "html":
                        if (!opening)
                            output.setSpan(
                                    new DrawableSpan(ResourcesCompat.getDrawable(getResources(), R.drawable.dashed_border_shape, null)),
                                    start, start + textToDash.length(), 0);
                }
            }
        });
        textView.setText(textToShow);

DrawableSpan

public class DrawableSpan extends ReplacementSpan {
    private Drawable mDrawable;
    private final Rect mPadding;

    public DrawableSpan(Drawable drawable) {
        super();
        mDrawable = drawable;
        mPadding = new Rect();
        mDrawable.getPadding(mPadding);
    }

    @Override
    public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
        RectF rect = new RectF(x, top, x + measureText(paint, text, start, end), bottom);
        mDrawable.setBounds((int) rect.left - mPadding.left, (int) rect.top - mPadding.top, (int) rect.right + mPadding.right, (int) rect.bottom + mPadding.bottom);
        canvas.drawText(text, start, end, x, y, paint);
        mDrawable.draw(canvas);
    }

    @Override
    public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
        return Math.round(paint.measureText(text, start, end));
    }

    private float measureText(Paint paint, CharSequence text, int start, int end) {
        return paint.measureText(text, start, end);
    }
}

res/drawable/dashed_border_shape.xml

<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle">
    <padding
        android:bottom="1dp"
        android:left="4dp"
        android:right="4dp"
        android:top="1dp"/>
    <solid android:color="@android:color/transparent"/>
    <stroke
        android:width="2dp"
        android:color="#ff474747"
        android:dashGap="10px"
        android:dashWidth="10px"/>
</shape>

The textView doesn't have anything special:

            <TextView
                android:id="@+id/..."
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginBottom="20dp"
                android:layout_marginLeft="8dp"
                android:layout_marginRight="8dp"
                android:layout_marginTop="30dp"
                android:gravity="center_horizontal"
                android:textSize="20sp"/>

I've even set android:clipChildren="false", android:clipToPadding="false" for multiple parents of this view (thinking that it doesn't draw as it tries to). Nothing helped.

How come, and what should I do to fix it?

Community
  • 1
  • 1
android developer
  • 114,585
  • 152
  • 739
  • 1,270
  • I really don't think there is a built in solution... You are going to have to write a custom background for your TextView... You can make a drawable with only the borders – Leandro Borges Ferreira Jan 30 '17 at 15:25
  • @LeandroBorgesFerreira Was assuming this would be it. But how do I do it? I want it for a part of the text, and not the entire of it (see image). The rectangle should be arround specific text within the TextView. Only way I know of is using spans, but I don't see any of them being suited for this task. – android developer Jan 30 '17 at 15:37
  • It has to be programatically or in XML? – Leandro Borges Ferreira Jan 30 '17 at 15:41
  • @LeandroBorgesFerreira Well, in this case, the text is from XML, but the part that needs to have the rectangle is a placeholder. For example : "Hello %s" . – android developer Jan 30 '17 at 15:52
  • @android developer what went wrong with the proposed solution – Charuක Feb 13 '17 at 16:45
  • 1
    @Charuක I don't know. For some reason, it worked fine on a POC, but when I tried on a large project, the left and right dashed lines became wider (see screenshot). That's why I've set a bounty. – android developer Feb 13 '17 at 19:05

1 Answers1

23

Solution 1

1 - Create a drawable for the dashes. Like this:

<shape android:shape="rectangle" xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:radius="5dp" />
    <solid android:color="@android:color/transparent" />
    <stroke
        android:color="@android:color/black"
        android:dashWidth="20px"
        android:dashGap="10px"
        android:width="3dp"/>
</shape>

2 - Set it as the background of your text view, it can be just a word.

<LinearLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:padding="8dp">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello!"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/button_shape"
        android:padding="4dp"
        android:text="world!"/>

</LinearLayout>

The result:

enter image description here

Important note: This solution is going to work only for small text, like show a score in a game or small messagens. It won't adapt to big texts.

Solution 2

If you need a more complex solution that works for big texts, you can use a Spannable.

1 -> Create a custom ReplacementSpan

  public class DashedBorderSpan extends ReplacementSpan {

    private Drawable mDrawable;
    private int mPadding;


    public DashedBorderSpan(Drawable drawable, int padding) {
        super();

        mDrawable = drawable;
        mPadding = padding;
    }

    @Override
    public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
        RectF rect = new RectF(x - mPadding, top - mPadding, x + measureText(paint, text, start, end) + mPadding, bottom + mPadding);

        mDrawable.setBounds((int) rect.left, (int)rect.top, (int)rect.right, (int)rect.bottom);

        canvas.drawText(text, start, end, x, y, paint);
        mDrawable.draw(canvas);
    }

    @Override
    public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
        return Math.round(paint.measureText(text, start, end));
    }

    private float measureText(Paint paint, CharSequence text, int start, int end) {
        return paint.measureText(text, start, end);
    }
}

2 -> Apply the Spannable

TextView textView = (TextView) findViewById(R.id.textasd);

        String hello = "Dashed!";

        SpannableStringBuilder stringBuilder = new SpannableStringBuilder();

        stringBuilder.append(hello);
        stringBuilder.setSpan(new DrawableSpan(getDrawable(R.drawable.dashed_border_shape)),
                0,
                stringBuilder.length(),
                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

        stringBuilder.append("not dashed... boring");

        textView.setText(stringBuilder);

This solution will work for all the cases. It's a better solution, although it is more complicated.

Example If you would like to use it with a place holder use it like this:

    String someText = "Some Text!";

//R.string.placeholder = Hello: %s
            String formatedText = String.format(getString(R.string.placeholder), someText);

            SpannableStringBuilder stringBuilderPlaceHolder = new SpannableStringBuilder();
            stringBuilderPlaceHolder.append(formatedText);

            stringBuilderPlaceHolder.setSpan(new DashedBorderSpan(getDrawable(R.drawable.dashed_border_shape), 10),
                    formatedText.length() - someText.length(),
                    formatedText.length(),
                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

            textViewPlaceHolder.setText(stringBuilderPlaceHolder);

Result: enter image description here

This way the span will be set only on your place holder. If you have a more complex holder use the same logic to achieve what you need.

Edit

There's a small problem with the solution 2, but there is a solution.

You must take care with padding of the dashed border drawable. If you use padding in the dashed border, you will need to set padding in the TextView that uses the Span. In the image that the author of the question provided, you can see that the upper and bottom lines got cut (if you increase the padding, the lines will be completly gone), in order to avoid this use padding in your textview. Like this:

<TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginBottom="20dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginTop="30dp"
        android:paddingTop="3dp" <!-- This will fix the problem!  -->
        android:paddingBottom="3dp" <!-- This will fix the problem!  -->
        android:gravity="center_horizontal"
        android:text="blabla"
        android:textSize="20sp"/>

This will fix the problem =] Happy coding!

Leandro Borges Ferreira
  • 12,422
  • 10
  • 53
  • 73
  • You are right... It won't work in a large text... If you want a solution for this that works directly in the text you will need to create a custom Spannable. This gives a little direction: http://stackoverflow.com/questions/19292838/android-spannablestring-set-background-behind-part-of-text – Leandro Borges Ferreira Jan 31 '17 at 13:41
  • I have a few questions: 1. What's the use of "mBitmap" ? 2.What will happen when the text goes to the next line (wrap, so it takes more than one line) ? 3.Is it possible to use it with a placeholder of "%s" which is inside a string resource, so that only the text that replaces the placeholder will be dashed? – android developer Feb 01 '17 at 08:22
  • 1 -> It useless, I removed it, check the edition. 2 -> Nothing will happen, the dashed word will still be dashed. 3 -> Yes, but you need to set the start and end of the setSpan properly – Leandro Borges Ferreira Feb 01 '17 at 12:10
  • 1. ok. 2. I meant if it's more than one word, meaning that one word will be at the end of the line, and the next on the start of a new line. 3. Can you please demonstrate it? I think I had a similar issue in the past: http://stackoverflow.com/a/38285592/878126 . I solved it by putting a customized XML tag, that once it's found, I set the span on it. If you have a better solution, please suggest what to do. – android developer Feb 01 '17 at 14:23
  • 4. I've noticed that the dashed rectangle is too close to the text, so I tried to set padding ( – android developer Feb 01 '17 at 15:19
  • 2 -> The text will adapt if you try do to so. Just keep in mind that if you try to set a Span too large it will break you text. Keep it small. 3 -> Check my edition, now you can see an example with place holder. 4 -> I included padding in the Class. It works ok, but you have to set padding in your TextView xml, so the text doesn't get truncated. – Leandro Borges Ferreira Feb 01 '17 at 15:49
  • 2. ok. 3. This is not a good solution, as it very depends on the content of the text. It won't work, for example on "Hello %s , how are you?" . I think I prefer my solution for this. 4. I meant padding to the drawable itself (in its XML file). You can call "getPadding" on the given drawable. This way you have specific padding for each direction . In any case, I tried both your sample codes, and they showed the dashes incorrectly (right side truncated), instead of the text you've shown on your screenshots, no matter how much padding I put. Here's the project: https://ufile.io/7ad731 . – android developer Feb 02 '17 at 08:51
  • 4. The weird thing is that on another project that I use the code, the dashes do display on all sides, yet the left dash seem thicker for some reason. Please have a look at the project I've put, and tell what you think – android developer Feb 02 '17 at 08:52
  • 1
    If you remove the top and bottom padding, the problem "goes away". The problem is that the padding is making the border go beyond the available space... If you change the top padding to 2dp you gona see that the upper border goes away completly. I am trying to solve this – Leandro Borges Ferreira Feb 15 '17 at 19:19
  • It works, but is there any way to avoid the padding? I've already set the parent to avoid clipping (using android:clipChildren="false", android:clipToPadding="false") , so why doesn't it work? – android developer Feb 16 '17 at 09:17