一个很快的问题,但我似乎找不到任何例子...... 我想通过Canvas
将多行文本写入自定义的View
中,在onDraw()
中我有:
...
String text = "This is\nmulti-line\ntext";
canvas.drawText(text, 100, 100, mTextPaint);
...
我本希望这样做可以实现换行,但是我看到的是一些加密的字符,它们应该在\n
的位置上。
欢迎提供任何指针。
保罗
一个很快的问题,但我似乎找不到任何例子...... 我想通过Canvas
将多行文本写入自定义的View
中,在onDraw()
中我有:
...
String text = "This is\nmulti-line\ntext";
canvas.drawText(text, 100, 100, mTextPaint);
...
我本希望这样做可以实现换行,但是我看到的是一些加密的字符,它们应该在\n
的位置上。
欢迎提供任何指针。
保罗
除了绘制多行文本之外,一个常见的问题是如何获取多行文本边界(例如,为了将其与画布对齐)。
默认的paint.getTextBounds()
在这种情况下不起作用,因为它仅会计量单行。
为了方便起见,我创建了这两个扩展函数:一个用于绘制多行文本,另一个用于获取文本边界。
private val textBoundsRect = Rect()
/**
* Draws multi line text on the Canvas with origin at (x,y), using the specified paint. The origin is interpreted
* based on the Align setting in the paint.
*
* @param text The text to be drawn
* @param x The x-coordinate of the origin of the text being drawn
* @param y The y-coordinate of the baseline of the text being drawn
* @param paint The paint used for the text (e.g. color, size, style)
*/
fun Canvas.drawTextMultiLine(text: String, x: Float, y: Float, paint: Paint) {
var lineY = y
for (line in text.split("\n")) {
drawText(line, x, lineY, paint)
lineY += paint.descent().toInt() - paint.ascent().toInt()
}
}
/**
* Retrieve the text boundary box, taking into account line breaks [\n] and store to [boundsRect].
*
* Return in bounds (allocated by the caller [boundsRect] or default mutable [textBoundsRect]) the smallest rectangle that
* encloses all of the characters, with an implied origin at (0,0).
*
* @param text string to measure and return its bounds
* @param start index of the first char in the string to measure. By default is 0.
* @param end 1 past the last char in the string to measure. By default is test length.
* @param boundsRect rect to save bounds. Note, you may not supply it. By default, it will apply values to the mutable [textBoundsRect] and return it.
* In this case it will be changed by each new this function call.
*/
fun Paint.getTextBoundsMultiLine(
text: String,
start: Int = 0,
end: Int = text.length,
boundsRect: Rect = textBoundsRect
): Rect {
getTextBounds(text, start, end, boundsRect)
val linesCount = text.split("\n").size
val allLinesHeight = (descent().toInt() - ascent().toInt()) * linesCount
boundsRect.bottom = boundsRect.top + allLinesHeight
return boundsRect
}
canvas.drawTextMultiLine(text, x, y, yourPaint)
用于测量文本:
val bounds = yourPaint.getTextBoundsMultiLine(text)
在这种情况下,它将测量从开头到结尾的所有文本,并使用默认分配的(可变)Rect。 您可以尝试传递额外的参数以增加灵活性。
// end results in the "largest" substring
val end = text.split("\n").map { measureText(it) }.max().toInt()
- goemic//Get post text
String text = post.getText();
//Get weight of space character in px
float spaceWeight = paint.measureText(" ");
//Start main algorithm of drawing words on canvas
//Split text to words
for (String line : text.split(" ")) {
//If we had empty space just continue
if (line.equals("")) continue;
//Get weight of the line
float lineWeight = paint.measureText(line);
//If our word(line) doesn't have any '\n' we do next
if (line.indexOf('\n') == -1) {
//If word can fit into current line
if (cnv.getWidth() - pxx - defaultMargin >= lineWeight) {
//Draw text
cnv.drawText(line, pxx, pxy, paint);
//Move start x point to word weight + space weight
pxx += lineWeight + spaceWeight;
} else {
//If word can't fit into current line
//Move x point to start
//Move y point to the next line
pxx = defaultMargin;
pxy += paint.descent() - paint.ascent();
//Draw
cnv.drawText(line, pxx, pxy, paint);
//Move x point to word weight + space weight
pxx += lineWeight + spaceWeight;
}
//If line contains '\n'
} else {
//If '\n' is on the start of the line
if (line.indexOf('\n') == 0) {
pxx = defaultMargin;
pxy += paint.descent() - paint.ascent();
cnv.drawText(line.replaceAll("\n", ""), pxx, pxy, paint);
pxx += lineWeight + spaceWeight;
} else {
//If '\n' is somewhere in the middle
//and it also can contain few '\n'
//Split line to sublines
String[] subline = line.split("\n");
for (int i = 0; i < subline.length; i++) {
//Get weight of new word
lineWeight = paint.measureText(subline[i]);
//If it's empty subline that's mean that we have '\n'
if (subline[i].equals("")) {
pxx = defaultMargin;
pxy += paint.descent() - paint.ascent();
cnv.drawText(subline[i], pxx, pxy, paint);
continue;
}
//If we have only one word
if (subline.length == 1 && i == 0) {
if (cnv.getWidth() - pxx >= lineWeight) {
cnv.drawText(subline[0], pxx, pxy, paint);
pxx = defaultMargin;
pxy += paint.descent() - paint.ascent();
} else {
pxx = defaultMargin;
pxy += paint.descent() - paint.ascent();
cnv.drawText(subline[0], pxx, pxy, paint);
pxx = defaultMargin;
pxy += paint.descent() - paint.ascent();
}
continue;
}
//If we have set of words separated with '\n'
//it is the first word
//Make sure we can put it into current line
if (i == 0) {
if (cnv.getWidth() - pxx >= lineWeight) {
cnv.drawText(subline[0], pxx, pxy, paint);
pxx = defaultMargin;
} else {
pxx = defaultMargin;
pxy += paint.descent() - paint.ascent();
cnv.drawText(subline[0], pxx, pxy, paint);
pxx = defaultMargin;
}
} else {
pxx = defaultMargin;
pxy += paint.descent() - paint.ascent();
cnv.drawText(subline[i], pxx, pxy, paint);
pxx += lineWeight + spaceWeight;
}
}
}
}
}
public static Bitmap getBitmapFromString(final String text, final String font, int textSize, final int textColor)
{
String lines[] = text.split("\n");
textSize = getRelX(textSize); //a method in my app that adjusts the font size relative to the screen size
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setTextSize(textSize);
paint.setColor(textColor);
paint.setTextAlign(Paint.Align.LEFT);
Typeface face = Typeface.createFromAsset(GameActivity.getContext().getAssets(),GameActivity.getContext().getString(R.string.font) + font + GameActivity.getContext().getString(R.string.font_ttf));
paint.setTypeface(face);
float baseline = -paint.ascent(); // ascent() is negative
int width = (int) (paint.measureText(text) + 0.5f); // round
int height = (int) (baseline + paint.descent() + 0.5f);
Bitmap image = Bitmap.createBitmap(width, (int)(height * 1.3 * lines.length), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(image);
for (int i = 0; i < lines.length; ++i)
{
canvas.drawText(lines[i], 0, baseline + textSize * 1.3f * i, paint);
}
return image;
}
我设计了一种更好的方式(我无法确定它是否更好,但这种方法应该很容易)用于在画布中处理多行文本,例如在SurfaceView中。
以下是代码:
public class MultiLineText implements ObjectListener {
private String[] lines;
private float x, y, textSize;
private int textColor;
private float currentY;
public MultiLineText(String[] lines, float x, float y, float textSize, int textColor) {
this.lines = lines;
this.x = x;
this.y = y;
this.textSize = textSize;
this.textColor = textColor;
}
@Override
public void draw(Canvas canvas) {
Paint paint = new Paint();
paint.setColor(textColor);
paint.setTextSize(textSize);
currentY = y;
for (int i = 0; i < lines.length; i++) {
if (i == 0)
canvas.drawText(lines[i], x, y, paint);
else {
currentY = currentY + textSize;
canvas.drawText(lines[i], x, currentY, paint);
}
}
}
@Override
public void update() {
}
}
导入两个类:import android.graphics.Canvas;
和 import android.graphics.Paint;
,以确保不会出现错误。
在简单的情况下,创建一个名为“ObjectListener”的接口类(或者您想要称呼它的任何名称,只需更改名称即可),并添加以下两行代码:
void draw(Canvas canvas);
void update();
要实现这个功能,在视图或渲染器中使用以下代码的draw(Canvas canvas)
方法:
new MultiLineText(new String[]{
"This is a multi-line text.",
"It's setup is basic. Just do the following code,",
"and you would be done."
}, 150, 150, 32, Color.WHITE).draw(canvas);
抱歉,我只是想实现这段文本,所以... 您可以将X和Y坐标从150更改为您喜欢的值。26号字体大小可读性良好,并且不会太大,因为Canvas以像素大小呈现文本。
Typeface font = ResourcesCompat.getFont(this, R.font.sf_pro_text_bold);
Bitmap textBm = BitmapUtils.createBitmapFromText("This is longgggggg texttttttttttttttttttt \n This is line breakkkkkkkkkkk"
, width // Container width
, Color.BLACK
, 40
, font
, TextAlign.CENTER
, 10f
, true);
public enum TextAlign {
CENTER,
LEFT,
RIGHT
}
/**
* Create bit map from text with auto line break
* */
public static Bitmap createBitmapFromText(String text, int containerWidth, @ColorInt int textColor, int textSize
, @Nullable Typeface textFont, @Nullable TextAlign textAlign
, @Nullable Float lineSpace, @Nullable Boolean trimLine) {
Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
textPaint.setTypeface(textFont);
textPaint.setTextSize(textSize);
textPaint.setColor(textColor);
ArrayList<String> linesText = splitTextToMultiLine(text, containerWidth, textSize, textFont);
if(lineSpace == null) {
lineSpace = 10f;
}
int bmWidth = containerWidth;
int bmHeight = 0;
for(String line : linesText) {
int lineHeight = (int)(calculateTextHeightFromFontSize(line, textSize, textFont)*1.1);
bmHeight += (lineHeight + lineSpace);
}
Bitmap result = Bitmap.createBitmap(bmWidth, bmHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(result);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
TextPaint staticLayoutPaint = new TextPaint();
staticLayoutPaint.setTextSize(textSize);
staticLayoutPaint.setTypeface(textFont);
Layout.Alignment align = Layout.Alignment.ALIGN_CENTER;
if(textAlign == TextAlign.LEFT) {
align = Layout.Alignment.ALIGN_NORMAL;
} else if(textAlign == TextAlign.RIGHT) {
align = Layout.Alignment.ALIGN_OPPOSITE;
}
StaticLayout.Builder staticLayoutBuilder = StaticLayout.Builder
.obtain("", 0, 0, staticLayoutPaint, containerWidth)
.setAlignment(align)
.setLineSpacing(lineSpace, 1)
.setText(text);
staticLayoutBuilder.build().draw(canvas);
} else {
float xOffset = 0;
float yOffset = -textPaint.ascent(); // ascent() is negative;
for(String lineText : linesText) {
if(trimLine) {
lineText = lineText.trim();
}
if(textAlign == TextAlign.RIGHT) {
xOffset = containerWidth - calculateTextWidthFromFontSize(lineText, textSize, textFont);
} else if (textAlign == TextAlign.CENTER){
xOffset = (containerWidth - calculateTextWidthFromFontSize(lineText, textSize, textFont)) / 2;
}
canvas.drawText(lineText, xOffset, yOffset, textPaint);
float nextLineOffset = calculateTextHeightFromFontSize(lineText, textSize, textFont) + lineSpace;
yOffset += nextLineOffset;
}
}
return result;
}
private static int calculateTextWidthFromFontSize(String text, int textSize, @Nullable Typeface textFont) {
String singleChar = text;
Rect bounds = new Rect();
Paint paint = new Paint();
paint.setTextSize(textSize);
paint.setTypeface(textFont);
paint.getTextBounds(singleChar, 0, singleChar.length(), bounds);
return bounds.width();
}
private static int calculateTextHeightFromFontSize(String text, int textSize, @Nullable Typeface textFont) {
String singleChar = text;
Rect bounds = new Rect();
Paint paint = new Paint();
paint.setTextSize(textSize);
paint.setTypeface(textFont);
paint.getTextBounds(singleChar, 0, singleChar.length(), bounds);
return bounds.height();
}
private static ArrayList<String> splitTextToMultiLine(String input, int containerWidth, int textSize, @Nullable Typeface textFont) {
ArrayList<String> result = new ArrayList<>();
//Split String by line break first
String[] multiLine = input.split("\n");
for(String line : multiLine) {
result.addAll(splitLongStringToMultiLine(line, containerWidth, textSize, textFont));
}
return result;
}
/**
* Split long string (without line break) to multi line
* */
private static ArrayList<String> splitLongStringToMultiLine(String input, int containerWidth, int textSize, @Nullable Typeface textFont) {
ArrayList<String> result = new ArrayList<>();
//Reduce loop performance
int singleTextWidth = calculateTextWidthFromFontSize("A", textSize, textFont);
int minTextPerLine = containerWidth/singleTextWidth;
if(minTextPerLine >= input.length()
|| calculateTextWidthFromFontSize(input, textSize, textFont) < containerWidth) {
result.add(input);
return result;
}
int startSplit = 0;
while (startSplit < input.length()) {
int addedTextPerLine = 0;
if(startSplit + minTextPerLine < input.length()) {
int endPos = startSplit + minTextPerLine;
String availableChild = input.substring(startSplit, endPos);
//Detect more character in line
int updatePos = endPos;
while (updatePos < input.length()
&& calculateTextWidthFromFontSize(availableChild, textSize, textFont) < containerWidth) {
availableChild = input.substring(startSplit, updatePos);
addedTextPerLine++;
updatePos = endPos + addedTextPerLine;
}
//Detect last space char and split
int spaceIndex = availableChild.lastIndexOf(" ");
if(spaceIndex > 0) {
//Update split point
startSplit -= (availableChild.length() - spaceIndex);
availableChild = availableChild.substring(0, spaceIndex);
} else{
//No space found ->
}
result.add(availableChild);
} else {
//Last line
String child = input.substring(startSplit);
result.add(child);
}
startSplit += minTextPerLine;
startSplit += addedTextPerLine;
addedTextPerLine = 0;
}
if(result.size() == 0) {
//Cheat
result.add(input);
}
return result;
}
这是我的解决方案。它不完美,但对我有用。
public static Bitmap textAsBitmap(String text, float textSize, int textColor) {
int lines = 1;
String lineString1 = "", lineString2 = "";
String[] texts = text.split(" ");
if (texts.length > 2) {
for (int i = 0; i < 2; i++) {
lineString1 = lineString1.concat(texts[i] + " ");
}
for (int i = 2; i < texts.length; i++) {
lineString2 = lineString2.concat(texts[i] + "");
}
} else {
lineString1 = text;
}
lineString1 = lineString1.trim();
lineString2 = lineString2.trim();
String[] lastText = new String[2];
lastText[0] = lineString1;
if (!lineString2.equals("")) {
lines = 2;
lastText[1] = lineString2;
}
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setTextSize(textSize);
paint.setColor(textColor);
paint.setTextAlign(Paint.Align.LEFT);
float baseline = -paint.ascent(); // ascent() is negative
String maxLengthText = "";
if (lines == 2) {
if (lineString1.length() > lineString2.length()) {
maxLengthText = maxLengthText.concat(lineString1);
} else {
maxLengthText = maxLengthText.concat(lineString2);
}
} else {
maxLengthText = maxLengthText.concat(text);
}
int width = (int) (paint.measureText(maxLengthText) + 0.5f); // round
int height = (int) ((baseline + paint.descent() + 0.5f) * lines);
Bitmap image = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(image);
for (int i = 0; i < lines; i++) {
canvas.drawText(lastText[i], 0, baseline, paint);
baseline *= lines;
}
return image;
}
我遇到了类似的问题,但我应该返回文本路径。 你可以在画布上绘制这条路径。 这是我的代码。我使用Break Text和path.op。
public Path createClipPath(int width, int height) {
final Path path = new Path();
if (textView != null) {
mText = textView.getText().toString();
mTextPaint = textView.getPaint();
float text_position_x = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
text_position_x = findTextBounds(textView).left;
}
boolean flag = true;
int line = 0;
int startPointer = 0;
int endPointer = mText.length();
while (flag) {
Path p = new Path();
int breakText = mTextPaint.breakText(mText.substring(startPointer), true, width, null);
mTextPaint.getTextPath(mText, startPointer, startPointer + breakText, text_position_x,
textView.getBaseline() + mTextPaint.getFontSpacing() * line, p);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
path.op(p, Path.Op.UNION);
}
endPointer -= breakText;
startPointer += breakText;
line++;
if (endPointer == 0) {
flag = false;
}
}
}
return path;
}
而为了找到文本边界,我使用了这个函数
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
private Rect findTextBounds(TextView textView) {
// Force measure of text pre-layout.
textView.measure(0, 0);
String s = (String) textView.getText();
// bounds will store the rectangle that will circumscribe the text.
Rect bounds = new Rect();
Paint textPaint = textView.getPaint();
// Get the bounds for the text. Top and bottom are measured from the baseline. Left
// and right are measured from 0.
textPaint.getTextBounds(s, 0, s.length(), bounds);
int baseline = textView.getBaseline();
bounds.top = baseline + bounds.top;
bounds.bottom = baseline + bounds.bottom;
int startPadding = textView.getPaddingStart();
bounds.left += startPadding;
// textPaint.getTextBounds() has already computed a value for the width of the text,
// however, Paint#measureText() gives a more accurate value.
bounds.right = (int) textPaint.measureText(s, 0, s.length()) + startPadding;
return bounds;
}
我的例子使用动态文本大小和间距,对我来说非常有效...
public Bitmap fontTexture(String string, final Context context) {
float text_x = 512;
float text_y = 512;
final float scale = context.getResources().getDisplayMetrics().density;
int mThreshold = (int) (THRESHOLD_DIP * scale + 0.5f);
String[] splited = string.split("\\s+");
double longest = 0;
for(String s:splited){
if (s.length() > longest) {
longest = s.length();
}
}
if(longest > MAX_STRING_LENGTH) {
double ratio = (double) MAX_STRING_LENGTH / longest;
mThreshold = (int) ((THRESHOLD_DIP * ((float) ratio)) * scale + 0.5f);
}
Bitmap bitmap = Bitmap.createBitmap(1024, 1024, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Typeface font = Typeface.createFromAsset(context.getAssets(),
"fonts/dotted_font.ttf");
TextPaint mTextPaint=new TextPaint();
mTextPaint.setColor(Color.YELLOW);
mTextPaint.setTextAlign(Paint.Align.CENTER);
mTextPaint.setTextSize(mThreshold);
mTextPaint.setTypeface(font);
StaticLayout mTextLayout = new StaticLayout(string, mTextPaint, canvas.getWidth(), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
canvas.save();
canvas.translate(text_x, text_y);
mTextLayout.draw(canvas);
canvas.restore();
return bitmap;
}
Layout
而不是直接调用Canvas.drawText
。这个问答展示了如何使用StaticLayout
来绘制多行文本。 - Suragch