How to prevent URLs from being wrapped in TextView? - android

I have a multiline TextView which can display some optional URL. Now I have a problem: some of my long URLs displayed wrapped in the position of ://
sometext sometext http:// <-- AUTO LINE WRAP
google.com/
How can I disable wrapping for the whole URL or at least for http(s):// prefix? I still need text wrapping to be enabled however.
My text should wrap like that
sometext sometext <-- AUTO LINE WRAP
http://google.com/

This is just proof of concept to implement custom wrap for textview.
You may need to add/edit conditions according to your requirement.
If your requirement is that our textview class must show multiline in such a way that it should not end with certain text ever (here http:// and http:),
I have modified the code of very popular textview class over SO to meet this requirement:
Source :
Auto Scale TextView Text to Fit within Bounds
Changes:
private boolean mCustomLineWrap = true;
/**
* Resize the text size with specified width and height
* #param width
* #param height
*/
public void resizeText(int width, int height) {
CharSequence text = getText();
// Do not resize if the view does not have dimensions or there is no text
if(text == null || text.length() == 0 || height <= 0 || width <= 0 || mTextSize == 0) {
return;
}
// Get the text view's paint object
TextPaint textPaint = getPaint();
// Store the current text size
float oldTextSize = textPaint.getTextSize();
// If there is a max text size set, use the lesser of that and the default text size
float targetTextSize = mMaxTextSize > 0 ? Math.min(mTextSize, mMaxTextSize) : mTextSize;
// Get the required text height
int textHeight = getTextHeight(text, textPaint, width, targetTextSize);
// Until we either fit within our text view or we had reached our min text size, incrementally try smaller sizes
while(textHeight > height && targetTextSize > mMinTextSize) {
targetTextSize = Math.max(targetTextSize - 2, mMinTextSize);
textHeight = getTextHeight(text, textPaint, width, targetTextSize);
}
if(mCustomLineWrap ) {
// Draw using a static layout
StaticLayout layout = new StaticLayout(text, textPaint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, false);
// Check that we have a least one line of rendered text
if(layout.getLineCount() > 0) {
String lineText[] = new String[layout.getLineCount()];
// Since the line at the specific vertical position would be cut off,
// we must trim up to the previous line
String wrapStr = "http:", wrapStrWithSlash = "http://";
boolean preAppendWrapStr = false, preAppendWrapStrWithSlash = false ;
for(int lastLine = 0; lastLine < layout.getLineCount(); lastLine++)
{
int start = layout.getLineStart(lastLine);
int end = layout.getLineEnd(lastLine);
lineText[lastLine] = ((String) getText()).substring(start,end);
if(preAppendWrapStr)
{
lineText[lastLine] = "\n" + wrapStr + lineText[lastLine];
preAppendWrapStr = false;
}
else if(preAppendWrapStrWithSlash)
{
lineText[lastLine] = "\n" + wrapStrWithSlash + lineText[lastLine];
preAppendWrapStrWithSlash = false;
}
if(lineText[lastLine].endsWith(wrapStr))
{
preAppendWrapStr = true;
lineText[lastLine] = lineText[lastLine].substring(0,lineText[lastLine].lastIndexOf(wrapStr));
}
if( lineText[lastLine].endsWith(wrapStrWithSlash))
{
preAppendWrapStrWithSlash = true;
lineText[lastLine] = lineText[lastLine].substring(0,lineText[lastLine].lastIndexOf(wrapStrWithSlash));
}
}
String compString = "";
for(String lineStr : lineText)
{
compString += lineStr;
}
setText(compString);
}
}
// Some devices try to auto adjust line spacing, so force default line spacing
// and invalidate the layout as a side effect
textPaint.setTextSize(targetTextSize);
setLineSpacing(mSpacingAdd, mSpacingMult);
// Notify the listener if registered
if(mTextResizeListener != null) {
mTextResizeListener.onTextResize(this, oldTextSize, targetTextSize);
}
// Reset force resize flag
mNeedsResize = false;
}

Related

Is there a way to put label text vertically in flutter charts_flutter: ^0.8.1

I am trying to create a graph using charts_flutter: ^0.8.1 package in flutter.
Here is my code. I have added SlidingViewport and PanAndZoomBehavior in behaviour.
charts.BarChart(
generateGraphData(months),
barRendererDecorator: charts.BarLabelDecorator<String>(
labelAnchor: charts.BarLabelAnchor.middle,
labelPosition: charts.BarLabelPosition.inside, ),
domainAxis: new charts.OrdinalAxisSpec(viewport: new charts.OrdinalViewport('month', 5),),
animate: false,
rtlSpec: charts.RTLSpec(axisDirection: charts.AxisDirection.reversed),
defaultInteractions: false,
behaviors: [
new charts.SlidingViewport(),
new charts.PanAndZoomBehavior(),
],
),
Is it possible to make the labeltext align vertically like in Horizontal Bar Label Bar? Some thing like the below image. I want to align the label text in the direction of bar chart.
To achieve it you need to create a new class where you need to overwrite one of the internal classes - 'BarRendererDecorator'. In that class there is a method - _decorateVerticalBars which writes the value inside the bars. There is a method call - canvas.drawText() which needs to be fed with one new argument rotation:-math.pi/2. Also to center the text inside the bar need to change the X offset like this - labelX = labelX + (labelElement.measurement.horizontalSliceWidth / 2.5).round();
And then in your decorator call in your graph use the new class you have created. Here is the complete code of the new class. And accept it as answer if it works for you.
import 'dart:math' as math;
import 'package:meta/meta.dart' show required;
import "package:charts_common/common.dart"
show
Color,
GraphicsFactory,
TextDirection,
TextElement,
TextStyle,
ChartCanvas,
TextStyleSpec,
ImmutableBarRendererElement,
BarRendererDecorator;
import "package:charts_common/src/data/series.dart" show AccessorFn;
import "chart_canvas_wrkaround.dart";
import "package:charts_flutter/src/chart_canvas.dart" as Canvasee;
import "package:charts_common/common.dart" as comm;
/* import '../../common/color.dart' show Color;
import '../../common/graphics_factory.dart' show GraphicsFactory;
import '../../common/text_element.dart' show TextDirection, TextElement;
import '../../common/text_style.dart' show TextStyle;
import '../../data/series.dart' show AccessorFn;
import '../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
import '../common/chart_canvas.dart' show ChartCanvas;
import 'bar_renderer.dart' show ImmutableBarRendererElement;
import 'bar_renderer_decorator.dart' show BarRendererDecorator; */
class BarLabelDecoratorWorkAround<D> extends BarRendererDecorator<D> {
// Default configuration
static const _defaultLabelPosition = BarLabelPosition.auto;
static const _defaultLabelPadding = 5;
static const _defaultHorizontalLabelAnchor = BarLabelAnchor.start;
static const _defaultVerticalLabelAnchor = BarLabelAnchor.end;
static final _defaultInsideLabelStyle =
new TextStyleSpec(fontSize: 12, color: Color.white);
static final _defaultOutsideLabelStyle =
new TextStyleSpec(fontSize: 12, color: Color.black);
static final _labelSplitPattern = '\n';
static final _defaultMultiLineLabelPadding = 2;
/// Configures [TextStyleSpec] for labels placed inside the bars.
final TextStyleSpec insideLabelStyleSpec;
/// Configures [TextStyleSpec] for labels placed outside the bars.
final TextStyleSpec outsideLabelStyleSpec;
/// Configures where to place the label relative to the bars.
final BarLabelPosition labelPosition;
/// For labels drawn inside the bar, configures label anchor position.
final BarLabelAnchor labelAnchor;
/// Space before and after the label text.
final int labelPadding;
BarLabelDecoratorWorkAround(
{TextStyleSpec insideLabelStyleSpec,
TextStyleSpec outsideLabelStyleSpec,
this.labelAnchor = null,
this.labelPosition = _defaultLabelPosition,
this.labelPadding = _defaultLabelPadding})
: insideLabelStyleSpec = insideLabelStyleSpec ?? _defaultInsideLabelStyle,
outsideLabelStyleSpec =
outsideLabelStyleSpec ?? _defaultOutsideLabelStyle;
#override
void decorate(Iterable<ImmutableBarRendererElement<D>> barElements,
comm.ChartCanvas canvas, GraphicsFactory graphicsFactory,
{#required math.Rectangle drawBounds,
#required double animationPercent,
#required bool renderingVertically,
bool rtl = false}) {
// Only decorate the bars when animation is at 100%.
if (animationPercent != 1.0) {
return;
}
/* final newCanvas = canvas as Canvasee.ChartCanvas;
final canvee =
ChartCanvasWorkAround(newCanvas.canvas, newCanvas.graphicsFactory); */
if (renderingVertically) {
_decorateVerticalBars(
barElements, canvas, graphicsFactory, drawBounds, rtl);
} else {
_decorateHorizontalBars(
barElements, canvas, graphicsFactory, drawBounds, rtl);
}
}
void _decorateVerticalBars(
Iterable<ImmutableBarRendererElement<D>> barElements,
ChartCanvas canvas,
GraphicsFactory graphicsFactory,
math.Rectangle drawBounds,
bool rtl) {
// Create [TextStyle] from [TextStyleSpec] to be used by all the elements.
// The [GraphicsFactory] is needed so it can't be created earlier.
final insideLabelStyle =
_getTextStyle(graphicsFactory, insideLabelStyleSpec);
final outsideLabelStyle =
_getTextStyle(graphicsFactory, outsideLabelStyleSpec);
for (var element in barElements) {
final labelFn = element.series.labelAccessorFn;
final datumIndex = element.index;
final label = (labelFn != null) ? labelFn(datumIndex) : null;
// If there are custom styles, use that instead of the default or the
// style defined for the entire decorator.
final datumInsideLabelStyle = _getDatumStyle(
element.series.insideLabelStyleAccessorFn,
datumIndex,
graphicsFactory,
defaultStyle: insideLabelStyle);
final datumOutsideLabelStyle = _getDatumStyle(
element.series.outsideLabelStyleAccessorFn,
datumIndex,
graphicsFactory,
defaultStyle: outsideLabelStyle);
// Skip calculation and drawing for this element if no label.
if (label == null || label.isEmpty) {
continue;
}
var labelElements = label
.split(_labelSplitPattern)
.map((labelPart) => graphicsFactory.createTextElement(labelPart));
final bounds = element.bounds;
// Get space available inside and outside the bar.
final totalPadding = labelPadding * 2;
final insideBarHeight = bounds.height - totalPadding;
final outsideBarHeight = drawBounds.height - bounds.height - totalPadding;
var calculatedLabelPosition = labelPosition;
if (calculatedLabelPosition == BarLabelPosition.auto) {
// For auto, first try to fit the text inside the bar.
labelElements = labelElements.map(
(labelElement) => labelElement..textStyle = datumInsideLabelStyle);
final labelMaxWidth = labelElements
.map(
(labelElement) => labelElement.measurement.horizontalSliceWidth)
.fold(0, (max, current) => max > current ? max : current);
// Total label height depends on the label element's text style.
final totalLabelHeight = _getTotalLabelHeight(labelElements);
// A label fits if the length and width of the text fits.
calculatedLabelPosition =
totalLabelHeight < insideBarHeight && labelMaxWidth < bounds.width
? BarLabelPosition.inside
: BarLabelPosition.outside;
}
// Set the max width, text style, and text direction.
labelElements = labelElements.map((labelElement) => labelElement
..textStyle = calculatedLabelPosition == BarLabelPosition.inside
? datumInsideLabelStyle
: datumOutsideLabelStyle
..maxWidth = bounds.height * 100
..textDirection = rtl ? TextDirection.rtl : TextDirection.ltr);
// Total label height depends on the label element's text style.
final totalLabelHeight = _getTotalLabelHeight(labelElements);
var labelsDrawn = 0;
for (var labelElement in labelElements) {
// Calculate the start position of label based on [labelAnchor].
int labelY;
final labelHeight = labelElement.measurement.verticalSliceWidth.round();
final offsetHeight =
(labelHeight + _defaultMultiLineLabelPadding) * labelsDrawn;
if (calculatedLabelPosition == BarLabelPosition.inside) {
final _labelAnchor = labelAnchor ?? _defaultVerticalLabelAnchor;
switch (_labelAnchor) {
case BarLabelAnchor.end:
labelY = bounds.top + labelPadding + offsetHeight;
break;
case BarLabelAnchor.middle:
labelY = (bounds.bottom -
bounds.height / 2 -
totalLabelHeight / 2 +
offsetHeight)
.round();
break;
case BarLabelAnchor.start:
labelY = bounds.bottom -
labelPadding -
totalLabelHeight +
offsetHeight;
break;
}
} else {
// calculatedLabelPosition == LabelPosition.outside
labelY = bounds.top - labelPadding - totalLabelHeight + offsetHeight;
}
// Center the label inside the bar.
int labelX = (bounds.left +
bounds.width / 2 -
labelElement.measurement.horizontalSliceWidth / 2)
.round();
labelX = labelX +
(labelElement.measurement.horizontalSliceWidth / 2.5).round();
canvas.drawText(labelElement, labelX, labelY, rotation: -math.pi / 2);
labelsDrawn += 1;
}
}
}
void _decorateHorizontalBars(
Iterable<ImmutableBarRendererElement<D>> barElements,
ChartCanvas canvas,
GraphicsFactory graphicsFactory,
math.Rectangle drawBounds,
bool rtl) {
// Create [TextStyle] from [TextStyleSpec] to be used by all the elements.
// The [GraphicsFactory] is needed so it can't be created earlier.
final insideLabelStyle =
_getTextStyle(graphicsFactory, insideLabelStyleSpec);
final outsideLabelStyle =
_getTextStyle(graphicsFactory, outsideLabelStyleSpec);
for (var element in barElements) {
final labelFn = element.series.labelAccessorFn;
final datumIndex = element.index;
final label = (labelFn != null) ? labelFn(datumIndex) : null;
// If there are custom styles, use that instead of the default or the
// style defined for the entire decorator.
final datumInsideLabelStyle = _getDatumStyle(
element.series.insideLabelStyleAccessorFn,
datumIndex,
graphicsFactory,
defaultStyle: insideLabelStyle);
final datumOutsideLabelStyle = _getDatumStyle(
element.series.outsideLabelStyleAccessorFn,
datumIndex,
graphicsFactory,
defaultStyle: outsideLabelStyle);
// Skip calculation and drawing for this element if no label.
if (label == null || label.isEmpty) {
continue;
}
final bounds = element.bounds;
// Get space available inside and outside the bar.
final totalPadding = labelPadding * 2;
final insideBarWidth = bounds.width - totalPadding;
final outsideBarWidth = drawBounds.width - bounds.width - totalPadding;
final labelElement = graphicsFactory.createTextElement(label);
var calculatedLabelPosition = labelPosition;
if (calculatedLabelPosition == BarLabelPosition.auto) {
// For auto, first try to fit the text inside the bar.
labelElement.textStyle = datumInsideLabelStyle;
// A label fits if the space inside the bar is >= outside bar or if the
// length of the text fits and the space. This is because if the bar has
// more space than the outside, it makes more sense to place the label
// inside the bar, even if the entire label does not fit.
calculatedLabelPosition = (insideBarWidth >= outsideBarWidth ||
labelElement.measurement.horizontalSliceWidth < insideBarWidth)
? BarLabelPosition.inside
: BarLabelPosition.outside;
}
// Set the max width and text style.
if (calculatedLabelPosition == BarLabelPosition.inside) {
labelElement.textStyle = datumInsideLabelStyle;
labelElement.maxWidth = insideBarWidth;
} else {
// calculatedLabelPosition == LabelPosition.outside
labelElement.textStyle = datumOutsideLabelStyle;
labelElement.maxWidth = outsideBarWidth;
}
// Only calculate and draw label if there's actually space for the label.
if (labelElement.maxWidth > 0) {
// Calculate the start position of label based on [labelAnchor].
int labelX;
if (calculatedLabelPosition == BarLabelPosition.inside) {
final _labelAnchor = labelAnchor ?? _defaultHorizontalLabelAnchor;
switch (_labelAnchor) {
case BarLabelAnchor.middle:
labelX = (bounds.left +
bounds.width / 2 -
labelElement.measurement.horizontalSliceWidth / 2)
.round();
labelElement.textDirection =
rtl ? TextDirection.rtl : TextDirection.ltr;
break;
case BarLabelAnchor.end:
case BarLabelAnchor.start:
final alignLeft = rtl
? (_labelAnchor == BarLabelAnchor.end)
: (_labelAnchor == BarLabelAnchor.start);
if (alignLeft) {
labelX = bounds.left + labelPadding;
labelElement.textDirection = TextDirection.ltr;
} else {
labelX = bounds.right - labelPadding;
labelElement.textDirection = TextDirection.rtl;
}
break;
}
} else {
// calculatedLabelPosition == LabelPosition.outside
labelX = bounds.right + labelPadding;
labelElement.textDirection = TextDirection.ltr;
}
// Center the label inside the bar.
final labelY = (bounds.top +
(bounds.bottom - bounds.top) / 2 -
labelElement.measurement.verticalSliceWidth / 2)
.round();
canvas.drawText(labelElement, labelX, labelY);
}
}
}
/// Helper function to get the total height for a group of labels.
/// This includes the padding in between the labels.
int _getTotalLabelHeight(Iterable<TextElement> labelElements) =>
(labelElements.first.measurement.verticalSliceWidth *
labelElements.length)
.round() +
_defaultMultiLineLabelPadding * (labelElements.length - 1);
// Helper function that converts [TextStyleSpec] to [TextStyle].
TextStyle _getTextStyle(
GraphicsFactory graphicsFactory, TextStyleSpec labelSpec) {
return graphicsFactory.createTextPaint()
..color = labelSpec?.color ?? Color.black
..fontFamily = labelSpec?.fontFamily
..fontSize = labelSpec?.fontSize ?? 12
..lineHeight = labelSpec?.lineHeight;
}
/// Helper function to get datum specific style
TextStyle _getDatumStyle(AccessorFn<TextStyleSpec> labelFn, int datumIndex,
GraphicsFactory graphicsFactory,
{TextStyle defaultStyle}) {
final styleSpec = (labelFn != null) ? labelFn(datumIndex) : null;
return (styleSpec != null)
? _getTextStyle(graphicsFactory, styleSpec)
: defaultStyle;
}
}
/// Configures where to place the label relative to the bars.
enum BarLabelPosition {
/// Automatically try to place the label inside the bar first and place it on
/// the outside of the space available outside the bar is greater than space
/// available inside the bar.
auto,
/// Always place label on the outside.
outside,
/// Always place label on the inside.
inside,
}
/// Configures where to anchor the label for labels drawn inside the bars.
enum BarLabelAnchor {
/// Anchor to the measure start.
start,
/// Anchor to the middle of the measure range.
middle,
/// Anchor to the measure end.
end,
}

canvas drawtext with multiline

I am developing a image commenting application. I draw text in canvas with canvas.drawText(text, x, y, imgPaint);
This appears in a single line. I need to break the line to multiline when the text crosses the canvas width
Thanks in advance
You need to use StaticLayout:
TextPaint mTextPaint=new TextPaint();
StaticLayout mTextLayout = new StaticLayout("my text\nNext line is very long text that does not definitely fit in a single line on an android device. This will show you how!", mTextPaint, canvas.getWidth(), Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
canvas.save();
// calculate x and y position where your text will be placed
textX = 100;
textY = 100;
canvas.translate(textX, textY);
mTextLayout.draw(canvas);
canvas.restore();
You need to split the line and draw each fragment separately with an increasing y based on font-height.
For example:
var lines = text.split("\n"),
x = 100, y = 100, fHeight = 16, // get x, y and proper font or line height here
i = 0, line;
while(line = lines[i++]) {
canvas.drawText(line, x, y, imgPaint);
y += fHeight;
}
Well it's quite late to add another answer but if someone doesn't want to use StaticLayout then they can try my logic for multiLine text
Note : This code is used in onSizeChanged() method of View and textArray is a class variable that store each line
//This array will store all the words contained in input string
val wordList = ArrayList<String>()
//Temporary variable to store char or string
var temp = ""
it.trim().forEachIndexed { index, letter ->
//Adding each letter to temp
temp += letter
//If letter is whiteSpace or last char then add it to wordList.
//For example : Let input be "This is a Info text"
// since there is no whiteSpace after that last 't' then the last word
// will not be added to wordList there for checking for last letter is required
//NOTE: the whiteSpace is also included in that word
if (letter.isWhitespace() || index == it.length -1 ) {
wordList.add(temp)
//Resetting temp
temp = ""
}
}
wordList.forEachIndexed { index, word ->
//Measuring temp + word to check if their width in pixel is more than or equal to
// the view's width + 60px(this is used so that word there is some space after each line. It can be changed)
if (textPaint.measureText(temp + word) >= w - 60) {
textArray.add(temp)
//If adding last word to temp surpasses the required width then add the last word
// separately since the loop will be terminated after that
if (index == wordList.size - 1){
textArray.add(word)
return#forEachIndexed
}
//Resetting temp
temp = ""
} else if (index == wordList.size - 1) {
//If adding last word to temp doesn't surpasses the required width the add that
// line to list
textArray.add(temp + word)
return#forEachIndexed
}
//Adding word to temp
temp += word
}
Then in onDraw() method
textArray.forEachIndexed { index, singleLine ->
//x is set to 16f so that there is some space before first word
//y changes with each line i.e 1st line will be drawn at y = 60f, 2nd at 120f and so on
it.drawText(singleLine, 16f, (index + 1) * 60f, textPaint)
}

Get absolute position for a given offset on TextView (Android)

I have a TextView in which I want to place a solid color block over given words of the TextView, for example:
"This is a text string, I want to put a rectangle over this WORD" - so, "WORD" would have a rectangle with a solid color over it.
To do this, I am thinking about overriding the onDraw(Canvas canvas) method, in order to draw a block over the text. My only problem is to find an efficient way to get the absolute position of a given word or character.
Basically, I am looking for something that does the exact opposite of the getOffsetForPosition(float x, float y) method
Based on this post: How get coordinate of a ClickableSpan inside a TextView?, I managed to use this code in order to put a rectangle on top of the text:
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.WHITE);
// Initialize global value
TextView parentTextView = this;
Rect parentTextViewRect = new Rect();
// Find where the WORD is
String targetWord = "WORD";
int startOffsetOfClickedText = this.getText().toString().indexOf(targetWord);
int endOffsetOfClickedText = startOffsetOfClickedText + targetWord.length();
// Initialize values for the computing of clickedText position
Layout textViewLayout = parentTextView.getLayout();
double startXCoordinatesOfClickedText = textViewLayout.getPrimaryHorizontal((int)startOffsetOfClickedText);
double endXCoordinatesOfClickedText = textViewLayout.getPrimaryHorizontal((int)endOffsetOfClickedText);
// Get the rectangle of the clicked text
int currentLineStartOffset = textViewLayout.getLineForOffset((int)startOffsetOfClickedText);
int currentLineEndOffset = textViewLayout.getLineForOffset((int)endOffsetOfClickedText);
boolean keywordIsInMultiLine = currentLineStartOffset != currentLineEndOffset;
textViewLayout.getLineBounds(currentLineStartOffset, parentTextViewRect);
// Update the rectangle position to his real position on screen
int[] parentTextViewLocation = {0,0};
parentTextView.getLocationOnScreen(parentTextViewLocation);
double parentTextViewTopAndBottomOffset = (
//parentTextViewLocation[1] -
parentTextView.getScrollY() +
parentTextView.getCompoundPaddingTop()
);
parentTextViewRect.top += parentTextViewTopAndBottomOffset;
parentTextViewRect.bottom += parentTextViewTopAndBottomOffset;
// In the case of multi line text, we have to choose what rectangle take
if (keywordIsInMultiLine){
WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
int screenHeight = display.getHeight();
int dyTop = parentTextViewRect.top;
int dyBottom = screenHeight - parentTextViewRect.bottom;
boolean onTop = dyTop > dyBottom;
if (onTop){
endXCoordinatesOfClickedText = textViewLayout.getLineRight(currentLineStartOffset);
}
else{
parentTextViewRect = new Rect();
textViewLayout.getLineBounds(currentLineEndOffset, parentTextViewRect);
parentTextViewRect.top += parentTextViewTopAndBottomOffset;
parentTextViewRect.bottom += parentTextViewTopAndBottomOffset;
startXCoordinatesOfClickedText = textViewLayout.getLineLeft(currentLineEndOffset);
}
}
parentTextViewRect.left += (
parentTextViewLocation[0] +
startXCoordinatesOfClickedText +
parentTextView.getCompoundPaddingLeft() -
parentTextView.getScrollX()
);
parentTextViewRect.right = (int) (
parentTextViewRect.left +
endXCoordinatesOfClickedText -
startXCoordinatesOfClickedText
);
canvas.drawRect(parentTextViewRect, paint);
}
You can use spans for that.
First you create a spannable for your text, like this:
Spannable span = new SpannableString(text);
Then you put a span around the word that you want to highlight, somewhat like this:
span.setSpan(new UnderlineSpan(), start, end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Unfortunately I don't know of an existing span that puts a border around a word. I found UnderlineSpan, and also BackgroundColorSpan, perhaps these are also useful for you, or you can have a look at the code and see if you can create a BorderSpan based on one of those.
Instead of drawing a rectangle over the WORD, you could simply replace its characters with an appropriate unicode symbol like U+25AE (▮ Black vertical rectangle).
So you'd get
"This is a text string, I want to put a rectangle over this ▮▮▮▮"
If that is sufficient. See for example Wikipedia for a wast list of unicode symbols.
If you actually need to paint that black box you can do the following as long as your text is in a single line:
Calculate the width of the text part before 'WORD' as explained here to find the left edge of the box and calcuate the width of 'WORD' using the same method to find the width of the box.
For a multiline text the explained method might also work but I think you'll have to do quite a lot of work here.
use getLayout().getLineBottom and textpaint.measureText to manually do the reverse calculation of getOffsetForPosition.
below is an example of using the calculated x,y for some textOffset to position the handle drawable when the textview gets clicked.
class TextViewCustom extends TextView{
float lastX,lastY;
#Override
public boolean onTouchEvent(MotionEvent event) {
boolean ret = super.onTouchEvent(event);
lastX=event.getX();
lastY=event.getY();
return ret;
}
BreakIterator boundary;
Drawable handleLeft;
private void init() {// call it in constructors
boundary = BreakIterator.getWordInstance();
handleLeft=getResources().getDrawable(R.drawable.abc_text_select_handle_left_mtrl_dark);
setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
int line = getLayout().getLineForVertical((int) lastY);
int offset = getLayout().getOffsetForHorizontal(line, lastX);
int wordEnd = boundary.following(offset);
int wordStart = boundary.previous();
CMN.Log(getText().subSequence(wordStart, wordEnd));
int y = getLayout().getLineBottom(line);
int trimA = getLayout().getLineStart(line);
float x = getPaddingLeft()+getPaint().measureText(getText(), trimA, wordStart);
x-=handleLeft.getIntrinsicWidth()*1.f*9/12;
handleLeft.setBounds((int)x,y,(int)(x+handleLeft.getIntrinsicWidth()),y+handleLeft.getIntrinsicHeight());
invalidate();
}
});
}
#Override
public void setText(CharSequence text, BufferType type) {
super.setText(text, type);
if(boundary!=null)
boundary.setText(text.toString());
}
}

Get current visible text in textview

I have a long passage in a TextView which is wrapped around by ScrollView. Is there any way to find the current visible text?
I can find the number of lines, line height in textview and also scrollx and scrolly from scrollview, but find the linkage to the current displayed text. Please help! Thanks.
It is simple to do this:
int start = textView.getLayout().getLineStart(0);
int end = textView.getLayout().getLineEnd(textView.getLineCount() - 1);
String displayed = textView.getText().toString().substring(start, end);
Here. Get the line number of the first displayed line. Then get the line number of the second displayed line. Then get the text and count the number of words.
private int getNumberOfWordsDisplayed() {
int start = textView.getLayout().getLineStart(getFirstLineIndex());
int end = textView.getLayout().getLineEnd(getLastLineIndex());
return textView.getText().toString().substring(start, end).split(" ").length;
}
/**
* Gets the first line that is visible on the screen.
*
* #return
*/
public int getFirstLineIndex() {
int scrollY = scrollView.getScrollY();
Layout layout = textView.getLayout();
if (layout != null) {
return layout.getLineForVertical(scrollY);
}
Log.d(TAG, "Layout is null: ");
return -1;
}
/**
* Gets the last visible line number on the screen.
* #return last line that is visible on the screen.
*/
public int getLastLineIndex() {
int height = scrollView.getHeight();
int scrollY = scrollView.getScrollY();
Layout layout = textView.getLayout();
if (layout != null) {
return layout.getLineForVertical(scrollY + height);
}
return -1;
}
Using textView.getLayout().getEllipsisStart(0) only works if android:singleLine="true"
Here is a solution that will work if android:maxLines is set:
public static String getVisibleText(TextView textView) {
// test that we have a textview and it has text
if (textView==null || TextUtils.isEmpty(textView.getText())) return null;
Layout l = textView.getLayout();
if (l!=null) {
// find the last visible position
int end = l.getLineEnd(textView.getMaxLines()-1);
// get only the text after that position
return textView.getText().toString().substring(0,end);
}
return null;
}
Remember: this works after the view is already loaded.
Usage:
textView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
textView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
Log.i("test" ,"VisibleText="+getVisibleText(textView));
}
});
You claim that you know scrollY, the current number of pixels scrolled. You also know the height of the window you're considering in pixels, so call that scrollViewHeight. Then
int scrollY; // This is your current scroll position in pixels.
int scrollViewHeight; // This is the height of your scrolling window.
TextView textView; // This is the TextView we're considering.
String text = (String) textView.getText();
int charsPerLine = text.length() / textView.getLineCount();
int lineHeight = textView.getLineHeight();
int startLine = scrollY / lineHeight;
int endLine = startLine + scrollViewHeight/lineHeight + 1;
int startChar = charsPerLine * startLine;
int endChar = charsPerLine * (endLine+1) + 1;
String approxVisibleString = text.substring(startChar, endChar);
It's an approximation, so use it as a last resort.
I also having about the same problem myself. I needed first visible line from textview currently visible in recyclerview. If you are trying to get currently displayed first line of textview in recyclerview you may use the following code:
TextView tv = (TextView) recyclerView.getChildAt(0); //gets current visible child view
// this is for top visible
//view or the textview directly
Rect r1 = new Rect();
tv.getHitRect(r1);//gets visible rect of textview
Layout l = tv.getLayout();
int line = l.getLineForVertical(-1 * r1.top);//first visible line
int start = l.getLineStart(line);//visible line start
int end = l.getLineEnd(line);//visible line end
String displayed = tv.getText().toString().substring(start, end);
try use getEllipsisStart()
int end = textView.getLayout().getEllipsisStart(0);
This depends on the use of Ellipsize in the TextView. Try this:
public String getVisibleText(TextView tv) {
int lastLine = tv.getMaxLines() < 1 || tv.getMaxLines() > tv.getLineCount() ? tv.getLineCount() : tv.getMaxLines();
if (tv.getEllipsize() != null && tv.getEllipsize().equals(TextUtils.TruncateAt.END)) {
int ellCount = tv.getLayout().getEllipsisCount(lastLine - 1);
if (ellCount > 0 && tv.length() > ellCount)
return tv.getText().toString().substring(0, tv_title.getText().length() - ellCount);
return tv.getText().toString();
} else {
int end = tv.getLayout().getLineEnd(lastLine - 1);
return tv.getText().toString().substring(0, end);
}
}
...
textView.post(new Runnable() {
#Override
public void run() {
Log.d(TAG, getVisibleText(textView));
}
});
Assuming you have the scrolled line number, you can use the following to get displayed text:
int start = tv.getLayout().getLineStart(scrolllinenumber);
int end=scrolllinenumber+tv.getLayout().getHeight();
String displayedtext = tv.getText().toString().substring(start, end);

How to find android TextView number of characters per line?

So I have a TextView in android that has the width of the whole length of the screen and a padding of dip 5. How can I calculate the number of characters that will fit a single line on the screen? I guess in other words, I'm trying to get the number of columns of a textview?
I considered manual calculation depending on textsize and width, but 1) don't know the correlation and 2) due to the padding in the units of dip, different screens will use different number of actual pixels to pad.
Overall Question: I am trying to use this to solve: if given a string how can I manually edit to string such that when the textview prints the string character by character, I will know when to start a word that won't fit on one line on the next. Note: I know that textview automatically puts words that won't fit onto the next line, however, since I'm printing character by character, like typing animation, textview doesn't know the word won't fit until it prints out the overflowing characters of that word.
Been searching everywhere for this...
Thanks!
Added solutions:
one possible solution:
public String measure2 (TextView t, String s) {
String u = "";
int start = 0;
int end = 1;
int space = 0;
boolean ellipsized = false;
float fwidth = t.getMeasuredWidth();
for(;;) {
//t.setText(s.substring(start, end));
float twidth = t.getPaint().measureText(s.substring(start, end));
if (twidth < fwidth){
if (end < s.length())
end++;
else {
if (!ellipsized)
return s;
return u + s.subSequence(start, end);
}
}
else {
ellipsized = true;
space = (u + s.substring(start, end)).lastIndexOf(" ");
if (space == -1)
space = end - 1;
u += s.subSequence(start, space) + "\n";
start = space + 1;
end = start + 1;
}
}
}
solution 2, but still uses solution1 sometimes:
public String measure3 (TextView t, String s) {
List<String> wlist = Arrays.asList(s.split(" "));
if (wlist.size() == 1)
return measure2(t, s);
String u = "";
int end = 1;
float fwidth = t.getMeasuredWidth();
for(;;) {
//t.setText(s.substring(start, end));
if (wlist.isEmpty())
return u;
String temp = listStr(wlist, end);
float twidth = t.getPaint().measureText(temp);
if (twidth < fwidth){
if (end < wlist.size())
end++;
else {
return u + temp;
}
}
else {
temp = listStr(wlist, end-1);
if (end == 1)
temp = measure2(t, temp);
if (wlist.isEmpty())
return u + temp;
else
u = u + temp + "\n";
wlist = wlist.subList(end - 1, wlist.size());
end = 1;
}
}
}
public String listStr (List<String> arr, int end) {
String s = "";
for (String e : arr.subList(0, end) ){
s = s + e + " ";
}
return s.trim();
}
I used the above code to generate off a original string s, a string u that would be printed. However, I think this approach is very inefficient. Is there another approach or a better algorithm? Note: there are some errors in measure3 that I fixed, but was too lazy to edit
Try this:
private boolean isTooLarge (TextView text, String newText) {
float textWidth = text.getPaint().measureText(newText);
return (textWidth >= text.getMeasuredWidth ());
}
Detecting how many characters fit will be impossible due to the variable width of the characters. The above function will test if a particular string will fit or not in the TextView. The content of newText should be all the characters in a particular line. If true, then start a new line (and using a new string to pass as parameter).
Answer to the comment:
because the app can be run in many systems is exactly why you need to measure it.
This is a way to solve your "overall question". What is the difference between using str.size()>numCol vs is too large? You will need to implement your animation (hint #1: insert a newline character)
as I said before when you start a new line, you start a new string (hint #2: if you extend TextView, you can implement all this in overriding setText). (hint #3: Keep track of the lines created with a static int lines; and use newString.split("\\r?\\n")[lines-1] to check for length).
You can get total line of Textview and get string for each characters by below code.Then you can set style to each line whichever you want.
I set first line bold.
private void setLayoutListner( final TextView textView ) {
textView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
textView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
final Layout layout = textView.getLayout();
// Loop over all the lines and do whatever you need with
// the width of the line
for (int i = 0; i < layout.getLineCount(); i++) {
int end = layout.getLineEnd(0);
SpannableString content = new SpannableString( textView.getText().toString() );
content.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, end, 0);
content.setSpan(new StyleSpan(android.graphics.Typeface.NORMAL), end, content.length(), 0);
textView.setText( content );
}
}
});
}
Try this way.You can apply multiple style this way.
I had the same issue and I calculated the number characters per line by 2 steps:
Step 1: Calculate the number of lines
val widthOfTvComment = widthOfScreen - marginLeft - marginRight
var bounds = Rect()
var paint = Paint()
paint.textSize = textSize
paint.getTextBounds(comment,0,comment.length,bounds)
val lines = ( bounds.width()/widthOfTvComment)
Step 2: Calculated the number characters per line
val charactersPerLine = comment.length / lines

Categories

Resources