Android Paint: .measureText () vs .getTextBounds ()

191

Saya mengukur teks menggunakan Paint.getTextBounds(), karena saya tertarik untuk mendapatkan tinggi dan lebar teks yang akan ditampilkan. Namun, teks yang sebenarnya diberikan selalu sedikit lebih lebar dari .width()dari Rectinformasi diisi oleh getTextBounds().

Yang mengejutkan saya, saya menguji .measureText(), dan menemukan bahwa itu mengembalikan nilai (lebih tinggi) yang berbeda. Saya mencobanya, dan ternyata benar.

Mengapa mereka melaporkan lebar yang berbeda? Bagaimana saya bisa mendapatkan tinggi dan lebar yang benar? Maksud saya, saya bisa menggunakan .measureText(), tetapi kemudian saya tidak akan tahu apakah saya harus mempercayai yang .height()dikembalikan oleh getTextBounds().

Seperti yang diminta, ini adalah kode minimal untuk mereproduksi masalah:

final String someText = "Hello. I believe I'm some text!";

Paint p = new Paint();
Rect bounds = new Rect();

for (float f = 10; f < 40; f += 1f) {
    p.setTextSize(f);

    p.getTextBounds(someText, 0, someText.length(), bounds);

    Log.d("Test", String.format(
        "Size %f, measureText %f, getTextBounds %d",
        f,
        p.measureText(someText),
        bounds.width())
    );
}

Outputnya menunjukkan bahwa perbedaannya tidak hanya menjadi lebih besar dari 1 (dan tidak ada kesalahan pembulatan menit terakhir), tetapi juga tampaknya meningkat dengan ukuran (saya akan menarik lebih banyak kesimpulan, tetapi mungkin sepenuhnya tergantung pada font):

D/Test    (  607): Size 10.000000, measureText 135.000000, getTextBounds 134
D/Test    (  607): Size 11.000000, measureText 149.000000, getTextBounds 148
D/Test    (  607): Size 12.000000, measureText 156.000000, getTextBounds 155
D/Test    (  607): Size 13.000000, measureText 171.000000, getTextBounds 169
D/Test    (  607): Size 14.000000, measureText 195.000000, getTextBounds 193
D/Test    (  607): Size 15.000000, measureText 201.000000, getTextBounds 199
D/Test    (  607): Size 16.000000, measureText 211.000000, getTextBounds 210
D/Test    (  607): Size 17.000000, measureText 225.000000, getTextBounds 223
D/Test    (  607): Size 18.000000, measureText 245.000000, getTextBounds 243
D/Test    (  607): Size 19.000000, measureText 251.000000, getTextBounds 249
D/Test    (  607): Size 20.000000, measureText 269.000000, getTextBounds 267
D/Test    (  607): Size 21.000000, measureText 275.000000, getTextBounds 272
D/Test    (  607): Size 22.000000, measureText 297.000000, getTextBounds 294
D/Test    (  607): Size 23.000000, measureText 305.000000, getTextBounds 302
D/Test    (  607): Size 24.000000, measureText 319.000000, getTextBounds 316
D/Test    (  607): Size 25.000000, measureText 330.000000, getTextBounds 326
D/Test    (  607): Size 26.000000, measureText 349.000000, getTextBounds 346
D/Test    (  607): Size 27.000000, measureText 357.000000, getTextBounds 354
D/Test    (  607): Size 28.000000, measureText 369.000000, getTextBounds 365
D/Test    (  607): Size 29.000000, measureText 396.000000, getTextBounds 392
D/Test    (  607): Size 30.000000, measureText 401.000000, getTextBounds 397
D/Test    (  607): Size 31.000000, measureText 418.000000, getTextBounds 414
D/Test    (  607): Size 32.000000, measureText 423.000000, getTextBounds 418
D/Test    (  607): Size 33.000000, measureText 446.000000, getTextBounds 441
D/Test    (  607): Size 34.000000, measureText 455.000000, getTextBounds 450
D/Test    (  607): Size 35.000000, measureText 468.000000, getTextBounds 463
D/Test    (  607): Size 36.000000, measureText 474.000000, getTextBounds 469
D/Test    (  607): Size 37.000000, measureText 500.000000, getTextBounds 495
D/Test    (  607): Size 38.000000, measureText 506.000000, getTextBounds 501
D/Test    (  607): Size 39.000000, measureText 521.000000, getTextBounds 515
slezica
sumber
Anda dapat melihat [jalanku] [1]. Itu bisa mendapatkan posisi dan ikatan yang benar. [1]: stackoverflow.com/a/19979937/1621354
Alex Chi
Penjelasan terkait tentang metode pengukuran teks untuk pengunjung lain untuk pertanyaan ini.
Suragch

Jawaban:

370

Anda dapat melakukan apa yang saya lakukan untuk memeriksa masalah seperti itu:

Pelajari kode sumber Android, sumber Paint.java, lihat kedua metode pengukuranText dan getTextBounds. Anda akan belajar bahwa mengukur mengukur panggilan native_measureText, dan getTextBounds panggilan nativeGetStringBounds, yang merupakan metode asli diimplementasikan dalam C ++.

Jadi, Anda akan terus belajar Paint.cpp, yang mengimplementasikan keduanya.

native_measureText -> SkPaintGlue :: MeasText_CII

nativeGetStringBounds -> SkPaintGlue :: getStringBounds

Sekarang studi Anda memeriksa di mana metode ini berbeda. Setelah beberapa pemeriksaan param, keduanya memanggil fungsi SkPaint :: MeasText di Skia Lib (bagian dari Android), tetapi keduanya memanggil form kelebihan beban yang berbeda.

Menggali lebih jauh ke Skia, saya melihat bahwa kedua panggilan menghasilkan komputasi yang sama dalam fungsi yang sama, hanya mengembalikan hasil yang berbeda.

Untuk menjawab pertanyaan Anda: Kedua panggilan Anda melakukan perhitungan yang sama. Kemungkinan perbedaan hasil terletak pada kenyataan bahwa getTextBounds mengembalikan batas sebagai integer, sementara mengukurText mengembalikan nilai float.

Jadi yang Anda dapatkan adalah pembulatan kesalahan selama konversi float ke int, dan ini terjadi di Paint.cpp di SkPaintGlue :: doTextBounds yang dipanggil untuk berfungsi SkRect :: roundOut.

Perbedaan antara lebar yang dihitung dari kedua panggilan tersebut mungkin secara maksimal 1.

EDIT 4 Okt 2011

Apa yang mungkin lebih baik daripada visualisasi. Saya mengambil upaya, untuk menjelajah sendiri, dan untuk mendapatkan hadiah yang layak :)

masukkan deskripsi gambar di sini

Ini adalah ukuran font 60, merah adalah batas persegi panjang, ungu adalah hasil dari sizeText.

Terlihat bahwa batas bagian kiri mulai beberapa piksel dari kiri, dan nilai dari sizeText bertambah dengan nilai ini di kiri dan kanan. Ini adalah sesuatu yang disebut nilai AdvanceX Glyph. (Saya menemukan ini di sumber Skia di SkPaint.cpp)

Jadi hasil dari tes ini adalah mengukurText menambahkan beberapa nilai muka pada teks di kedua sisi, sementara getTextBounds menghitung batas minimal di mana teks yang diberikan akan cocok.

Semoga hasil ini bermanfaat bagi Anda.

Kode pengujian:

  protected void onDraw(Canvas canvas){
     final String s = "Hello. I'm some text!";

     Paint p = new Paint();
     Rect bounds = new Rect();
     p.setTextSize(60);

     p.getTextBounds(s, 0, s.length(), bounds);
     float mt = p.measureText(s);
     int bw = bounds.width();

     Log.i("LCG", String.format(
          "measureText %f, getTextBounds %d (%s)",
          mt,
          bw, bounds.toShortString())
      );
     bounds.offset(0, -bounds.top);
     p.setStyle(Style.STROKE);
     canvas.drawColor(0xff000080);
     p.setColor(0xffff0000);
     canvas.drawRect(bounds, p);
     p.setColor(0xff00ff00);
     canvas.drawText(s, 0, bounds.bottom, p);
  }
Pointer Null
sumber
3
Maaf telah lama berkomentar, saya mengalami minggu yang sibuk. Terima kasih telah meluangkan waktu untuk menggali kode C ++! Sayangnya, perbedaannya lebih besar dari 1, dan itu terjadi bahkan ketika menggunakan nilai-nilai bulat - misalnya pengaturan ukuran teks menjadi 29,0 menghasilkan ukuranText () = 263 dan getTextBounds (). Width = 260
slezica
Silakan kirim kode minimal dengan pengaturan dan pengukuran Paint, yang dapat mereproduksi masalah.
Pointer Null
Diperbarui. Maaf lagi atas keterlambatannya.
slezica
Saya dengan Rich di sini. Penelitian hebat! Karunia sepenuhnya layak dan diberikan. Terima kasih atas waktu dan usaha Anda!
slezica
16
@ tikus, saya baru saja menemukan pertanyaan ini (saya OP). Saya lupa betapa luar biasanya jawaban Anda. Terima kasih dari masa depan! 100rp terbaik yang saya investasikan!
slezica
21

Pengalaman saya dengan ini adalah bahwa getTextBoundsakan mengembalikan persegi batas minimum absolut yang merangkum teks, belum tentu lebar terukur yang digunakan saat rendering. Saya juga ingin mengatakan bahwa measureTextmengasumsikan satu baris.

Untuk mendapatkan hasil pengukuran yang akurat, Anda harus menggunakan StaticLayout untuk merender teks dan mengeluarkan pengukuran.

Sebagai contoh:

String text = "text";
TextPaint textPaint = textView.getPaint();
int boundedWidth = 1000;

StaticLayout layout = new StaticLayout(text, textPaint, boundedWidth , Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
int height = layout.getHeight();
Mengejar
sumber
Tidak tahu tentang StaticLayout! getWidth () tidak akan berfungsi, itu hanya mengembalikan apa yang saya lewati di costructor (yang width, tidak maxWidth), tetapi getLineWith () akan. Saya juga menemukan metode statis getDesiredWidth (), meskipun tidak ada yang setara tinggi. Harap perbaiki jawabannya! Juga, saya benar-benar ingin tahu mengapa begitu banyak metode berbeda menghasilkan hasil yang berbeda.
slezica
Buruk saya tentang lebar. Jika Anda ingin lebar beberapa teks yang akan dirender pada satu baris, maka measureTextharus berfungsi dengan baik. Jika tidak, jika Anda mengetahui batasan lebar (seperti dalam onMeasureatau onLayout, Anda dapat menggunakan solusi yang saya posting di atas. Apakah Anda mencoba mengubah ukuran teks secara otomatis? Juga, alasan batas teks selalu sedikit lebih kecil adalah karena tidak termasuk padding karakter. , hanya kotak batas terkecil yang diperlukan untuk menggambar
Chase
Saya memang mencoba mengubah ukuran teks secara otomatis. Saya membutuhkan ketinggian juga, di situlah masalah saya dimulai! MeasWidth () berfungsi dengan baik jika tidak. Setelah itu, saya jadi penasaran mengapa pendekatan yang berbeda memberikan nilai yang berbeda
slezica
1
Satu hal yang juga perlu diperhatikan adalah bahwa tampilan teks yang membungkus dan multiline akan mengukur pada match_parent untuk lebar dan bukan hanya lebar yang diperlukan, yang membuat param lebar di atas agar StaticLayout valid. Jika Anda memerlukan pengubah ukuran teks, periksa posting saya di stackoverflow.com/questions/5033012/…
Chase
Saya perlu memiliki animasi ekspansi teks dan ini membantu banyak! Terima kasih!
kotak
18

Jawaban tikus itu hebat ... Dan di sini adalah deskripsi masalah sebenarnya:

Jawaban singkatnya adalah Paint.getTextBounds(String text, int start, int end, Rect bounds)pengembalian Rectyang tidak dimulai (0,0). Artinya, untuk mendapatkan lebar aktual teks yang akan ditetapkan dengan memanggil Canvas.drawText(String text, float x, float y, Paint paint)dengan objek Paint yang sama dari getTextBounds (), Anda harus menambahkan posisi kiri Rect. Sesuatu seperti itu:

public int getTextWidth(String text, Paint paint) {
    Rect bounds = new Rect();
    paint.getTextBounds(text, 0, end, bounds);
    int width = bounds.left + bounds.width();
    return width;
}

Perhatikan ini bounds.left- ini kunci masalahnya.

Dengan cara ini Anda akan menerima lebar teks yang sama, yang akan Anda terima menggunakan Canvas.drawText().

Dan fungsi yang sama seharusnya untuk mendapatkan heightteks:

public int getTextHeight(String text, Paint paint) {
    Rect bounds = new Rect();
    paint.getTextBounds(text, 0, end, bounds);
    int height = bounds.bottom + bounds.height();
    return height;
}

PS: Saya tidak menguji kode persis ini, tetapi menguji konsepsi.


Penjelasan lebih rinci diberikan dalam jawaban ini .

Prizoff
sumber
2
Rect mendefinisikan (b.width) menjadi (b.right-b.left) dan (b.height) menjadi (b.bottom-b.top). Oleh karena itu Anda dapat mengganti (b.left + b.width) dengan (b.right) dan (b.bottom + b.height) dengan (2 * b.bottom-b.top), tetapi saya pikir Anda ingin memiliki ( b.bottom-b.top), yang sama dengan (b.height). Saya pikir Anda benar tentang lebar (berdasarkan memeriksa sumber C ++), getTextWidth () mengembalikan nilai yang sama dengan (b.right), mungkin ada beberapa kesalahan pembulatan. (b adalah referensi untuk batas)
Nulano
11

Maaf telah menjawab lagi pertanyaan itu ... Saya harus menyematkan gambar.

Saya pikir hasil @ tikus ditemukan menyesatkan. Pengamatan mungkin benar untuk ukuran font 60 tetapi mereka berubah jauh lebih berbeda ketika teks lebih kecil. Misalnya. 10px. Dalam hal ini teks sebenarnya digambar di luar batas.

masukkan deskripsi gambar di sini

Sumber kode tangkapan layar:

  @Override
  protected void onDraw( Canvas canvas ) {
    for( int i = 0; i < 20; i++ ) {
      int startSize = 10;
      int curSize = i + startSize;
      paint.setTextSize( curSize );
      String text = i + startSize + " - " + TEXT_SNIPPET;
      Rect bounds = new Rect();
      paint.getTextBounds( text, 0, text.length(), bounds );
      float top = STEP_DISTANCE * i + curSize;
      bounds.top += top;
      bounds.bottom += top;
      canvas.drawRect( bounds, bgPaint );
      canvas.drawText( text, 0, STEP_DISTANCE * i + curSize, paint );
    }
  }
Moritz
sumber
Ah, misteri terus berlanjut. Saya masih tertarik dengan ini, meskipun penasaran. Beri tahu saya jika Anda menemukan hal lain, tolong!
slezica
Masalahnya dapat diperbaiki sedikit ketika Anda mengalikan ukuran font yang ingin Anda ukur dengan faktor katakanlah 20 dan kemudian bagilah hasilnya dengan itu. DARI Anda mungkin juga ingin menambahkan 1-2% lebih lebar dari ukuran yang diukur hanya untuk berada di sisi yang aman. Pada akhirnya Anda akan memiliki teks yang diukur sebagai panjang tetapi setidaknya tidak ada teks yang tumpang tindih.
Moritz
6
Untuk menggambar teks persis di dalam recting bounding Anda tidak harus menggambar Text dengan X = 0,0f, karena recting recting akan kembali seperti rect, tidak seperti lebar dan tinggi. Untuk menggambarnya dengan benar, Anda harus mengimbangi koordinat X pada nilai "-bounds.left", maka akan ditampilkan dengan benar.
Roman
10

PENOLAKAN: Solusi ini tidak 100% akurat dalam hal menentukan lebar minimum.

Saya juga mencari tahu bagaimana mengukur teks pada kanvas. Setelah membaca posting hebat dari tikus saya punya beberapa masalah tentang bagaimana mengukur teks multiline. Tidak ada cara yang jelas dari kontribusi ini tetapi setelah beberapa penelitian saya melintas di kelas StaticLayout. Ini memungkinkan Anda untuk mengukur teks multiline (teks dengan "\ n") dan mengonfigurasi lebih banyak properti teks Anda melalui Paint terkait.

Berikut ini cuplikan yang menunjukkan cara mengukur teks multiline:

private StaticLayout measure( TextPaint textPaint, String text, Integer wrapWidth ) {
    int boundedWidth = Integer.MAX_VALUE;
    if (wrapWidth != null && wrapWidth > 0 ) {
       boundedWidth = wrapWidth;
    }
    StaticLayout layout = new StaticLayout( text, textPaint, boundedWidth, Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false );
    return layout;
}

Wrapwitdh dapat menentukan apakah Anda ingin membatasi teks multiline hingga lebar tertentu.

Karena StaticLayout.getWidth () hanya mengembalikan ini boundedWidth Anda harus mengambil langkah lain untuk mendapatkan lebar maksimum yang diperlukan oleh teks multiline Anda. Anda dapat menentukan lebar setiap garis dan lebar maks adalah lebar garis tertinggi tentu saja:

private float getMaxLineWidth( StaticLayout layout ) {
    float maxLine = 0.0f;
    int lineCount = layout.getLineCount();
    for( int i = 0; i < lineCount; i++ ) {
        if( layout.getLineWidth( i ) > maxLine ) {
            maxLine = layout.getLineWidth( i );
        }
    }
    return maxLine;
}
Moritz
sumber
Anda tidak boleh melewati igetLineWidth (Anda hanya melewati 0 setiap kali)
Rich S
2

Ada cara lain untuk mengukur batas teks dengan tepat, pertama Anda harus mendapatkan path untuk Paint dan teks saat ini. Dalam kasus Anda itu harus seperti ini:

p.getTextPath(someText, 0, someText.length(), 0.0f, 0.0f, mPath);

Setelah itu Anda dapat menelepon:

mPath.computeBounds(mBoundsPath, true);

Dalam kode saya itu selalu mengembalikan nilai yang benar dan yang diharapkan. Tapi, tidak yakin apakah itu bekerja lebih cepat daripada pendekatan Anda.

Roma
sumber
Saya menemukan ini berguna karena saya melakukan getTextPath, untuk menggambar garis besar stroke di sekitar teks.
ToolmakerSteve
2

Perbedaan antara getTextBoundsdan measureTextdijelaskan dengan gambar di bawah ini.

Pendeknya,

  1. getTextBoundsadalah untuk mendapatkan RECT dari teks yang tepat. Ini measureTextadalah panjang teks, termasuk celah ekstra di kiri dan kanan.

  2. Jika ada spasi di antara teks, itu diukur measureTexttetapi tidak termasuk dalam panjang TextBounds, meskipun koordinat digeser.

  3. Teks dapat dimiringkan (miring). Dalam hal ini, sisi kiri kotak pembatas akan melebihi di luar pengukuran MeasText, dan panjang keseluruhan dari teks terikat akan lebih besar darimeasureText

  4. Teks bisa dimiringkan (Miring) dengan benar. Dalam hal ini, sisi kanan kotak pembatas akan melebihi di luar pengukuran MeasText, dan panjang keseluruhan dari teks terikat akan lebih besar darimeasureText

masukkan deskripsi gambar di sini

Elye
sumber
0

Inilah cara saya menghitung dimensi sebenarnya untuk huruf pertama (Anda dapat mengubah header metode yang sesuai dengan kebutuhan Anda, yaitu alih-alih char[]menggunakan String):

private void calculateTextSize(char[] text, PointF outSize) {
    // use measureText to calculate width
    float width = mPaint.measureText(text, 0, 1);

    // use height from getTextBounds()
    Rect textBounds = new Rect();
    mPaint.getTextBounds(text, 0, 1, textBounds);
    float height = textBounds.height();
    outSize.x = width;
    outSize.y = height;
}

Perhatikan bahwa saya menggunakan TextPaint, bukan kelas Paint asli.

milosmns
sumber