Android 文本视图大纲文本

有没有一种简单的方法可以让文字有一个黑色的轮廓?我有不同颜色的文本视图,但有些颜色不能很好地显示在我的背景上,所以我想知道是否有一个简单的方法得到一个黑色的轮廓或其他东西,将做这项工作?我宁愿不必创建一个自定义视图和画布等。

158552 次浏览

So you want a stroke around the textview? Unfortunately there is no simple way to do it with the styling. You'll have to create another view and place your textview over-top, making the parent view (the one it's on top of) just a few pixels bigger - this should create an outline.

You can put a shadow behind the text, which can often help readability. Try experimenting with 50% translucent black shadows on your green text. Details on how to do this are over here: Android - shadow on text?

To really add a stroke around the text, you need to do something a bit more involved, like this: How do you draw text with a border on a MapView in Android?

I've just been trying to figure out how to do this and couldn't find a good guide online but eventually figured it out. As Steve Pomeroy suggested, you do have to do something more involved. In order to get the outlined text effect, you draw the text twice: once with a thick outline and then the second time we draw the main text over the outline. But, the task is made easier because you can very easily adapt one of the code samples provided with the SDK, namely the one under this name in your SDK directory: "/samples/android-/ApiDemos/src/com/example/android/apis/view/LabelView.java". Which can also found on the Android developer website here.

Depending on what you're doing, it's very easy to see you will only need to make minor modifications to that code, such as changing it to extend from TextView, etc. Before I discovered this sample I forgot to override onMeasure() (which you must do in addition to overriding onDraw() as is mentioned in the "Building Custom Components" guide on the Android Developer website), which is part of why I was having trouble.

Once you've done that, you can do what I did:

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

So, in order to get the outlined text effect, you draw the text twice: once with a thick outline and then the second time we draw the main text over the outline.

The framework supports text-shadow but does not support text-outline. But there is a trick: shadow is something that is translucent and fades. Redraw the shadow a couple of times and all the alpha gets summed up and the result is an outline.

A very simple implementation extends TextView and overrides the draw(..) method. Every time a draw is requested our subclass does 5-10 drawings.

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

So, little late, but MagicTextView will do text outlines, amongst other things.

enter image description here

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

Note: I made this, and am posting more for the sake of future travelers than the OP. It's borderline spam, but being on-topic, perhaps acceptable?

Here's the trick I found that works better than MagicTextView's stroke IMO

@Override
protected void onDraw(Canvas pCanvas) {
int textColor = getTextColors().getDefaultColor();
setTextColor(mOutlineColor); // your stroke's color
getPaint().setStrokeWidth(10);
getPaint().setStyle(Paint.Style.STROKE);
super.onDraw(pCanvas);
setTextColor(textColor);
getPaint().setStrokeWidth(0);
getPaint().setStyle(Paint.Style.FILL);
super.onDraw(pCanvas);
}

outline effect can be achieved using shadow in 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"

It is quite an old question but still I don't see any complete answers. So I am posting this solution, hoping that someone struggling with this problem might find it useful. The simplest and most effective solution is to override TextView class' onDraw method. Most implementations I have seen use drawText method to draw the stroke but that approach doesn't account for all the formatting alignment and text wrapping that goes in. And as a result often the stroke and text end up at different places. Following approach uses super.onDraw to draw both the stroke and fill parts of the text so you don't have to bother about rest of the stuff. Here are the steps

  1. Extend TextView class
  2. Override onDraw method
  3. Set paint style to FILL
  4. call parent class on Draw to render text in fill mode.
  5. save current text color.
  6. Set current text color to your stroke color
  7. Set paint style to Stroke
  8. Set stroke width
  9. And call parent class onDraw again to draw the stroke over the previously rendered text.

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

That is all. This class uses custom XML attributes to enable specifying stroke color and width from the XML layout files. Therefore, you need to add these attributes in your attr.xml file in subfolder 'values' under folder 'res'. Copy and paste the following in your attr.xml file.

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

Once you are done with that, you can use the custom StrokedTextView class in your XML layout files and specify stroke color and width as well. Here is an example

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

Remember to replace package name with your project's package name. Also add the xmlns namespace in the layout file in order to use custom XML attributes. You can add the following line in your layout file's root node.

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

I've written a class to perform text with outline and still support all the other attributes and drawing of a normal text view.

it basically uses the super.onDraw(Canves canvas) on the TextView but draws twice with different styles.

hope this helps.

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>

MagicTextView is very useful to make stroke font, but in my case, it cause error like this this error caused by duplication background attributes which set by MagicTextView

so you need to edit attrs.xml and MagicTextView.java

attrs.xml

<attr name="background" format="reference|color" />
↓
<attr name="mBackground" format="reference|color" />

MagicTextView.java 88:95

if (a.hasValue(R.styleable.MagicTextView_mBackground)) {
Drawable background = a.getDrawable(R.styleable.MagicTextView_mBackground);
if (background != null) {
this.setBackgroundDrawable(background);
} else {
this.setBackgroundColor(a.getColor(R.styleable.MagicTextView_mBackground, 0xff000000));
}
}

I have created a library based on Nouman Hanif's answer with some additions. For example, fixing a bug that caused an indirect infinite loop on View.invalidate() calls.

OTOH, the library also supports outlined text in EditText widgets, as it was my real goal and it needed a bit more work than TextView.

Here is the link to my library: https://github.com/biomorgoth/android-outline-textview

Thanks to Nouman Hanif for the initial idea on the solution!

You can do this programmatically with the below snippet. That provides white letters with black background:

textView.setTextColor(Color.WHITE);
textView.setShadowLayer(1.6f,1.5f,1.3f,Color.BLACK);

The parameters of the method are radius,dx,dy,color. You can change them for you specific needs.

I hope I will help someone that creates TextView programmatically and not having it inside xml.

Cheers to the stackOverflow community!

I found simple way to outline view without inheritance from TextView. I had wrote simple library that use Android's Spannable for outlining text. This solution gives possibility to outline only part of text.

I already had answered on same question (answer)

Class:

class OutlineSpan(
@ColorInt private val strokeColor: Int,
@Dimension private val strokeWidth: Float
): ReplacementSpan() {


override fun getSize(
paint: Paint,
text: CharSequence,
start: Int,
end: Int,
fm: Paint.FontMetricsInt?
): Int {
return paint.measureText(text.toString().substring(start until end)).toInt()
}




override fun draw(
canvas: Canvas,
text: CharSequence,
start: Int,
end: Int,
x: Float,
top: Int,
y: Int,
bottom: Int,
paint: Paint
) {
val originTextColor = paint.color


paint.apply {
color = strokeColor
style = Paint.Style.STROKE
this.strokeWidth = this@OutlineSpan.strokeWidth
}
canvas.drawText(text, start, end, x, y.toFloat(), paint)


paint.apply {
color = originTextColor
style = Paint.Style.FILL
}
canvas.drawText(text, start, end, x, y.toFloat(), paint)
}


}

Library: OutlineSpan

I want to add a solution in order to solve the performance issue. For example, the answer of @YGHM and a few others does the job, but it causes infinite call of onDraw because setTextColor calls invalidate(). So in order to solve it, you also need to override invalidate() and add a variable isDrawing that you will set to true, when onDraw() is in progress and drawing with a stroke. invalidate will return if the variable is true.

override fun invalidate() {
if (isDrawing) return
super.invalidate()
}

Your onDraw will look like this:

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

credit to @YGHM add shadow support enter image description here

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


}

attr define

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

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

Here is the simplest way I could find by extending TextView

public class CustomTextView extends androidx.appcompat.widget.AppCompatTextView {


float mStroke;


public CustomTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.CustomTextView);
mStroke=a.getFloat(R.styleable.CustomTextView_stroke,1.0f);
a.recycle();
}


@Override
protected void onDraw(Canvas canvas) {
TextPaint paint = this.getPaint();
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(mStroke);
    

super.onDraw(canvas);
}
}

then you only need to add the following to the attrs.xml file

<declare-styleable name="CustomTextView">
<attr name="stroke" format="float"/>
</declare-styleable>

and now you will be able to set the stroke widht by app:stroke while retaining all other desirable properties of TextView. my solution only draws the stroke w/o a fill. this makes it a bit simpler than the others. bellow a screencapture with the result while setting a custom font to my customtextview.

enter image description here