在Canvas上绘制多行文本

137

一个很快的问题,但我似乎找不到任何例子...... 我想通过Canvas将多行文本写入自定义的View中,在onDraw()中我有:

...
String text = "This is\nmulti-line\ntext";
canvas.drawText(text, 100, 100, mTextPaint);
...

我本希望这样做可以实现换行,但是我看到的是一些加密的字符,它们应该在\n的位置上。

欢迎提供任何指针。

保罗


1
文档建议使用Layout而不是直接调用Canvas.drawText这个问答展示了如何使用StaticLayout来绘制多行文本。 - Suragch
19个回答

3

除了绘制多行文本之外,一个常见的问题是如何获取多行文本边界(例如,为了将其与画布对齐)。 默认的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。 您可以尝试传递额外的参数以增加灵活性。


“getTextBoundsMultiLine”的“end”计算是错误的,它在分割文本之前计算宽度。因此,矩形结果为字符串的单行宽度!请使用以下内容正确计算“end”:// end results in the "largest" substring val end = text.split("\n").map { measureText(it) }.max().toInt() - goemic

3
没有使用StaticLayout的解决方案
//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;
                    }
                }

            }
        }
    }

你太棒了,伙计!干得好!我真的很喜欢它。你能否帮忙扩展你的代码,使其能够在第一页达到限制后处理下一页?在我的应用程序中,我使用画布创建PDF(A4 595x842)。我可以调整页面的宽度,但如果文本很长,我需要继续下一页,以此类推。非常感谢您的帮助。 - Mark Delphi

2
我使用了已有的内容,这些内容已经将单行转换为画布。我参考了Lumis的答案,最终得出了以下结果。其中1.3和1.3f是相对于字体大小用来填充行间距的。
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;
}

0

我设计了一种更好的方式(我无法确定它是否更好,但这种方法应该很容易)用于在画布中处理多行文本,例如在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以像素大小呈现文本。


现在我正在2023年阅读它,我现在对这个答案感到非常尴尬。 - BeChris 100

0
希望这可以帮到你,你可以在画布中设置文本字体、对齐文本。
        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;
}

0

0

这是我的解决方案。它不完美,但对我有用。

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;
}

0

我遇到了类似的问题,但我应该返回文本路径。 你可以在画布上绘制这条路径。 这是我的代码。我使用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;
}

-1

我的例子使用动态文本大小和间距,对我来说非常有效...

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;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接