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)
}
Related
What is the right way to know the frame of a word inside of a line in Corona SDK? in other words, I want to add a rect above a specific word, I tried using webview instead and mark tag but the webview keeps flickering on iOS, so the webview solution is canceled.
I have added a rect manually into this word, but what is the best way that I specify a word and know it's frame so I highlight it with the rect, like move the rect to the next word? the font I use is Arial, and it's not monospaced.
local myRect = display.newRect(20,165,32,12.5)
myRect.alpha = 0.5
myRect:setFillColor(1,1,0)
myRect.anchorX = 0
local myString = "Word is highlighted"
local line = display.newText(myString, 0,165, "Arial", 12.5)
line.anchorX = 0
line.x = 20
thanks a lot.
If I were you I would just write additional function for that. Something like:
function highlightedText(pre, high, post, x, y, font, size)
local text = display.newText("", x, y, font, size)
local dx, rectangle = 0
if pre then
local t = display.newText(pre, 0, 0, font, size)
text.text = pre
dx = t.width
-- We need to add line below according to Corona Docs
text.anchorX = 0 text.x = x text.y = y
t:removeSelf()
end
if high then
local t = display.newText(high, 0, 0, font, size)
rectangle = display.newRect(x+dx, y, t.width, t.height)
rectangle:toBack()
text.text = text.text .. high
-- We need to add line below according to Corona Docs
text.anchorX = 0 text.x = x text.y = y
t:removeSelf()
end
if post then
text.text = text.text .. post
-- We need to add line below according to Corona Docs
text.anchorX = 0 text.x = x text.y = y
end
return text, rectangle, dx -- `dx` passed in case of moving whole text along with rectangle
end
local text, rect, _ = highlightedText("The", "word", "is highlighted.", 10, 10, "Arial", 12.5)
rect.alpha = 0.5
rect:setFillColor(1,1,0)
rect.strokeWidth = 3
text:setFillColor(1,1,1)
I'm no expert in Corona but this should work just fine with single-line static text.
Try this one.
local player2 = display.newText("test221234567890", 210, 210 )
player2:setFillColor( 0.6, 0.4, 0.8 )
local myRectangle = display.newRect( player2.x, player2.y, player2.width, player2.height )
myRectangle.strokeWidth = 3
myRectangle:setFillColor( 0.5 )
myRectangle:toBack()
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;
}
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());
}
}
How can I get the height of a given string's descender?
For instance,
abc should return 0.
abcl should return 0.
abcp should return distance from descnder line to baseline.
abclp should return distance from descnder line to baseline.
The best I could came out so far is
private int getDecender(String string, Paint paint) {
// Append "l", to ensure there is Ascender
string = string + "l";
final String stringWithoutDecender = "l";
final Rect bounds = new Rect();
final Rect boundsForStringWithoutDecender = new Rect();
paint.getTextBounds(string, 0, string.length(), bounds);
paint.getTextBounds(stringWithoutDecender, 0, stringWithoutDecender.length(), boundsForStringWithoutDecender);
return bounds.height() - boundsForStringWithoutDecender.height();
}
However, my code smell is that they are not good enough. Is there any better and faster way?
Actually I was looking for the same functionality. It turns out there is much simpler way,
you do not even need separate function for that.
If you just call getTextBounds() on a given string, the returned bounding box will already have that information.
For example:
paint.getTextBounds(exampleString1 , 0, exampleString1.length(), bounds);
if (bounds.bottom > 0) Log.i("Test", "String HAS descender");
else Log.i("Test", "String DOES NOT HAVE descender");
Simply saying bounds.top tells you the ascent of the string (it has negative value as Y axis 0 point is at the baseline of the string) and bounds.bottom tells you the descent of the string (which can be 0 or positive value for strings having descent).
You should have a look at Paint.FontMetrics. The descent member will give you "The recommended distance below the baseline for singled spaced text.".
I would like to get height too if possible.
You can use the getTextBounds(String text, int start, int end, Rect bounds) method of a Paint object. You can either use the paint object supplied by a TextView or build one yourself with your desired text appearance.
Using a Textview you Can do the following:
Rect bounds = new Rect();
Paint textPaint = textView.getPaint();
textPaint.getTextBounds(text, 0, text.length(), bounds);
int height = bounds.height();
int width = bounds.width();
If you just need the width you can use:
float width = paint.measureText(string);
http://developer.android.com/reference/android/graphics/Paint.html#measureText(java.lang.String)
There are two different width measures for a text. One is the number of pixels which has been drawn in the width, the other is the number of 'pixels' the cursor should be advanced after drawing the text.
paint.measureText and paint.getTextWidths returns the number of pixels (in float) which the cursor should be advanced after drawing the given string. For the number of pixels painted use paint.getTextBounds as mentioned in other answer. I believe this is called the 'Advance' of the font.
For some fonts these two measurements differ (alot), for instance the font Black Chancery have letters which extend past the other letters (overlapping) - see the capital 'L'. Use paint.getTextBounds as mentioned in other answer to get pixels painted.
I have measured width in this way:
String str ="Hiren Patel";
Paint paint = new Paint();
paint.setTextSize(20);
Typeface typeface = Typeface.createFromAsset(getAssets(), "Helvetica.ttf");
paint.setTypeface(typeface);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.FILL);
Rect result = new Rect();
paint.getTextBounds(str, 0, str.length(), result);
Log.i("Text dimensions", "Width: "+result.width());
This would help you.
Most likely you want to know the painted dimensions for a given string of text with a given font (i.e. a particular Typeface such as the “sans-serif” font family with a BOLD_ITALIC style, and particular size in sp or px).
Rather than inflating a full-blown TextView, you can go lower level and work with a Paint object directly for single-line text, for example:
// Maybe you want to construct a (possibly static) member for repeated computations
Paint paint = new Paint();
// You can load a font family from an asset, and then pick a specific style:
//Typeface plain = Typeface.createFromAsset(assetManager, pathToFont);
//Typeface bold = Typeface.create(plain, Typeface.DEFAULT_BOLD);
// Or just reference a system font:
paint.setTypeface(Typeface.create("sans-serif",Typeface.BOLD));
// Don't forget to specify your target font size. You can load from a resource:
//float scaledSizeInPixels = context.getResources().getDimensionPixelSize(R.dimen.mediumFontSize);
// Or just compute it fully in code:
int spSize = 18;
float scaledSizeInPixels = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
spSize,
context.getResources().getDisplayMetrics());
paint.setTextSize(scaledSizeInPixels);
// Now compute!
Rect bounds = new Rect();
String myString = "Some string to measure";
paint.getTextBounds(myString, 0, myString.length(), bounds);
Log.d(TAG, "width: " + bounds.width() + " height: " + bounds.height());
For multi-line or spanned text (SpannedString), consider using a StaticLayout, in which you provide the width and derive the height. For
a very elaborate answer on measuring and drawing text to a canvas in a custom view doing that, see: https://stackoverflow.com/a/41779935/954643
Also worth noting #arberg's reply below about the pixels painted vs the advance width ("number of pixels (in float) which the cursor should be advanced after drawing the given string"), in case you need to deal with that.
I'd like to share a better way (more versatile then the current accepted answer) of getting the exact width of a drawn text (String) with the use of static class StaticLayout:
StaticLayout.getDesiredWidth(text, textPaint))
this method is more accurate than textView.getTextBounds(), since you can calculate width of a single line in a multiline TextView, or you might not use TextView to begin with (for example in a custom View implementation).
This way is similar to textPaint.measureText(text), however it seems to be more accurate in rare cases.
simplay i tack max charcter in the line and defieded it with max space and create new line
v_y = v_y + 30;
String tx = "مبلغ وقدرة : "+ AmountChar+" لا غير";
myPaint.setTextAlign(Paint.Align.RIGHT);
int pxx = 400;
int pxy = v_y ;
int word_no = 1;
int word_lng = 0;
int max_word_lng = 45;
int new_line = 0;
int txt_lng = tx.length();
int words_lng =0;
String word_in_line = "" ;
for (String line : tx.split(" "))
{
word_lng = line.length() ;
words_lng += line.length() + 1;
if (word_no == 1 )
{word_in_line = line;
word_no += 1;
}
else
{ word_in_line += " " + line;
word_no += 1;
}
if (word_in_line.length() >= max_word_lng)
{
canvas.drawText(word_in_line, pxx, pxy, myPaint);
new_line += 1;
pxy = pxy + 30;
word_no = 1;
word_in_line = "";
}
if (txt_lng <= words_lng )
{ canvas.drawText(word_in_line, pxx, pxy, myPaint); }
}
v_y = pxy;