安卓TextView轮廓文字

103

有没有简单的方法让文本具有黑色轮廓? 我有一些文本视图将具有不同的颜色,但其中一些颜色在我的背景上并不是很清晰,所以我想知道是否有一种简单的方法来获取黑色轮廓或其他能够完成任务的东西? 我宁愿不用创建自定义视图并制作画布之类的东西。


8
阅读此问题并考虑使用"刷画笔解决方案"的任何人,请注意在Android 4.4中存在一个涂笔bug。如果文字大小超过256像素,则会导致非常奇怪的涂笔渲染。一种解决方法是使用 此答案中提供的另一种方法来绘制轮廓/涂笔。我不想在每个“涂笔”类型的回答上都发这个信息,所以在这里放置它,让大家知道并避免他们经历我经历的痛苦。 - Tony Chan
1
可能是以下问题的重复:在Android TextView中添加不透明的“阴影”(轮廓) - juergen d
16个回答

103

可以使用TextView中的阴影来实现轮廓效果:

    android:shadowColor="#000000"
    android:shadowDx="1.5"
    android:shadowDy="1.3"
    android:shadowRadius="1.6"
    android:text="CCC"
    android:textAllCaps="true"
    android:textColor="@android:color/white"

1
这应该是最好的解决方案。太棒了!谢谢。 - Renan Bandeira
9
这并不会生成轮廓,因为它只显示在两个侧面上。 - ban-geoengineering
完美对我来说!! - Ely Dantas
4
这个阴影对于轮廓来说非常弱。 - user924
这不是一个大纲。这是一个影子。完全不同。 - undefined

57

稍晚了一些,但是MagicTextView可以做文本轮廓,以及其他功能。

输入图像描述

<com.qwerjk.better_text.MagicTextView
    xmlns:qwerjk="http://schemas.android.com/apk/res/com.qwerjk.better_text"
    android:textSize="78dp"
    android:textColor="#ff333333"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    qwerjk:strokeColor="#FFff0000"
    qwerjk:strokeJoinStyle="miter"
    qwerjk:strokeWidth="5"
    android:text="Magic" />

注意:这是我做的,我发布更多内容是为了未来的旅行者而不是为了原始发帖人。它有点像垃圾邮件,但因为与话题相关,可能还算可以接受?


1
你好,我们如何在EditText中输入的文本上添加类似这样的边框? - TilalHusain
关于EditText有什么想法吗? - Piotr
dreamText.setStroke(4, Color.BLACK); dreamText.setTextColor(Color.WHITE); 我正在使用这些设置,但我的文本颜色是透明的,然而我可以看到黑色轮廓。出了什么问题? - Muhammad Umar
没问题,但它并没有真正添加边框。它实际上是将文本取出边缘作为边框,这不会产生相同的视觉效果。 - Warpzit
2
这个解决方案会导致onDraw递归调用,因为在onDraw内部调用了setTextColor - Sermilion

56

3
请注意,在Android 4.4版本中有一个涉及笔画的错误(bug),如果文本大小超过256像素,则会导致非常奇怪的笔画渲染效果。解决方法是使用此答案中介绍的备用方法来绘制轮廓/笔画。 - Tony Chan
这条注释是指的TextView还是字体大小? - John
阴影不够好,白色文本在白色背景布局上仍然看起来非常糟糕,带有黑色阴影。 - user924

29

虽然这是一个老问题,但我仍然没有看到完整的答案。因此,我发布了这个解决方案,希望有人在处理这个问题时会觉得有用。最简单、最有效的解决方法是覆盖TextView类的onDraw方法。我看到的大多数实现都使用drawText方法来绘制描边,但这种方法并未考虑所有格式对齐和文本换行等问题。结果经常导致描边和文本出现在不同的位置。以下方法使用super.onDraw来绘制文本的填充和描边部分,因此您不必担心其他问题。 这里是步骤:

  1. 扩展TextView类
  2. 重写onDraw方法
  3. 将paint样式设置为FILL
  4. 调用父类onDraw以渲染填充模式下的文本。
  5. 保存当前文本颜色。
  6. 将当前文本颜色设置为描边颜色。
  7. 将paint样式设置为Stroke
  8. 设置描边宽度
  9. 再次调用父类的onDraw方法,以在先前呈现的文本上绘制描边。

package com.example.widgets;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.widget.Button;

public class StrokedTextView extends Button {

    private static final int DEFAULT_STROKE_WIDTH = 0;

    // fields
    private int _strokeColor;
    private float _strokeWidth;

    // constructors
    public StrokedTextView(Context context) {
        this(context, null, 0);
    }

    public StrokedTextView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public StrokedTextView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        if(attrs != null) {
            TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.StrokedTextAttrs);
            _strokeColor = a.getColor(R.styleable.StrokedTextAttrs_textStrokeColor,
                    getCurrentTextColor());         
            _strokeWidth = a.getFloat(R.styleable.StrokedTextAttrs_textStrokeWidth,
                    DEFAULT_STROKE_WIDTH);

            a.recycle();
        }
        else {          
            _strokeColor = getCurrentTextColor();
            _strokeWidth = DEFAULT_STROKE_WIDTH;
        } 
        //convert values specified in dp in XML layout to
        //px, otherwise stroke width would appear different
        //on different screens
        _strokeWidth = dpToPx(context, _strokeWidth);           
    }    

    // getters + setters
    public void setStrokeColor(int color) {
        _strokeColor = color;        
    }

    public void setStrokeWidth(int width) {
        _strokeWidth = width;
    }

    // overridden methods
    @Override
    protected void onDraw(Canvas canvas) {
        if(_strokeWidth > 0) {
            //set paint to fill mode
            Paint p = getPaint();
            p.setStyle(Paint.Style.FILL);        
            //draw the fill part of text
            super.onDraw(canvas);       
            //save the text color   
            int currentTextColor = getCurrentTextColor();    
            //set paint to stroke mode and specify 
            //stroke color and width        
            p.setStyle(Paint.Style.STROKE);
            p.setStrokeWidth(_strokeWidth);
            setTextColor(_strokeColor);
            //draw text stroke
            super.onDraw(canvas);      
           //revert the color back to the one 
           //initially specified
           setTextColor(currentTextColor);
       } else {
           super.onDraw(canvas);
       }
   }

   /**
    * Convenience method to convert density independent pixel(dp) value
    * into device display specific pixel value.
    * @param context Context to access device specific display metrics 
    * @param dp density independent pixel value
    * @return device specific pixel value.
    */
   public static int dpToPx(Context context, float dp)
   {
       final float scale= context.getResources().getDisplayMetrics().density;
       return (int) (dp * scale + 0.5f);
   }            
}
那就是全部内容了。该类使用自定义XML属性来允许在XML布局文件中指定描边颜色和宽度。因此,您需要在“res”文件夹下的子文件夹“values”中的attr.xml文件中添加这些属性。将以下内容复制并粘贴到您的attr.xml文件中即可。
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="StrokedTextAttrs">
        <attr name="textStrokeColor" format="color"/>    
        <attr name="textStrokeWidth" format="float"/>
    </declare-styleable>                

</resources>

完成上述步骤后,您便可在XML布局文件中使用自定义StrokedTextView类,并指定描边颜色和宽度。以下是一个示例:

<com.example.widgets.StrokedTextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Stroked text sample"
    android:textColor="@android:color/white"
    android:textSize="25sp"
    strokeAttrs:textStrokeColor="@android:color/black"
    strokeAttrs:textStrokeWidth="1.7" />

记得使用你项目的包名替换掉这里的包名。同时,在布局文件中添加xmlns命名空间,以便使用自定义的XML属性。你可以在布局文件的根节点中添加以下行。

xmlns:strokeAttrs="http://schemas.android.com/apk/res-auto"

2
多么优秀、优雅的解决方案啊!我已经实现了它,而且它运行良好。我只是将textStrokeWidth更改为一个尺寸(和a.getDimensionPixelSize)。谢谢! - dgmltn
1
做得很好,谢谢。由于大纲接管了整个文本,我改变了顺序:先绘制大纲,然后再绘制文本。 - mdiener
2
非常好用。不要使用Android Studio的设计视图来测试轮廓,因为表示不够准确。刚刚浪费了2个小时调试一个无关紧要的问题。 - llmora
10
这个解决方案会导致无限次的onDraw调用,因为setTextColor会调用invalidate。 - Guliash
3
实际上,@Guliash是正确的。经过测试,一旦调用了这个方法,由于在setTextColor的内部机制中嵌入了invalidate()调用,它会导致无限循环调用自身。除非你想复制每一行来自TextView的代码到你自己的类中,否则我能看到的唯一解决方法是使用反射强制访问TextView的私有字段mCurTextColor。参见此答案 大致了解如何做到这一点。只需使用field.set(this, colorInt)而不是使用field.get()即可。 - VerumCH
显示剩余8条评论

23

这个框架支持text-shadow,但不支持text-outline。但是有一个技巧:阴影是半透明的并且会褪色。多重绘制阴影几次,所有alpha值就会被累加起来,结果就是一个轮廓。

一个非常简单的实现方法是扩展TextView并覆盖draw(..)方法。 每次请求绘制时,我们的子类会绘制5-10次。

public class OutlineTextView extends TextView {

    // Constructors

    @Override
    public void draw(Canvas canvas) {
        for (int i = 0; i < 5; i++) {
            super.draw(canvas);
        }
    }

}


<OutlineTextView
    android:shadowColor="#000"
    android:shadowRadius="3.0" />

3
非常感谢。然而我更倾向于使用这个方法:'@Override protected void onDraw(Canvas canvas) { for (int i = 0; i < 5; i++) { super.onDraw(canvas); } }'。请注意,此处为代码翻译,不会提供解释或其他内容。 - JoachimR
1
额外信息:必须至少实现带有Context和AttributeSet的构造函数。否则会遇到java.lang.NoSuchMethodException: <init> [class android.content.Context, interface android.util.AttributeSet]的错误。 - Bevor

15

我一直在尝试弄清楚如何做到这一点,但在网上找不到好的指南,最后终于想出了方法。正如Steve Pomeroy所建议的那样,你确实需要做更多的事情。为了获得描边文本效果,您需要绘制两次文本:一次使用粗描边绘制,然后第二次我们在描边上面绘制主要文本。但是,任务变得更容易,因为您可以非常容易地对SDK中提供的代码示例之一进行调整,即SDK目录下名为“/samples/android-/ApiDemos/src/com/example/android/apis/view/LabelView.java”的代码。该示例还可以在Android开发者网站此处找到。

根据您的需求,很容易看出您只需要对该代码进行轻微修改,例如将其更改为扩展自TextView等。在发现此示例之前,我忘记了覆盖onMeasure()方法(除了覆盖onDraw()方法外,在Android Developer网站的“构建自定义组件”指南中也提到),这也是我遇到问题的原因之一。

完成此操作后,您可以像我一样:

public class TextViewOutline extends TextView {

private Paint mTextPaint;
private Paint mTextPaintOutline; //add another paint attribute for your outline
...
//modify initTextViewOutline to setup the outline style
   private void initTextViewOutline() {
       mTextPaint = new Paint();
       mTextPaint.setAntiAlias(true);
       mTextPaint.setTextSize(16);
       mTextPaint.setColor(0xFF000000);
       mTextPaint.setStyle(Paint.Style.FILL);

       mTextPaintOutline = new Paint();
       mTextPaintOutline.setAntiAlias(true);
       mTextPaintOutline.setTextSize(16);
       mTextPaintOutline.setColor(0xFF000000);
       mTextPaintOutline.setStyle(Paint.Style.STROKE);
       mTextPaintOutline.setStrokeWidth(4);

       setPadding(3, 3, 3, 3);
}
...
//make sure to update other methods you've overridden to handle your new paint object
...
//and finally draw the text, mAscent refers to a member attribute which had
//a value assigned to it in the measureHeight and Width methods
   @Override
   protected void onDraw(Canvas canvas) {
       super.onDraw(canvas);
       canvas.drawText(mText, getPaddingLeft(), getPaddingTop() - mAscent, 
           mTextPaintOutline);
       canvas.drawText(mText, getPaddingLeft(), getPaddingTop() - mAscent, mTextPaint);
   }
所以,为了获得描边文本的效果,您需要绘制两次文本:第一次是使用粗描边绘制描边,然后第二次我们在描边上绘制主要文本。

15

感谢@YGHM添加阴影支持 在这里输入图像描述

package com.megvii.demo;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;

public class TextViewOutline extends android.support.v7.widget.AppCompatTextView {

// constants
private static final int DEFAULT_OUTLINE_SIZE = 0;
private static final int DEFAULT_OUTLINE_COLOR = Color.TRANSPARENT;

// data
private int mOutlineSize;
private int mOutlineColor;
private int mTextColor;
private float mShadowRadius;
private float mShadowDx;
private float mShadowDy;
private int mShadowColor;

public TextViewOutline(Context context) {
    this(context, null);
}

public TextViewOutline(Context context, AttributeSet attrs) {
    super(context, attrs);
    setAttributes(attrs);
}

private void setAttributes(AttributeSet attrs) {
    // set defaults
    mOutlineSize = DEFAULT_OUTLINE_SIZE;
    mOutlineColor = DEFAULT_OUTLINE_COLOR;
    // text color   
    mTextColor = getCurrentTextColor();
    if (attrs != null) {
        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.TextViewOutline);
        // outline size
        if (a.hasValue(R.styleable.TextViewOutline_outlineSize)) {
            mOutlineSize = (int) a.getDimension(R.styleable.TextViewOutline_outlineSize, DEFAULT_OUTLINE_SIZE);
        }
        // outline color
        if (a.hasValue(R.styleable.TextViewOutline_outlineColor)) {
            mOutlineColor = a.getColor(R.styleable.TextViewOutline_outlineColor, DEFAULT_OUTLINE_COLOR);
        }
        // shadow (the reason we take shadow from attributes is because we use API level 15 and only from 16 we have the get methods for the shadow attributes)
        if (a.hasValue(R.styleable.TextViewOutline_android_shadowRadius)
                || a.hasValue(R.styleable.TextViewOutline_android_shadowDx)
                || a.hasValue(R.styleable.TextViewOutline_android_shadowDy)
                || a.hasValue(R.styleable.TextViewOutline_android_shadowColor)) {
            mShadowRadius = a.getFloat(R.styleable.TextViewOutline_android_shadowRadius, 0);
            mShadowDx = a.getFloat(R.styleable.TextViewOutline_android_shadowDx, 0);
            mShadowDy = a.getFloat(R.styleable.TextViewOutline_android_shadowDy, 0);
            mShadowColor = a.getColor(R.styleable.TextViewOutline_android_shadowColor, Color.TRANSPARENT);
        }

        a.recycle();
    }

}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setPaintToOutline();
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

private void setPaintToOutline() {
    Paint paint = getPaint();
    paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeWidth(mOutlineSize);
    super.setTextColor(mOutlineColor);
    super.setShadowLayer(0, 0, 0, Color.TRANSPARENT);

}

private void setPaintToRegular() {
    Paint paint = getPaint();
    paint.setStyle(Paint.Style.FILL);
    paint.setStrokeWidth(0);
    super.setTextColor(mTextColor);
    super.setShadowLayer(mShadowRadius, mShadowDx, mShadowDy, mShadowColor);
}


@Override
public void setTextColor(int color) {
    super.setTextColor(color);
    mTextColor = color;
}


public void setOutlineSize(int size) {
    mOutlineSize = size;
}

public void setOutlineColor(int color) {
    mOutlineColor = color;
}

@Override
protected void onDraw(Canvas canvas) {
    setPaintToOutline();
    super.onDraw(canvas);

    setPaintToRegular();
    super.onDraw(canvas);
}

}

属性定义

<declare-styleable name="TextViewOutline">
    <attr name="outlineSize" format="dimension"/>
    <attr name="outlineColor" format="color|reference"/>
    <attr name="android:shadowRadius"/>
    <attr name="android:shadowDx"/>
    <attr name="android:shadowDy"/>
    <attr name="android:shadowColor"/>
</declare-styleable>

以下是XML代码

<com.megvii.demo.TextViewOutline
    android:id="@+id/product_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:layout_marginTop="110dp"
    android:background="#f4b222"
    android:fontFamily="@font/kidsmagazine"
    android:padding="10dp"
    android:shadowColor="#d7713200"
    android:shadowDx="0"
    android:shadowDy="8"
    android:shadowRadius="1"
    android:text="LIPSTICK SET"
    android:textColor="@android:color/white"
    android:textSize="30sp"
    app:outlineColor="#cb7800"
    app:outlineSize="3dp" />

它适用于TextView,我们如何将其应用于EditText? - Usama Saeed US
1
onDraw()中调用super.setTextColor()将导致视图无效,进而再次调用onDraw()。这将导致无限循环。 - Sergey Stasishin

12
你可以使用以下代码来以编程方式实现此操作。它提供了白色字体和黑色背景:
textView.setTextColor(Color.WHITE);            
textView.setShadowLayer(1.6f,1.5f,1.3f,Color.BLACK);

该方法的参数包括半径、dx、dy和颜色,您可以根据自己的需求更改它们。

我希望能够帮助那些在程序中创建TextView而不是在xml中创建的人。

为StackOverflow社区欢呼!


9
我希望添加一个解决性能问题的解决方案。例如,@YGHM和其他一些人的答案可以完成任务,但它会引起onDraw的无限调用,因为setTextColor会调用invalidate()。所以,为了解决这个问题,你还需要重写invalidate()并添加一个变量isDrawing,当正在进行带有描边的绘制时,将其设置为true。如果变量isDrawingtrue,则invalidate将返回。
override fun invalidate() {
    if (isDrawing) return
    super.invalidate()
  }

您的onDraw方法应该如下所示:

override fun onDraw(canvas: Canvas) {
    if (strokeWidth > 0) {
      isDrawing = true
      val textColor = textColors.defaultColor
      setTextColor(strokeColor)
      paint.strokeWidth = strokeWidth
      paint.style = Paint.Style.STROKE
      super.onDraw(canvas)
      setTextColor(textColor)
      paint.strokeWidth = 0f
      paint.style = Paint.Style.FILL
      isDrawing = false
      super.onDraw(canvas)
    } else {
      super.onDraw(canvas)
    }
  }

3
为什么点赞这么少?这是一个非常好的解决方案,可以解决所有这些“无限循环”和“隐藏API反射”问题! - Sergey Stasishin
无法工作,onDraw仍然处于无限循环状态。 - JOSEMAFUEN

8
我已经编写了一个类来执行带有轮廓的文本,并仍然支持正常文本视图的所有其他属性和绘图。
基本上,它使用TextView上的super.onDraw(Canvas canvas),但使用不同的样式进行两次绘制。
希望这可以帮到你。
public class TextViewOutline extends TextView {

    // constants
    private static final int DEFAULT_OUTLINE_SIZE = 0;
    private static final int DEFAULT_OUTLINE_COLOR = Color.TRANSPARENT;

    // data
    private int mOutlineSize;
    private int mOutlineColor;
    private int mTextColor;
    private float mShadowRadius;
    private float mShadowDx;
    private float mShadowDy;
    private int mShadowColor;

    public TextViewOutline(Context context) {
        this(context, null);
    }

    public TextViewOutline(Context context, AttributeSet attrs) {
        super(context, attrs);
        setAttributes(attrs);
    }

    private void setAttributes(AttributeSet attrs){ 
        // set defaults
        mOutlineSize = DEFAULT_OUTLINE_SIZE;
        mOutlineColor = DEFAULT_OUTLINE_COLOR;   
        // text color   
        mTextColor = getCurrentTextColor();
        if(attrs != null) {
            TypedArray a = getContext().obtainStyledAttributes(attrs,R.styleable.TextViewOutline);
            // outline size
            if (a.hasValue(R.styleable.TextViewOutline_outlineSize)) {
                mOutlineSize = (int) a.getDimension(R.styleable.TextViewOutline_outlineSize, DEFAULT_OUTLINE_SIZE);
            }
            // outline color
            if (a.hasValue(R.styleable.TextViewOutline_outlineColor)) {
                mOutlineColor = a.getColor(R.styleable.TextViewOutline_outlineColor, DEFAULT_OUTLINE_COLOR);
            }
            // shadow (the reason we take shadow from attributes is because we use API level 15 and only from 16 we have the get methods for the shadow attributes)
            if (a.hasValue(R.styleable.TextViewOutline_android_shadowRadius) 
                    || a.hasValue(R.styleable.TextViewOutline_android_shadowDx)
                    || a.hasValue(R.styleable.TextViewOutline_android_shadowDy) 
                    || a.hasValue(R.styleable.TextViewOutline_android_shadowColor)) {
                mShadowRadius = a.getFloat(R.styleable.TextViewOutline_android_shadowRadius, 0);
                mShadowDx = a.getFloat(R.styleable.TextViewOutline_android_shadowDx, 0);
                mShadowDy = a.getFloat(R.styleable.TextViewOutline_android_shadowDy, 0);
                mShadowColor = a.getColor(R.styleable.TextViewOutline_android_shadowColor, Color.TRANSPARENT);
            }

            a.recycle();
        }

        PFLog.d("mOutlineSize = " + mOutlineSize);
        PFLog.d("mOutlineColor = " + mOutlineColor);
    }

    private void setPaintToOutline(){
        Paint paint = getPaint();
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(mOutlineSize);
        super.setTextColor(mOutlineColor);
        super.setShadowLayer(mShadowRadius, mShadowDx, mShadowDy,  mShadowColor);
    }

    private void setPaintToRegular() {
        Paint paint = getPaint();
        paint.setStyle(Paint.Style.FILL);
        paint.setStrokeWidth(0);
        super.setTextColor(mTextColor);
        super.setShadowLayer(0, 0, 0, Color.TRANSPARENT);
    } 

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setPaintToOutline();
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    public void setTextColor(int color) {
        super.setTextColor(color);
        mTextColor = color;
    } 

    @Override
    public void setShadowLayer(float radius, float dx, float dy, int color) {
        super.setShadowLayer(radius, dx, dy, color);
        mShadowRadius = radius;
        mShadowDx = dx;
        mShadowDy = dy;
        mShadowColor = color;
    }

    public void setOutlineSize(int size){
        mOutlineSize = size;
    }

    public void setOutlineColor(int color){
       mOutlineColor = color;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        setPaintToOutline();
        super.onDraw(canvas);
        setPaintToRegular();
        super.onDraw(canvas);
    }

}

attr.xml

<declare-styleable name="TextViewOutline">
    <attr name="outlineSize" format="dimension"/>
    <attr name="outlineColor" format="color|reference"/>
    <attr name="android:shadowRadius"/>
    <attr name="android:shadowDx"/>
    <attr name="android:shadowDy"/>
    <attr name="android:shadowColor"/>
</declare-styleable>

TypedArray上缺少a.recycle()方法。 - Leos Literak

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