在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个回答

230

我找到了另一种使用静态布局的方法。这里的代码供任何人参考:

TextPaint mTextPaint=new TextPaint();
StaticLayout mTextLayout = new StaticLayout(mText, 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 = ...
textY = ...

canvas.translate(textX, textY);
mTextLayout.draw(canvas);
canvas.restore();

3
在我的看法中,有一个更好的解决方案。不需要将文本拆分成行。这尤其方便于文本在开头没有任何换行符或者我们不知道它是否有换行符的情况下。 - Ewoks
8
太棒了,它对我有用。我们能防止大文本超出画布高度吗? - moDev
1
非常有帮助,但是在居中StaticLayout时,请注意如何设置TextPaint()上的对齐方式。使用TextPaing.setTextAlign(Align.CENTER)会导致问题,因为不同的手机会对此做出不同的处理。 - greg7gkb
2
canvas.getWidth() 应该改为 getWidth() - getPaddingLeft() - getPaddingRight(),以考虑视图的填充。此外,请注意只有在文本或视图大小更改时才能计算 StaticLayout,并且可以在不构建新布局的情况下进行绘制,这可能更好! - Jules
1
@Eenvincible 你可以在这里查看我的博客文章:http://www.skoumal.net/en/android-drawing-multiline-text-on-bitmap/ - gingo
显示剩余4条评论

107

只需遍历每一行:

int x = 100, y = 100;
for (String line: text.split("\n")) {
      canvas.drawText(line, x, y, mTextPaint);
      y += mTextPaint.descent() - mTextPaint.ascent();
}

有没有一个合理的方法来计算新的y位置?添加一个看似随机的数字让我感到不太舒服... - AgentKnopf
1
如果你觉得上升+下降太小了,你可以添加一个常数间隙因子,或者按照自己的口味进行乘法运算(例如1.5行)。 - Dave
1
请注意,上升高度为负数。实际上您需要使用下降高度减去上升高度才能得到高度。 - Amir Uval
1
您可以获取所选字符的度量值,例如font.measure("Y")。 - GregD
自己这样做的问题在于,您无法使用Paint的getTextBounds等类似功能来查找边界框。Canvas或Paint是否有一些了解多行文本框的方法?那似乎是相当常见的需求。 - interstar

34

很遗憾,Android不知道\n是什么意思。你需要剔除\n,然后偏移Y值以使文本在下一行显示。像这样:

canvas.drawText("This is", 100, 100, mTextPaint);
canvas.drawText("multi-line", 100, 150, mTextPaint);
canvas.drawText("text", 100, 200, mTextPaint);

1
那么我需要将文本分成三个独立的块,然后调用三次 drawText() 函数,对吗? - Unpossible
5
是的,我刚刚添加了一个例子。使用String.Split在'\n'处拆分,然后偏移每个拆分结果。 - Icemanind
非常感谢您提供这个想法。 - Sumit Kumar
Android 知道什么是 \n,只需将其添加到字符串文本中即可。 - Danny RDX

16

我已经写了一个完整的示例

在此输入图片描述

colors.xml

  <color name="transparentBlack">#64000000</color>

Java类

 public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.amit);
        ImageView imageView = (ImageView)findViewById(R.id.imageView);
        imageView.setImageBitmap(drawTextToBitmap(this, bm, "Name: Kolala\nDate: Dec 23 2016 12:47 PM, \nLocation: 440 Banquets & Restaurents"));

    }

  public Bitmap drawTextToBitmap(Context gContext,
                                   Bitmap bitmap,
                                   String gText) {
        Resources resources = gContext.getResources();
        float scale = resources.getDisplayMetrics().density;

        android.graphics.Bitmap.Config bitmapConfig =
                bitmap.getConfig();
        // set default bitmap config if none
        if(bitmapConfig == null) {
            bitmapConfig = android.graphics.Bitmap.Config.ARGB_8888;
        }
        // resource bitmaps are imutable,
        // so we need to convert it to mutable one
        bitmap = bitmap.copy(bitmapConfig, true);

        Canvas canvas = new Canvas(bitmap);
        // new antialised Paint
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

        // text color - #3D3D3D
        paint.setColor(Color.WHITE);
        // text size in pixels
        paint.setTextSize((int) (25 * scale));
        // text shadow
        paint.setShadowLayer(1f, 0f, 1f, Color.WHITE);

        // draw text to the Canvas center
        Rect bounds = new Rect();

        int noOfLines = 0;
        for (String line: gText.split("\n")) {
           noOfLines++;
        }

        paint.getTextBounds(gText, 0, gText.length(), bounds);
        int x = 20;
        int y = (bitmap.getHeight() - bounds.height()*noOfLines);

        Paint mPaint = new Paint();
        mPaint.setColor(getResources().getColor(R.color.transparentBlack));
        int left = 0;
        int top = (bitmap.getHeight() - bounds.height()*(noOfLines+1));
        int right = bitmap.getWidth();
        int bottom = bitmap.getHeight();
        canvas.drawRect(left, top, right, bottom, mPaint);

        for (String line: gText.split("\n")) {
            canvas.drawText(line, x, y, paint);
            y += paint.descent() - paint.ascent();
        }

        return bitmap;
    }
}

4
为什么要使用循环来计算行数?int noOfLines = gText.split("\n").length。(注:原文中的 gText 表示一个字符串变量,split() 方法可以根据给定的分隔符将字符串拆分为数组。此处代码的意思是将 gText 字符串按照换行符 "\n" 进行拆分,然后返回数组的长度作为行数。) - Tomasz

9
这是我的解决方案,基于 @Dave 的答案(顺便说一下,感谢他;-))。
import android.graphics.Canvas;
import android.graphics.Paint;

public class mdCanvas
{
    private Canvas m_canvas;

    public mdCanvas(Canvas canvas)
    {
        m_canvas = canvas;
    }

    public void drawMultiline(String str, int x, int y, Paint paint)
    {
        for (String line: str.split("\n"))
        {
              m_canvas.drawText(line, x, y, paint);
              y += -paint.ascent() + paint.descent();
        }
    }
}

我尝试继承Canvas,但实际上它不允许你这样做。因此,这是一个中间类!

1
我尝试了这种方法...除了我的最长的一行的最后一个单词的最后一个字符只显示了一半之外,其他都正常工作。? - Aada

8
我必须在这里补充我的版本,其中考虑了笔画宽度。
void drawMultiLineText(String str, float x, float y, Paint paint, Canvas canvas) {
   String[] lines = str.split("\n");
   float txtSize = -paint.ascent() + paint.descent();       

   if (paint.getStyle() == Style.FILL_AND_STROKE || paint.getStyle() == Style.STROKE){
      txtSize += paint.getStrokeWidth(); //add stroke width to the text size
   }
   float lineSpace = txtSize * 0.2f;  //default line spacing

   for (int i = 0; i < lines.length; ++i) {
      canvas.drawText(lines[i], x, y + (txtSize + lineSpace) * i, paint);
   }
}

7

它会正常工作。我已经测试过了。

 public Bitmap drawMultilineTextToBitmap(Context gContext,
                                       int gResId,
                                       String gText) {    
      // prepare canvas
      Resources resources = gContext.getResources();
      float scale = resources.getDisplayMetrics().density;
      Bitmap bitmap = BitmapFactory.decodeResource(resources, gResId);

      android.graphics.Bitmap.Config bitmapConfig = bitmap.getConfig();
      // set default bitmap config if none
      if(bitmapConfig == null) {
        bitmapConfig = android.graphics.Bitmap.Config.ARGB_8888;
      }
      // resource bitmaps are imutable,
      // so we need to convert it to mutable one
      bitmap = bitmap.copy(bitmapConfig, true);

      Canvas canvas = new Canvas(bitmap);

      // new antialiased Paint
      TextPaint paint=new TextPaint(Paint.ANTI_ALIAS_FLAG);
      // text color - #3D3D3D
      paint.setColor(Color.rgb(61, 61, 61));
      // text size in pixels
      paint.setTextSize((int) (14 * scale));
      // text shadow
      paint.setShadowLayer(1f, 0f, 1f, Color.WHITE);

      // set text width to canvas width minus 16dp padding
      int textWidth = canvas.getWidth() - (int) (16 * scale);

      // init StaticLayout for text
      StaticLayout textLayout = new StaticLayout(
        gText, paint, textWidth, Layout.Alignment.ALIGN_CENTER, 1.0f, 0.0f, false);

      // get height of multiline text
      int textHeight = textLayout.getHeight();

      // get position of text's top left corner
      float x = (bitmap.getWidth() - textWidth)/2;
      float y = (bitmap.getHeight() - textHeight)/2;

      // draw text to the Canvas center
      canvas.save();
      canvas.translate(x, y);
      textLayout.draw(canvas);
      canvas.restore();

      return bitmap;
    }

来源: http://www.skoumal.net/en/android-drawing-multiline-text-on-bitmap/

本文介绍了如何在Android中将多行文本绘制到位图上。您需要使用Paint类和Canvas对象来设置字体、颜色和对齐方式,然后使用StaticLayout类将文本布局到位图上。请注意,这种方法适用于较短的文本段落,否则可能会出现性能问题。


当我使用Bitmap image = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.transparent_flag);时,它可以正常工作,但如果我使用TextView ID代替它,则无法工作。 - DKV
谢谢,它达到了我想要的效果,但如果您可以帮我编辑其中的文本或将其滑动到其他位置,就像 Photoshop 一样,那么再次感谢您。 - kvadityaaz

6
是的。使用canvas.getFontSpacing()作为增量即可。出于好奇,我自己尝试过,它适用于任何字号。

2
我想你指的是Paint.getFontSpacing - Jose M.

5

试一试

Paint paint1 = new Paint();
paint1.setStyle(Paint.Style.FILL);
paint1.setAntiAlias(true);
paint1.setColor(Color.BLACK);
paint1.setTextSize(15);


TextView tv = new TextView(context);
tv.setTextColor(Color.BLACK);
LinearLayout.LayoutParams llp = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
llp.setMargins(5, 2, 0, 0); // llp.setMargins(left, top, right, bottom);
tv.setLayoutParams(llp);
tv.setTextSize(10);
String text="this is good to see you , i am the king of the team";

tv.setText(text);
tv.setDrawingCacheEnabled(true);
tv.measure(MeasureSpec.makeMeasureSpec(canvas.getWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(canvas.getHeight(), MeasureSpec.EXACTLY));
tv.layout(0, 0, tv.getMeasuredWidth(), tv.getMeasuredHeight());
canvas.drawBitmap(tv.getDrawingCache(), 5, 10, paint1);
tv.setDrawingCacheEnabled(false);

8
我认为这是在onDraw方法中做事情的反面教材。 - rupps
@rupps 是的,在onDraw中包含所有这些可能是完全过度设计,但答案并没有告诉你这样做。而且这个想法很巧妙(它解决了我的问题)。静态布局和字符串分割都可以滚蛋了! - Rodia

4

我重新使用了GreenBee提出的解决方案,并编写了一个函数,在指定的边界内绘制一些多行文本,如果出现截断,则在结尾处添加“...”:

public static void drawMultiLineEllipsizedText(final Canvas _canvas, final TextPaint _textPaint, final float _left,
            final float _top, final float _right, final float _bottom, final String _text) {
        final float height = _bottom - _top;

        final StaticLayout measuringTextLayout = new StaticLayout(_text, _textPaint, (int) Math.abs(_right - _left),
                Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);

        int line = 0;
        final int totalLineCount = measuringTextLayout.getLineCount();
        for (line = 0; line < totalLineCount; line++) {
            final int lineBottom = measuringTextLayout.getLineBottom(line);
            if (lineBottom > height) {
                break;
            }
        }
        line--;

        if (line < 0) {
            return;
        }

        int lineEnd;
        try {
            lineEnd = measuringTextLayout.getLineEnd(line);
        } catch (Throwable t) {
            lineEnd = _text.length();
        }
        String truncatedText = _text.substring(0, Math.max(0, lineEnd));

        if (truncatedText.length() < 3) {
            return;
        }

        if (truncatedText.length() < _text.length()) {
            truncatedText = truncatedText.substring(0, Math.max(0, truncatedText.length() - 3));
            truncatedText += "...";
        }
        final StaticLayout drawingTextLayout = new StaticLayout(truncatedText, _textPaint, (int) Math.abs(_right
                - _left), Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);

        _canvas.save();
        _canvas.translate(_left, _top);
        drawingTextLayout.draw(_canvas);
        _canvas.restore();
    }

3
当文本被截断时,您的代码可能会将适合该空间的整个单词割掉。因此,这里有一个小建议来改进您的代码:用只包含三个点的一个字符“…”(在HTML中为…代码)替换三个字符“...”。然后,您只需删除一个字符(通常是一个空格),而不是三个字符,并保持单词未被切断: truncatedText = truncatedText.substring(0, Math.max(0, truncatedText.length() - 1)); - Asterius

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