Textview with Background Color and Line Spacing

UITextView/UILabel with background but with spacing between lines

After some research I found the best solution for what I needed.
The solution below is only iOS7+.

First we add this to - (void)drawRect:(CGRect)rect of your UITextView subclass.

- (void)drawRect:(CGRect)rect    

/// Position each line behind each line.
[self.layoutManager enumerateLineFragmentsForGlyphRange:NSMakeRange(0, self.text.length) usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer *textContainer, NSRange glyphRange, BOOL *stop) {

/// The frame of the rectangle.
UIBezierPath *rectanglePath = [UIBezierPath bezierPathWithRect:CGRectMake(usedRect.origin.x, usedRect.origin.y+3, usedRect.size.width, usedRect.size.height-4)];

/// Set the background color for each line.
[UIColor.blackColor setFill];

/// Build the rectangle.
[rectanglePath fill];
}];
}];

Then we set the line spacing for the UITextView:

- (CGFloat)layoutManager:(NSLayoutManager *)layoutManager lineSpacingAfterGlyphAtIndex:(NSUInteger)glyphIndex withProposedLineFragmentRect:(CGRect)rect
{
return 15;
}

The method above is only called if you set the NSLayoutManagerDelegate. You could do that in your init, initWithFrame and initWithCode methods like this:

self.layoutManager.delegate = self;

Also don't forget to declare that your subclass is a delegate in your .h file:

@interface YOUR_SUBCLASS_OF_UITEXTVIEW : UITextView <NSLayoutManagerDelegate>

Android TextView text background color

As far as I can see, there's no 'nice' way of doing this without overriding TextView and drawing custom paints on the view which includes the gap colour.

Even setting the lineSpacingExtra property only expands the background colour.

You could also potentially look into creating a custom spannable and use it like

Spannable str = new SpannableStringBuilder("How can I achieve such an effect with an Android TextView. It looks somehow like selected text and I couldn't find something similar in the API.");
str.setSpan(new NewSpannableClass(), 0, str.length() - 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
((TextView)findViewById(R.id.text)).setText(str);

Where NewSpannableClass is the custom spannable.

Seeing as many people are lazy to look up how to make custom spannables, here's an example

public class CustomSpannable extends ClickableSpan
{
@Override public void updateDrawState(TextPaint ds)
{
super.updateDrawState(ds);
ds.setUnderlineText(true);
}
}

This example will underline the text. Use TextPaint to change the look of the spanned text.

Android Multiline text-only background

It looks like your highlighting has rounded corners. If you do want the rounded corners, take a look at Drawing a rounded corner background on text, a Medium post by Florina Muntenescu.

That solution leaves some space between the lines while you example fills that space in, but I think that you can modify the code to fill it in.

How to draw background color that wraps text length in textview?

This can't really be done using simple Canvas built-in shape drawing methods as you need to draw concave corner arcs. However, you can use Path primitive operations to build the required shapes.

It is conceivable that you can create a single path to surround the text, but I found it simpler to create individual paths for each line and decide whether corners need turned in like normal rounded rect (i.e. convex, positive radius) or turned out (concave, negative radius) depending on how the line's bounds align with those above and below.

I have provided an example below of how this can be done as a fully-working implementation of a derived TextView class.

A few additional things to note about this implementation:

The main work generating the paths is done in the onSizeChanged() method: it's bad practice to do heavy calculation and/or allocation in the draw() method. The draw() method instead simply draws the pre-calculated paths on the canvas.

The tricky bit is in the Path roundedRect() method, which creates the paths for each line. The RectF argument is the shape of the rectangle if all corners were zero radius, and each float argument gives the radius of each of the top-left, top-right, bottom-left and bottom-right corners respectively. A positive radius specifies a normal rounded corner, a negative radius specifies a turned-out concave corner, and zero specifies a straight corner with no rounding.

It is also possible to add custom attributes to control the look and feel of your custom widget. I've shown this in the example attrs.xml and in the widget constructor to specify the maximum corner radius to be used. You may wish to add others to control colour, margins etc. rather than hard-coding them as I have done in my example.

I hope this helps.

BgColourTextView.java:

package com.example.bgcolourtextview;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.widget.TextView;
import java.util.ArrayList;

public class BgColourTextView extends TextView
{

private static final float DEFAULT_MAX_CORNER_RADIUS_DIP = 10f;

private final Paint mPaint;
private final ArrayList<Path> mOutlines;
private final float mMaxCornerRadius;

public BgColourTextView(Context context) {
this(context, null, 0);
}

public BgColourTextView(Context context, AttributeSet attr) {
this(context, attr, 0);
}

public BgColourTextView(Context context, AttributeSet attr, int defStyle) {
super(context, attr, defStyle);
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mOutlines = new ArrayList<>();
TypedArray ta = context.getTheme().obtainStyledAttributes(attr, R.styleable.BgColourTextView, 0, defStyle);
try {
mMaxCornerRadius = ta.getDimension(R.styleable.BgColourTextView_maxCornerRadius,
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_MAX_CORNER_RADIUS_DIP,
context.getResources().getDisplayMetrics()));
} finally {
ta.recycle();
}
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mOutlines.clear();
final ArrayList<RectF> lineBounds = new ArrayList<>();
for (int i = 0; i < getLayout().getLineCount(); i++) {
RectF rect = new RectF(getLayout().getLineLeft(i), getLayout().getLineTop(i), getLayout().getLineRight(i), getLayout().getLineBottom(i));
rect.offset(getPaddingLeft(), getPaddingTop());
rect.inset(-getPaddingLeft() / 2f, 0f);
lineBounds.add(rect);
}
for (int i = 0; i < lineBounds.size(); i++) {
float rTl = limitRadius(i == 0 ? lineBounds.get(i).width() / 2f : (lineBounds.get(i - 1).left - lineBounds.get(i).left) / 2f);
float rTr = limitRadius(i == 0 ? lineBounds.get(i).width() / 2f : (lineBounds.get(i).right - lineBounds.get(i - 1).right) / 2f);
float rBl = limitRadius(i == lineBounds.size() - 1 ? lineBounds.get(i).width() / 2f : (lineBounds.get(i + 1).left - lineBounds.get(i).left) / 2f);
float rBr = limitRadius(i == lineBounds.size() - 1 ? lineBounds.get(i).width() / 2f : (lineBounds.get(i).right - lineBounds.get(i + 1).right) / 2f);
mOutlines.add(roundedRect(lineBounds.get(i), rTl, rTr, rBl, rBr));
}
}

@Override
public void draw(Canvas canvas) {
for (Path p: mOutlines) {
canvas.drawPath(p, mPaint);
}
super.draw(canvas);
}

/**
* Limit the corner radius to a maximum absolute value.
*/
private float limitRadius(float aRadius) {
return Math.abs(aRadius) < mMaxCornerRadius ? aRadius : mMaxCornerRadius * aRadius / Math.abs(aRadius);
}

/**
* Generate a rectangular path with rounded corners, where a positive corner radius indicates a normal convex corner and
* negative indicates a concave corner (turning out horizontally rather than round).
*/
private Path roundedRect(RectF aRect, float rTl, float rTr, float rBl, float rBr) {
Log.d("BgColourTextView", String.format("roundedRect(%s, %s, %s, %s, %s)", aRect, rTl, rTr, rBl, rBr));
Path path = new Path();
path.moveTo(aRect.right, aRect.top + Math.abs(rTr));
if (rTr > 0) {
path.arcTo(new RectF(aRect.right - 2 * rTr, aRect.top, aRect.right, aRect.top + 2 * rTr), 0, -90, false);
} else if (rTr < 0) {
path.arcTo(new RectF(aRect.right , aRect.top, aRect.right - 2 * rTr, aRect.top - 2 * rTr), 180, 90, false);
}
path.lineTo(aRect.left + 2 * Math.abs(rTl), aRect.top);
if (rTl > 0) {
path.arcTo(new RectF(aRect.left, aRect.top, aRect.left + 2 * rTl, aRect.top + 2 * rTl), 270, -90, false);
} else if (rTl < 0) {
path.arcTo(new RectF(aRect.left + 2 * rTl, aRect.top, aRect.left, aRect.top - 2 * rTl), 270, 90, false);
}
path.lineTo(aRect.left, aRect.bottom - 2 * Math.abs(rBl));
if (rBl > 0) {
path.arcTo(new RectF(aRect.left, aRect.bottom - 2 * rBl, aRect.left + 2 * rBl, aRect.bottom), 180, -90, false);
} else if (rBl < 0) {
path.arcTo(new RectF(aRect.left + 2 * rBl, aRect.bottom + 2 * rBl, aRect.left, aRect.bottom), 0, 90, false);
}
path.lineTo(aRect.right - 2 * Math.abs(rBr), aRect.bottom);
if (rBr > 0) {
path.arcTo(new RectF(aRect.right - 2 * rBr, aRect.bottom - 2 * rBr, aRect.right, aRect.bottom), 90, -90, false);
} else if (rBr < 0) {
path.arcTo(new RectF(aRect.right, aRect.bottom + 2 * rBr, aRect.right - 2 * rBr, aRect.bottom), 90, 90, false);
}
path.lineTo(aRect.right, aRect.top + Math.abs(rTr));
return path;
}

res/values/attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

<declare-styleable name="BgColourTextView">
<attr name="maxCornerRadius" format="dimension" />
</declare-styleable>

</resources>

res/layout/main.xml

<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:gravity="center"
android:orientation="vertical">

<com.example.bgcolourtextview.BgColourTextView
android:text="Example\n10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:gravity="center_horizontal"
android:textSize="32sp"
app:maxCornerRadius="10dp" />

<com.example.bgcolourtextview.BgColourTextView
android:text="Example 15dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:gravity="center_horizontal"
android:textSize="32sp"
app:maxCornerRadius="15dp" />

<com.example.bgcolourtextview.BgColourTextView
android:text="Example\n20dp radius\ntext"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:gravity="center_horizontal"
android:textSize="32sp"
app:maxCornerRadius="20dp" />

<com.example.bgcolourtextview.BgColourTextView
android:text="Example\ntext\n5dp radius\ntext"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:gravity="center_horizontal"
android:textSize="32sp"
app:maxCornerRadius="5dp" />

</LinearLayout>

Example output:

Example output



Related Topics



Leave a reply



Submit