如何在TextView中对部分文本设置矩形虚线/点线轮廓?

5

背景

有很多方法可以为TextView中显示的一部分文本设置样式,例如设置其前景色(这里)以及其他方式(这里)。

问题

我无法确定是否有一种方法可以在TextView的部分文本上设置矩形虚线/点线轮廓。类似于这样:

enter image description here

尝试过的方法

我尝试寻找这样的解决方案,并且我还尝试阅读CharacterStyle的文档。但是,我没有看到任何可用范围适用于此样式。

问题

是否有内置的解决方案,还是需要使用自定义实现?


我使用了建议的修改版本下面的,在POC上运行良好,但由于某种原因,在实际项目中,文本两侧的垂直虚线粗体显示:

enter image description here

以下是当前代码:

要使用的字符串

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

使用代码

        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>

textView 没有什么特别之处:

            <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"/>

我甚至为这个视图的多个父级设置了android:clipChildren="false",android:clipToPadding="false"(认为它不会绘制)。但什么也没用。

为什么会这样?我该怎么做才能解决这个问题?


我真的不认为有一个内置的解决方案... 您将需要为TextView编写自定义背景... 您可以创建一个仅具有边框的可绘制对象。 - Leandro Borges Ferreira
@LeandroBorgesFerreira 原以为这就是答案。但我该怎么做呢?我想对一部分文本进行操作,而不是整个文本(请参见图片)。矩形应该围绕TextView中特定的文本。我知道的唯一方法是使用Span,但我没有看到任何一个适合这个任务的Span。 - android developer
需要以编程方式还是XML格式? - Leandro Borges Ferreira
@LeandroBorgesFerreira 好的,在这种情况下,文本来自XML,但需要有矩形的部分是占位符。例如:"Hello %s"。 - android developer
@Android开发者,提出的解决方案出了什么问题? - Charuක
1
@Charuක 我不知道。由于某种原因,在一个 POC 上它可以正常工作,但是当我在一个大项目上尝试时,左右虚线变得更宽了(见截图)。这就是为什么我设置了赏金的原因。 - android developer
1个回答

23

解决方案 1

1 - 为破折号创建一个可绘制对象。像这样:

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

将其设置为文本视图的背景,可以只是一个单词。

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

结果:

enter image description here

重要提示: 此解决方案仅适用于小型文本,例如显示游戏中的得分或小型消息。 它不适用于大型文本。

解决方案2

如果您需要一个适用于大型文本的更复杂的解决方案,可以使用 Spannable。

1-> 创建一个自定义 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 -> 应用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);

这个解决方案适用于所有情况。它是更好的解决方案,尽管它更复杂。

示例 如果您想将其与占位符一起使用,请像这样使用:

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

结果: 输入图像描述

这样,跨度将仅在您的占位符上设置。如果您有一个更复杂的占位符,请使用相同的逻辑来实现所需的效果。

编辑

解决方案2存在一些小问题,但有解决方法

您必须注意虚线边框可绘制对象的填充。如果在虚线边框中使用填充,则需要在使用跨度的TextView中设置填充。在问题作者提供的图像中,您可以看到上下线被切断(如果增加填充,则线将完全消失),为了避免这种情况,请在您的TextView中使用填充。就像这样:

<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"/>

这将解决问题 =] 祝编码愉快!


你是对的... 它在大文本中不起作用... 如果你想要一个直接在文本中工作的解决方案,你需要创建一个自定义的Spannable。这里提供了一些指导:https://dev59.com/amIk5IYBdhLWcg3wbdnx - Leandro Borges Ferreira
2 -> 如果您尝试调整文本,则文本将自适应。只需记住,如果您尝试设置太大的跨度,它将破坏您的文本。保持小巧。 3 -> 检查我的版本,现在您可以看到一个带有占位符的示例。 4 -> 我在类中包含了填充。它可以正常工作,但您必须在TextView xml中设置填充,以便文本不会被截断。 - Leandro Borges Ferreira
奇怪的是,在我使用该代码的另一个项目中,破折号在所有方向上都会显示,但左侧的破折号似乎更粗一些。请查看我放置的项目,并告诉我您的想法。 - android developer
1
如果您移除顶部和底部的填充,问题就会“消失”。问题在于填充使边框超出了可用空间... 如果您将顶部填充更改为2dp,您会发现上边框完全消失了。我正在尝试解决这个问题。 - Leandro Borges Ferreira
它能工作,但有没有办法避免填充?我已经将父级设置为避免剪切(使用android:clipChildren="false",android:clipToPadding="false"),那为什么它不起作用呢? - android developer
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接