Android textview text get cut off on the sides with custom font - android

This is what happens in the preview and on device:
TextView is nothing special, it just loads the custom font:
public class TestTextView extends AppCompatTextView {
public TestTextView(Context context) {
super(context);
init(context);
}
public TestTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public TestTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
void init(Context context) {
Typeface t = Typeface.createFromAsset(context.getAssets(), "fonts/daisy.ttf");
setTypeface(t);
}
}
Layout is also very basic, but just in case:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/material_red200"
android:orientation="vertical">
<*custompackage* .TestTextView
android:gravity="left"
android:padding="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="just some text for testing"
android:textColor="#color/material_black"
android:textSize="100dp" />
</LinearLayout>
As you can see, the left parts, like 'j' and 'f' are cut off.
Setting the padding or margin did not work.
This font fits into it's frame when using from other programs.
Thanks in advance.
Edit:
What #play_err_ mentioned is not a solution in my case.
I am using in the final version a textview that resizes automatically, so adding spaces would be terribly difficult.
I need an explanation why other programs (eg photoshop, after effects...) can calculate a proper bounding box and android cannot
I am also loading different fonts dynamically and I do not want to create an
if(badfont)
addSpaces()

This answer has led me to the right path:
https://stackoverflow.com/a/28625166/4420543
So, the solution is to create a custom Textview and override the onDraw method:
#Override
protected void onDraw(Canvas canvas) {
final Paint paint = getPaint();
final int color = paint.getColor();
// Draw what you have to in transparent
// This has to be drawn, otherwise getting values from layout throws exceptions
setTextColor(Color.TRANSPARENT);
super.onDraw(canvas);
// setTextColor invalidates the view and causes an endless cycle
paint.setColor(color);
System.out.println("Drawing text info:");
Layout layout = getLayout();
String text = getText().toString();
for (int i = 0; i < layout.getLineCount(); i++) {
final int start = layout.getLineStart(i);
final int end = layout.getLineEnd(i);
String line = text.substring(start, end);
System.out.println("Line:\t" + line);
final float left = layout.getLineLeft(i);
final int baseLine = layout.getLineBaseline(i);
canvas.drawText(line,
left + getTotalPaddingLeft(),
// The text will not be clipped anymore
// You can add a padding here too, faster than string string concatenation
baseLine + getTotalPaddingTop(),
getPaint());
}
}

I have encountered the same problem and i found a one liner solution for thouse who are not using the TextView.shadowLayer.
this is based on the source code that [Dmitry Kopytov] brought here:
editTextOrTextView.setShadowLayer(editTextOrTextView.textSize, 0f, 0f, Color.TRANSPARENT)
that's it, now the canvas.clipRect in TextView.onDraw() won't cut off the curly font sides.

Reworked #Dmitry Kopytov solution:
in Kotlin
recycle the old bitmap
added documentation
fall back on default TextView rendering if the bitmap cannot be created (not enough memory)
Code:
/**
* This TextView is able to draw text on the padding area.
* It's mainly used to support italic texts in custom fonts that can go out of bounds.
* In this case, you've to set an horizontal padding (or just end padding).
*
* This implementation is doing a render-to-texture procedure, as such it consumes more RAM than a standard TextView,
* it uses an additional bitmap of the size of the view.
*/
class TextViewNoClipping(context: Context, attrs: AttributeSet?) : AppCompatTextView(context, attrs) {
private class NonClippableCanvas(#NonNull val bitmap: Bitmap) : Canvas(bitmap) {
override fun clipRect(left: Float, top: Float, right: Float, bottom: Float): Boolean {
return true
}
}
private var rttCanvas: NonClippableCanvas? = null
override fun onSizeChanged(width: Int, height: Int,
oldwidth: Int, oldheight: Int) {
if ((width != oldwidth || height != oldheight) && width > 0 && height > 0) {
rttCanvas?.bitmap?.recycle()
try {
Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)?.let {
rttCanvas = NonClippableCanvas(it)
}
} catch (t: Throwable) {
// If for some reasons the bitmap cannot be created, we fall back on default rendering (potentially cropping the text).
rttCanvas?.bitmap?.recycle()
rttCanvas = null
}
}
super.onSizeChanged(width, height, oldwidth, oldheight)
}
override fun onDraw(canvas: Canvas) {
rttCanvas?.let {
// Clear the RTT canvas from the previous font.
it.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
// Draw on the RTT canvas (-> bitmap) that will use clipping on the NonClippableCanvas, resulting in no-clipping
super.onDraw(it)
// Finally draw the bitmap that contains the rendered text (no clipping used here, will display on top of padding)
canvas.drawBitmap(it.bitmap, 0f, 0f, null)
} ?: super.onDraw(canvas) // If rtt is not available, use default rendering process
}
}

I encountered the same problem when I used some fonts in EditText.
My first attempt was to use padding. Size of view increased but text is still cropped.
Then I looked at the source code TextView. In method onDraw method Canvas.clipRect is called to perform this crop.
My solution to bypass cropping when use padding :
1) Сreate custom class inherited from Canvas and override method clipRect
public class NonClippableCanvas extends Canvas {
public NonClippableCanvas(#NonNull Bitmap bitmap) {
super(bitmap);
}
#Override
public boolean clipRect(float left, float top, float right, float bottom) {
return true;
}
}
2) Create custom TextView and override methods onSizeChanged and onDraw.
In the method onSizeChanged create bitmap and canvas.
In the method onDraw draw on bitmap by passing our custom Canvas to method super.onDraw. Next, draw this bitmap on the target canvas.
public class CustomTextView extends AppCompatTextView {
private Bitmap _bitmap;
private NonClippableCanvas _canvas;
#Override
protected void onSizeChanged(final int width, final int height,
final int oldwidth, final int oldheight) {
if (width != oldwidth || height != oldheight) {
_bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
_canvas = new NonClippableCanvas(_bitmap);
}
super.onSizeChanged(width, height, oldwidth, oldheight);
}
#Override
protected void onDraw(Canvas canvas) {
_canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
super.onDraw(_canvas);
canvas.drawBitmap(_bitmap, 0, 0, null);
}
}

A workaround is to add a space before typing. It will save you a lot of coding but will result in a "padding" to the left.
android:text=" text after a space"

replace TextView.BufferType.SPANNABLE with TextView.BufferType.NORMAL

What if you wrap it in another layout and add padding to that? For example something like this:
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp">
<*custompackage* .TestTextView
android:gravity="left"
android:padding="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="just some text for testing"
android:textColor="#color/material_black"
android:textSize="100dp" />
</RelativeLayout>
Not having your font and other themes etc I've just tried it with the cursive font for example and on my machine it would look like this.
screenshot
Update:
Looks like you're not the only one to have had this issue and the other answers here and here both unfortunately relate to adding extra spaces.
I've created a bug ticket here since it looks like a bug to me.

Related

How to fade out the end of the last line in a TextView?

How can we achieve the fade-out effect on the last line of a TextView, like in the "WHAT'S NEW" section in the Play Store app?
That fade effect can be accomplished by subclassing a TextView class to intercept its draw, and doing something like what the View class does to fade out edges, but only in the last stretch of the final text line.
In this example, we create a unit horizontal linear gradient that goes from transparent to solid black. As we prepare to draw, this unit gradient is scaled to a length calculated as a simple fraction of the TextView's final line length, and then positioned accordingly.
An off-screen buffer is created, and we let the TextView draw its content to that. We then draw the fade gradient over it with a transfer mode of PorterDuff.Mode.DST_OUT, which essentially clears the underlying content to a degree relative to the gradient's opacity at a given point. Drawing that buffer back on-screen results in the desired fade, no matter what is in the background.
public class FadingTextView extends AppCompatTextView {
private static final float FADE_LENGTH_FACTOR = .4f;
private final RectF drawRect = new RectF();
private final Rect realRect = new Rect();
private final Path selection = new Path();
private final Matrix matrix = new Matrix();
private final Paint paint = new Paint();
private final Shader shader =
new LinearGradient(0f, 0f, 1f, 0f, 0x00000000, 0xFF000000, Shader.TileMode.CLAMP);
public FadingTextView(Context context) {
this(context, null);
}
public FadingTextView(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.textViewStyle);
}
public FadingTextView(Context context, AttributeSet attrs, int defStyleAttribute) {
super(context, attrs, defStyleAttribute);
paint.setShader(shader);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
}
#Override
protected void onDraw(Canvas canvas) {
// Locals
final RectF drawBounds = drawRect;
final Rect realBounds = realRect;
final Path selectionPath = selection;
final Layout layout = getLayout();
// Figure last line index, and text offsets there
final int lastLineIndex = getLineCount() - 1;
final int lastLineStart = layout.getLineStart(lastLineIndex);
final int lastLineEnd = layout.getLineEnd(lastLineIndex);
// Let the Layout figure a Path that'd cover the last line text
layout.getSelectionPath(lastLineStart, lastLineEnd, selectionPath);
// Convert that Path to a RectF, which we can more easily modify
selectionPath.computeBounds(drawBounds, false);
// Naive text direction determination; may need refinement
boolean isRtl =
layout.getParagraphDirection(lastLineIndex) == Layout.DIR_RIGHT_TO_LEFT;
// Narrow the bounds to just the fade length
if (isRtl) {
drawBounds.right = drawBounds.left + drawBounds.width() * FADE_LENGTH_FACTOR;
} else {
drawBounds.left = drawBounds.right - drawBounds.width() * FADE_LENGTH_FACTOR;
}
// Adjust for drawables and paddings
drawBounds.offset(getTotalPaddingLeft(), getTotalPaddingTop());
// Convert drawing bounds to real bounds to determine
// if we need to do the fade, or a regular draw
drawBounds.round(realBounds);
realBounds.offset(-getScrollX(), -getScrollY());
boolean needToFade = realBounds.intersects(getTotalPaddingLeft(), getTotalPaddingTop(),
getWidth() - getTotalPaddingRight(), getHeight() - getTotalPaddingBottom());
if (needToFade) {
// Adjust and set the Shader Matrix
final Matrix shaderMatrix = matrix;
shaderMatrix.reset();
shaderMatrix.setScale(drawBounds.width(), 1f);
if (isRtl) {
shaderMatrix.postRotate(180f, drawBounds.width() / 2f, 0f);
}
shaderMatrix.postTranslate(drawBounds.left, drawBounds.top);
shader.setLocalMatrix(shaderMatrix);
// Save, and start drawing to an off-screen buffer
final int saveCount;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
saveCount = canvas.saveLayer(null, null);
} else {
saveCount = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);
}
// Let TextView draw itself to the buffer
super.onDraw(canvas);
// Draw the fade to the buffer, over the TextView content
canvas.drawRect(drawBounds, paint);
// Restore, and draw the buffer back to the Canvas
canvas.restoreToCount(saveCount);
} else {
// Regular draw
super.onDraw(canvas);
}
}
}
This is a drop-in replacement for TextView, and you'd use it in your layout similarly.
<com.example.app.FadingTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#e2f3eb"
android:textColor="#0b8043"
android:lineSpacingMultiplier="1.2"
android:text="#string/umang" />
Notes:
The fade length calculation is based on a constant fraction of the final line's text length, here determined by FADE_LENGTH_FACTOR. This seems to be the same basic methodology of the Play Store component, as the absolute length of the fade appears to vary with line length. The FADE_LENGTH_FACTOR value can be altered as desired.
FadingTextView currently extends AppCompatTextView, but it works perfectly well as a plain TextView, if you should need that instead. I would think that it will work as a MaterialTextView too, though I've not tested that thoroughly.
This example is geared mainly toward relatively plain use; i.e., as a simple wrapped, static label. Though I've attempted to account for and test every TextView setting I could think of that might affect this – e.g., compound drawables, paddings, selectable text, scrolling, text direction and alignment, etc. – I can't guarantee that I've thought of everything.

Android: Black outline around text [duplicate]

Is there a simple way to have text be able to have a black outline? I have textviews that will be different colors, but some of the colors don't show up on my background so well, so I was wondering if there's an easy way to get a black outline or something else that will do the job? I'd prefer not to have to create a custom view and make a canvas and such.
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"
So, little late, but MagicTextView will do text outlines, amongst other things.
<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?
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?
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
Extend TextView class
Override onDraw method
Set paint style to FILL
call parent class on Draw to render text in fill
mode.
save current text color.
Set current text color to your stroke color
Set paint style to Stroke
Set stroke width
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"
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" />
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.
credit to #YGHM add shadow support
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" />
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 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)
}
}
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);
}
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>
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!
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
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));
}
}
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.
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.

Android Text Centering

I'm trying to draw a number inside of a circle and having issues with the centering on the text. The following is the xml I'm using to display the TextView
<RelativeLayout
android:id="#+id/label"
android:layout_width="72dip"
android:layout_height="72dip"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:layout_margin="8dip" >
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#drawable/hole_background"
android:layout_centerInParent="true"/>
<TextView
android:id="#+id/hole"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textColor="#android:color/white"
android:textSize="56sp"
android:textStyle="bold"
android:gravity="center"/>
</RelativeLayout>
The problem is it is just slightly more towards the bottom (by about 10 pixels on the device I tested). This is a screenshot from the view hierarchy viewer that shows what I'm talking about...
I've tried combining the views and using the background such as here, or the removing the font padding like suggested here but neither worked, the font padding ending up as seen below...
I am currently am using a negative margin to adjust it to look right but that doesn't seem like the correct way to do it. Does anyone have any ideas on how to make text centered without using negative margins that I have to manually check based upon the text size?
Thanks in advance
So based upon the comments of LairdPleng and 323go I ended up just creating a custom view. The following view will do the centering exactly based upon the height of the number being drawn...
public class Label extends View {
private static final int TEXT_SIZE = 56;
private String mText;
private float mCenterX;
private float mCenterY;
private float mRadius;
private Paint mCirclePaint;
private Paint mTextPaint;
private Rect mTextBounds;
public HoleLabel(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setupCanvasComponents(context);
}
public HoleLabel(Context context, AttributeSet attrs) {
super(context, attrs);
setupCanvasComponents(context);
}
public HoleLabel(Context context) {
super(context);
setupCanvasComponents(context);
}
public void setText(String text) {
if (!StringUtils.equals(mText, text)) {
mText = text;
invalidate();
}
}
private void setupCanvasComponents(Context context) {
mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mCirclePaint.setColor(Color.BLACK);
mCirclePaint.setStyle(Paint.Style.FILL);
Typeface font = Typeface.createFromAsset(context.getAssets(), "fonts/CustomFont.otf");
DisplayMetrics displayMetrics = new DisplayMetrics();
((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay().getMetrics(displayMetrics);
float scaledDensity = displayMetrics.scaledDensity;
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.LINEAR_TEXT_FLAG);
mTextPaint.setColor(Color.WHITE);
mTextPaint.setStyle(Paint.Style.FILL);
mTextPaint.setTextSize(TEXT_SIZE * scaledDensity);
mTextPaint.setTypeface(font);
mTextPaint.setTextAlign(Align.CENTER);
mTextBounds = new Rect();
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mCenterX = w / 2.0f;
mCenterY = h / 2.0f;
mRadius = w / 2.0f;
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Draw the background
canvas.drawCircle(mCenterX, mCenterY, mRadius, mCirclePaint);
// Draw the text
if (mText != null) {
mTextPaint.getTextBounds(mText, 0, mText.length(), mTextBounds);
canvas.drawText(mText, mCenterX, (mCenterY + mTextBounds.height() / 2), mTextPaint);
}
}
}
And in xml...
<com.example.widget.Label
android:id="#+id/label"
android:layout_width="72dip"
android:layout_height="72dip"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:layout_margin="8dip" />
The result being...
I had a very specific use case so I don't know if it'll work well if with less rigid size specifications but it was the way to go for me.
Would add this as a comment, but don't have enough reputation. Expanding on 323go's comment, you might find it easier to use images for your numbers, rather than spending a whole lot of time changing fonts and manually adjusting offsets - which may look fine on one dpi then look off again on another.

How to calculate line height containing TextView with specific height in Android?

I want to calculate the line (or layout) height (in DP) which contains only TextView as outcome of the TextView text size when using default line spacing ?
I.E. for this layout :
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<TextView
android:id="#+id/minRow1col1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textIsSelectable="false"
android:textSize="11dp" />
</LinearLayout>
What will be the layout/line height ? (any formula ? I don't need the value in run time)
Thanks !
I don't know if this help you guys, but my solution to get the height of a line it's independent of the height of the layout, just take the font metrics like this:
myTextView.getPaint().getFontMetrics().bottom - myTextView.getPaint().getFontMetrics().top)
With this you will get the line height and for me, it's works with all words ( there are some chars like "g" or "j" that take some bottom space, the so called "descender" and so on ).
Try using the TextPaint object of TextView.
TextView tv = useTextView;
String text = tv.getText().toString();
Paint textPaint = tv.getPaint();
Rect textRect = new Rect();
textPaint.getTextBounds(text, 0, text.length(), textRext);
int textHeight = textRect.height();
Per documentation of Paint#getTextBound:
Return in bounds (allocated by the caller) the smallest rectangle that
encloses all of the characters, with an implied origin at (0,0).
Using the Paint object that the TextView uses will ensure it has the same parameters set that will be used to draw the text.
You have to create custom Textview and use getActualHeight() method. Where the formula is: actualHeight=(int) ((getLineCount()-1)*getTextSize());
public class TextViewHeightPlus extends TextView {
private static final String TAG = "TextView";
private int actualHeight=0;
public int getActualHeight() {
return actualHeight;
}
public TextViewHeightPlus(Context context) {
super(context);
}
public TextViewHeightPlus(Context context, AttributeSet attrs) {
super(context, attrs);
setCustomFont(context, attrs);
}
public TextViewHeightPlus(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
actualHeight=0;
actualHeight=(int) ((getLineCount()-1)*getTextSize());
}
}
You can try Paint.getTextBounds():
String finalVal ="Hello";
Paint paint = new Paint();
paint.setTextSize(18);
paint.setTypeface(Typeface.SANS_SERIF);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.FILL);
Rect result = new Rect();
// Measure the text rectangle to get the height
paint.getTextBounds(finalVal, 0, finalVal.length(), result);
Log.d("WIDTH :", String.valueOf(result.width()));
Log.d("HEIGHT :", String.valueOf(result.height()));
Source.
For my usecase I started out by assuming that the line height is a linear function that depends on the textsize, something like:
line_height = text_size*some_constant
Now some_constant is probably a function as well, that depends on what font you use. But since in my requirements the font was static, I was able to calculate some_constant and use repeatedly in a safe way.
To clue you in a little a bit more on my usecase, I was scaling a piece of multiline text in order to fit it in a box of variable height.
In my usecase I wanted to go a step further and included the space multiplier. It was quite simple, as it was just another factor in the equation:
line_height = text_size*some_constant*spacing_multiplier
In summary if you stick to the same font and you calculate the value of some_constant once, you can (probably) get your function.
DISCLAIMER: I say "probably" a lot because I haven't tested a lot of my assumptions, but like I said, it worked for a decently complex usecase.

Android textview outline text

Is there a simple way to have text be able to have a black outline? I have textviews that will be different colors, but some of the colors don't show up on my background so well, so I was wondering if there's an easy way to get a black outline or something else that will do the job? I'd prefer not to have to create a custom view and make a canvas and such.
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"
So, little late, but MagicTextView will do text outlines, amongst other things.
<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?
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?
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
Extend TextView class
Override onDraw method
Set paint style to FILL
call parent class on Draw to render text in fill
mode.
save current text color.
Set current text color to your stroke color
Set paint style to Stroke
Set stroke width
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"
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" />
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.
credit to #YGHM add shadow support
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" />
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 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)
}
}
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);
}
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>
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!
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
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));
}
}
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.
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.

Categories

Resources